mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-21 21:09:00 +00:00
Compare commits
2 Commits
feat/stagi
...
fix-pr-491
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a9ee1a71c | ||
|
|
80685c3b4b |
@@ -210,52 +210,29 @@ echo "Last stable release: $LAST_STABLE"
|
||||
echo "WARNING: PR #$PR not on main branch!"
|
||||
done
|
||||
```
|
||||
3. Create standardized release notes using this exact template:
|
||||
3. Create comprehensive release notes including:
|
||||
- **Version Change**: Show version bump details
|
||||
- **Changelog** grouped by type:
|
||||
- 🚀 **Features** (feat:)
|
||||
- 🐛 **Bug Fixes** (fix:)
|
||||
- 💥 **Breaking Changes** (BREAKING CHANGE)
|
||||
- 📚 **Documentation** (docs:)
|
||||
- 🔧 **Maintenance** (chore:, refactor:)
|
||||
- ⬆️ **Dependencies** (deps:, dependency updates)
|
||||
- **Litegraph Changes** (if version updated):
|
||||
- 🚀 Features: ${LITEGRAPH_FEATURES}
|
||||
- 🐛 Bug Fixes: ${LITEGRAPH_FIXES}
|
||||
- 💥 Breaking Changes: ${LITEGRAPH_BREAKING}
|
||||
- 🔧 Other Changes: ${LITEGRAPH_OTHER}
|
||||
- **Other Major Dependencies**: ${OTHER_DEP_CHANGES}
|
||||
- Include PR numbers and links
|
||||
- Add issue references (Fixes #123)
|
||||
4. **Save release notes:**
|
||||
```bash
|
||||
cat > release-notes-${NEW_VERSION}.md << 'EOF'
|
||||
## ⚠️ Breaking Changes
|
||||
<!-- List breaking changes if any, otherwise remove this entire section -->
|
||||
- Breaking change description (#PR_NUMBER)
|
||||
|
||||
---
|
||||
|
||||
## What's Changed
|
||||
|
||||
### 🚀 Features
|
||||
<!-- List features here, one per line with PR reference -->
|
||||
- Feature description (#PR_NUMBER)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
<!-- List bug fixes here, one per line with PR reference -->
|
||||
- Bug fix description (#PR_NUMBER)
|
||||
|
||||
### 🔧 Maintenance
|
||||
<!-- List refactoring, chore, and other maintenance items -->
|
||||
- Maintenance item description (#PR_NUMBER)
|
||||
|
||||
### 📚 Documentation
|
||||
<!-- List documentation changes if any, remove section if empty -->
|
||||
- Documentation update description (#PR_NUMBER)
|
||||
|
||||
### ⬆️ Dependencies
|
||||
<!-- List dependency updates -->
|
||||
- Updated dependency from vX.X.X to vY.Y.Y (#PR_NUMBER)
|
||||
|
||||
**Full Changelog**: https://github.com/Comfy-Org/ComfyUI_frontend/compare/${BASE_TAG}...v${NEW_VERSION}
|
||||
EOF
|
||||
# Save release notes for PR and GitHub release
|
||||
echo "$RELEASE_NOTES" > release-notes-${NEW_VERSION}.md
|
||||
```
|
||||
4. **Parse commits and populate template:**
|
||||
- Group commits by conventional commit type (feat:, fix:, chore:, etc.)
|
||||
- Extract PR numbers from commit messages
|
||||
- For breaking changes, analyze if changes affect:
|
||||
- Public APIs (app object, api module)
|
||||
- Extension/workspace manager APIs
|
||||
- Node schema, workflow schema, or other public schemas
|
||||
- Any other public-facing interfaces
|
||||
- For dependency updates, list version changes with PR numbers
|
||||
- Remove empty sections (e.g., if no documentation changes)
|
||||
- Ensure consistent bullet format: `- Description (#PR_NUMBER)`
|
||||
5. **CONTENT REVIEW**: Release notes follow standard format?
|
||||
5. **CONTENT REVIEW**: Release notes clear and comprehensive with dependency details?
|
||||
|
||||
### Step 9: Create Version Bump PR
|
||||
|
||||
@@ -296,12 +273,38 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
--body-file release-notes-${NEW_VERSION}.md \
|
||||
--label "Release"
|
||||
```
|
||||
3. **Update PR with release notes:**
|
||||
3. **Add required sections to PR body:**
|
||||
```bash
|
||||
# For workflow-created PRs, update the body with our release notes
|
||||
gh pr edit ${PR_NUMBER} --body-file release-notes-${NEW_VERSION}.md
|
||||
# Create PR body with release notes plus required sections
|
||||
cat > pr-body.md << EOF
|
||||
${RELEASE_NOTES}
|
||||
|
||||
## Breaking Changes
|
||||
${BREAKING_CHANGES:-None}
|
||||
|
||||
## Testing Performed
|
||||
- ✅ Full test suite (unit, component)
|
||||
- ✅ TypeScript compilation
|
||||
- ✅ Linting checks
|
||||
- ✅ Build verification
|
||||
- ✅ Security audit
|
||||
|
||||
## Distribution Channels
|
||||
- GitHub Release (with dist.zip)
|
||||
- PyPI Package (comfyui-frontend-package)
|
||||
- npm Package (@comfyorg/comfyui-frontend-types)
|
||||
|
||||
## Post-Release Tasks
|
||||
- [ ] Verify all distribution channels
|
||||
- [ ] Update external documentation
|
||||
- [ ] Monitor for issues
|
||||
EOF
|
||||
```
|
||||
4. **PR REVIEW**: Version bump PR created with standardized release notes?
|
||||
4. Update PR with enhanced description:
|
||||
```bash
|
||||
gh pr edit ${PR_NUMBER} --body-file pr-body.md
|
||||
```
|
||||
5. **PR REVIEW**: Version bump PR created and enhanced correctly?
|
||||
|
||||
### Step 10: Critical Release PR Verification
|
||||
|
||||
@@ -589,46 +592,55 @@ The command implements multiple quality gates:
|
||||
- Draft release status
|
||||
- Python package specs require that prereleases use alpha/beta/rc as the preid
|
||||
|
||||
## Critical Implementation Notes
|
||||
## Common Issues and Solutions
|
||||
|
||||
When executing this release process, pay attention to these key aspects:
|
||||
### Issue: Pre-release Version Confusion
|
||||
**Problem**: Not sure whether to promote pre-release or create new version
|
||||
**Solution**:
|
||||
- Follow semver standards: a prerelease version is followed by a normal release. It should have the same major, minor, and patch versions as the prerelease.
|
||||
|
||||
### Version Handling
|
||||
- For pre-release versions (e.g., 1.24.0-rc.1), the next stable release should be the same version without the suffix (1.24.0)
|
||||
- Never skip version numbers - follow semantic versioning strictly
|
||||
### Issue: Wrong Commit Count
|
||||
**Problem**: Changelog includes commits from other branches
|
||||
**Solution**: Always use `--first-parent` flag with git log
|
||||
|
||||
### Commit History Analysis
|
||||
- **ALWAYS** use `--first-parent` flag with git log to avoid including commits from merged feature branches
|
||||
- Verify PR merge targets before including them in changelogs:
|
||||
```bash
|
||||
gh pr view ${PR_NUMBER} --json baseRefName
|
||||
```
|
||||
**Update**: Sometimes update-locales doesn't add [skip ci] - always verify!
|
||||
|
||||
### Release Workflow Triggers
|
||||
- The "Release" label on the PR is **CRITICAL** - without it, PyPI/npm publishing won't occur
|
||||
- Check for `[skip ci]` in commit messages before merging - this blocks the release workflow
|
||||
- If you encounter `[skip ci]`, push an empty commit to override it:
|
||||
```bash
|
||||
git commit --allow-empty -m "Trigger release workflow"
|
||||
```
|
||||
### Issue: Missing PRs in Changelog
|
||||
**Problem**: PR was merged to different branch
|
||||
**Solution**: Verify PR merge target with:
|
||||
```bash
|
||||
gh pr view ${PR_NUMBER} --json baseRefName
|
||||
```
|
||||
|
||||
### PR Creation Details
|
||||
- Version bump PRs come from `comfy-pr-bot`, not `github-actions`
|
||||
- The workflow typically completes in 20-30 seconds
|
||||
- Always wait for the PR to be created before trying to edit it
|
||||
### Issue: Incomplete Dependency Changelog
|
||||
**Problem**: Litegraph or other dependency updates only show version bump, not actual changes
|
||||
**Solution**: The command now automatically:
|
||||
- Detects litegraph version changes between releases
|
||||
- Clones the litegraph repository temporarily
|
||||
- Extracts and categorizes changes between versions
|
||||
- Includes detailed litegraph changelog in release notes
|
||||
- Cleans up temporary files after analysis
|
||||
|
||||
### Breaking Changes Detection
|
||||
- Analyze changes to public-facing APIs:
|
||||
- The `app` object and its methods
|
||||
- The `api` module exports
|
||||
- Extension and workspace manager interfaces
|
||||
- Node schema, workflow schema, and other public schemas
|
||||
- Any modifications to these require marking as breaking changes
|
||||
### Issue: Release Failed Due to [skip ci]
|
||||
**Problem**: Release workflow didn't trigger after merge
|
||||
**Prevention**: Always avoid this scenario
|
||||
- Ensure that `[skip ci]` or similar flags are NOT in the `HEAD` commit message of the PR
|
||||
- Push a new, empty commit to the PR
|
||||
- Always double-check this immediately before merging
|
||||
|
||||
### Recovery Procedures
|
||||
If the release workflow fails to trigger:
|
||||
1. Create a revert PR to restore the previous version
|
||||
2. Merge the revert
|
||||
3. Re-run the version bump workflow
|
||||
4. This approach is cleaner than creating extra version numbers
|
||||
**Recovery Strategy**:
|
||||
1. Revert version in a new PR (e.g., 1.24.0 → 1.24.0-1)
|
||||
2. Merge the revert PR
|
||||
3. Run version bump workflow again
|
||||
4. This creates a fresh PR without [skip ci]
|
||||
Benefits: Cleaner than creating extra version numbers
|
||||
|
||||
## Key Learnings & Notes
|
||||
|
||||
1. **PR Author**: Version bump PRs are created by `comfy-pr-bot`, not `github-actions`
|
||||
2. **Workflow Speed**: Version bump workflow typically completes in ~20-30 seconds
|
||||
3. **Update-locales Behavior**: Inconsistent - sometimes adds [skip ci], sometimes doesn't
|
||||
4. **Recovery Options**: Reverting version is cleaner than creating extra versions
|
||||
5. **Dependency Tracking**: Command now automatically includes litegraph and major dependency changes in changelogs
|
||||
6. **Litegraph Integration**: Temporary cloning of litegraph repo provides detailed change analysis between versions
|
||||
|
||||
|
||||
@@ -138,50 +138,14 @@ For each commit:
|
||||
```bash
|
||||
gh pr create --base core/X.Y --head release/1.23.5 \
|
||||
--title "[Release] v1.23.5" \
|
||||
--body "Release notes will be added shortly..." \
|
||||
--body "..." \
|
||||
--label "Release"
|
||||
```
|
||||
3. **CRITICAL**: Verify "Release" label is added
|
||||
4. Create standardized release notes:
|
||||
```bash
|
||||
cat > release-notes-${NEW_VERSION}.md << 'EOF'
|
||||
## ⚠️ Breaking Changes
|
||||
<!-- List breaking changes if any, otherwise remove this entire section -->
|
||||
- Breaking change description (#PR_NUMBER)
|
||||
|
||||
---
|
||||
|
||||
## What's Changed
|
||||
|
||||
### 🚀 Features
|
||||
<!-- List features here, one per line with PR reference -->
|
||||
- Feature description (#PR_NUMBER)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
<!-- List bug fixes here, one per line with PR reference -->
|
||||
- Bug fix description (#PR_NUMBER)
|
||||
|
||||
### 🔧 Maintenance
|
||||
<!-- List refactoring, chore, and other maintenance items -->
|
||||
- Maintenance item description (#PR_NUMBER)
|
||||
|
||||
### 📚 Documentation
|
||||
<!-- List documentation changes if any, remove section if empty -->
|
||||
- Documentation update description (#PR_NUMBER)
|
||||
|
||||
### ⬆️ Dependencies
|
||||
<!-- List dependency updates -->
|
||||
- Updated dependency from vX.X.X to vY.Y.Y (#PR_NUMBER)
|
||||
|
||||
**Full Changelog**: https://github.com/Comfy-Org/ComfyUI_frontend/compare/v${CURRENT_VERSION}...v${NEW_VERSION}
|
||||
EOF
|
||||
```
|
||||
- For hotfixes, typically only populate the "Bug Fixes" section
|
||||
- Include links to the cherry-picked PRs/commits
|
||||
- Update the PR body with the release notes:
|
||||
```bash
|
||||
gh pr edit ${PR_NUMBER} --body-file release-notes-${NEW_VERSION}.md
|
||||
```
|
||||
4. PR description should include:
|
||||
- Version: `1.23.4` → `1.23.5`
|
||||
- Included fixes (link to previous PR)
|
||||
- Release notes for users
|
||||
5. **CONFIRMATION REQUIRED**: Release PR has "Release" label?
|
||||
|
||||
### Step 11: Monitor Release Process
|
||||
|
||||
1
.github/workflows/lint-and-format.yaml
vendored
1
.github/workflows/lint-and-format.yaml
vendored
@@ -56,7 +56,6 @@ jobs:
|
||||
run: |
|
||||
npm run lint
|
||||
npm run format:check
|
||||
npm run knip
|
||||
|
||||
- name: Comment on PR about auto-fix
|
||||
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
@@ -9,7 +9,7 @@ module.exports = defineConfig({
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'],
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
|
||||
@@ -75,7 +75,7 @@ The development of successive minor versions overlaps. For example, while versio
|
||||
<summary>v1.5: Native translation (i18n)</summary>
|
||||
|
||||
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
|
||||
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, Korean, or Arabic. This native
|
||||
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, or Korean. This native
|
||||
implementation offers better performance, reliability, and maintainability compared to previous solutions.<br>
|
||||
|
||||
More details available here: https://blog.comfy.org/p/native-localization-support-i18n
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
{
|
||||
"id": "dec788c2-9829-4a5d-a1ee-d6f0a678b42a",
|
||||
"revision": 0,
|
||||
"last_node_id": 9,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 7,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [413, 389],
|
||||
"size": [425.27801513671875, 180.6060791015625],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [6]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": ["text, watermark"]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [415, 186],
|
||||
"size": [422.84503173828125, 164.31304931640625],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [4]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [473, 609],
|
||||
"size": [315, 106],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [2]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [512, 512, 1]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": [863, 186],
|
||||
"size": [315, 262],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 6
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [7]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
156680208700286,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "VAEDecode",
|
||||
"pos": [1209, 188],
|
||||
"size": [210, 46],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 7
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"slot_index": 0,
|
||||
"links": [9]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "SaveImage",
|
||||
"pos": [1451, 189],
|
||||
"size": [210, 58],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [26, 474],
|
||||
"size": [315, 98],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": [1]
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": [3, 5]
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [8]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 4, 0, 3, 0, "MODEL"],
|
||||
[2, 5, 0, 3, 3, "LATENT"],
|
||||
[3, 4, 1, 6, 0, "CLIP"],
|
||||
[4, 6, 0, 3, 1, "CONDITIONING"],
|
||||
[5, 4, 1, 7, 0, "CLIP"],
|
||||
[6, 7, 0, 3, 2, "CONDITIONING"],
|
||||
[7, 3, 0, 8, 0, "LATENT"],
|
||||
[8, 4, 2, 8, 1, "VAE"],
|
||||
[9, 8, 0, 9, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
"pos": [372.66668701171875, 415.33331298828125],
|
||||
"linkIds": [3]
|
||||
}
|
||||
],
|
||||
"linkExtensions": [
|
||||
{
|
||||
"id": 3,
|
||||
"parentId": 1
|
||||
}
|
||||
],
|
||||
"frontendVersion": "1.26.1"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -34,23 +34,17 @@ const getContentType = (filename: string, fileType: OutputFileType) => {
|
||||
}
|
||||
|
||||
const setQueueIndex = (task: TaskItem) => {
|
||||
task.prompt.priority = TaskHistory.queueIndex++
|
||||
task.prompt[0] = TaskHistory.queueIndex++
|
||||
}
|
||||
|
||||
const setPromptId = (task: TaskItem) => {
|
||||
if (!task.prompt.prompt_id || task.prompt.prompt_id === 'prompt-id') {
|
||||
task.prompt.prompt_id = uuidv4()
|
||||
}
|
||||
task.prompt[1] = uuidv4()
|
||||
}
|
||||
|
||||
export default class TaskHistory {
|
||||
static queueIndex = 0
|
||||
static readonly defaultTask: Readonly<HistoryTaskItem> = {
|
||||
prompt: {
|
||||
priority: 0,
|
||||
prompt_id: 'prompt-id',
|
||||
extra_data: { client_id: uuidv4() }
|
||||
},
|
||||
prompt: [0, 'prompt-id', {}, { client_id: uuidv4() }, []],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
@@ -72,43 +66,16 @@ export default class TaskHistory {
|
||||
)
|
||||
|
||||
private async handleGetHistory(route: Route) {
|
||||
const url = route.request().url()
|
||||
|
||||
// Handle history_v2/:prompt_id endpoint
|
||||
const promptIdMatch = url.match(/history_v2\/([^?]+)/)
|
||||
if (promptIdMatch) {
|
||||
const promptId = promptIdMatch[1]
|
||||
const task = this.tasks.find((t) => t.prompt.prompt_id === promptId)
|
||||
const response: Record<string, any> = {}
|
||||
if (task) {
|
||||
response[promptId] = task
|
||||
}
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response)
|
||||
})
|
||||
}
|
||||
|
||||
// Handle history_v2 list endpoint
|
||||
// Convert HistoryTaskItem to RawHistoryItem format expected by API
|
||||
const rawHistoryItems = this.tasks.map((task) => ({
|
||||
prompt_id: task.prompt.prompt_id,
|
||||
prompt: task.prompt,
|
||||
status: task.status,
|
||||
outputs: task.outputs,
|
||||
...(task.meta && { meta: task.meta })
|
||||
}))
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ history: rawHistoryItems })
|
||||
body: JSON.stringify(this.tasks)
|
||||
})
|
||||
}
|
||||
|
||||
private async handleGetView(route: Route) {
|
||||
const fileName = getFilenameParam(route.request())
|
||||
if (!this.outputContentTypes.has(fileName)) return route.continue()
|
||||
if (!this.outputContentTypes.has(fileName)) route.continue()
|
||||
|
||||
const asset = this.loadAsset(fileName)
|
||||
return route.fulfill({
|
||||
@@ -124,7 +91,7 @@ export default class TaskHistory {
|
||||
|
||||
async setupRoutes() {
|
||||
return this.comfyPage.page.route(
|
||||
/.*\/api\/(view|history_v2)(\/[^?]*)?(\?.*)?$/,
|
||||
/.*\/api\/(view|history)(\?.*)?$/,
|
||||
async (route) => {
|
||||
const request = route.request()
|
||||
const method = request.method()
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('History API v2', () => {
|
||||
const TEST_PROMPT_ID = 'test-prompt-id'
|
||||
const TEST_CLIENT_ID = 'test-client'
|
||||
|
||||
test('Can fetch history with new v2 format', async ({ comfyPage }) => {
|
||||
// Set up mocked history with tasks
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
|
||||
// Verify history_v2 API response format
|
||||
const result = await comfyPage.page.evaluate(async () => {
|
||||
try {
|
||||
const response = await window['app'].api.getHistory()
|
||||
return { success: true, data: response }
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch history:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).toHaveProperty('History')
|
||||
expect(Array.isArray(result.data.History)).toBe(true)
|
||||
expect(result.data.History.length).toBeGreaterThan(0)
|
||||
|
||||
const historyItem = result.data.History[0]
|
||||
|
||||
// Verify the new prompt structure (object instead of array)
|
||||
expect(historyItem.prompt).toHaveProperty('priority')
|
||||
expect(historyItem.prompt).toHaveProperty('prompt_id')
|
||||
expect(historyItem.prompt).toHaveProperty('extra_data')
|
||||
expect(typeof historyItem.prompt.priority).toBe('number')
|
||||
expect(typeof historyItem.prompt.prompt_id).toBe('string')
|
||||
expect(historyItem.prompt.extra_data).toHaveProperty('client_id')
|
||||
})
|
||||
|
||||
test('Can load workflow from history using history_v2 endpoint', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Simple mock workflow for testing
|
||||
const mockWorkflow = {
|
||||
version: 0.4,
|
||||
nodes: [{ id: 1, type: 'TestNode', pos: [100, 100], size: [200, 100] }],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {}
|
||||
}
|
||||
|
||||
// Set up history with workflow data
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp'], 'images', {
|
||||
prompt: {
|
||||
priority: 0,
|
||||
prompt_id: TEST_PROMPT_ID,
|
||||
extra_data: {
|
||||
client_id: TEST_CLIENT_ID,
|
||||
extra_pnginfo: { workflow: mockWorkflow }
|
||||
}
|
||||
}
|
||||
})
|
||||
.setupRoutes()
|
||||
|
||||
// Load initial workflow to clear canvas
|
||||
await comfyPage.loadWorkflow('simple_slider')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Load workflow from history
|
||||
const loadResult = await comfyPage.page.evaluate(async (promptId) => {
|
||||
try {
|
||||
const workflow =
|
||||
await window['app'].api.getWorkflowFromHistory(promptId)
|
||||
if (workflow) {
|
||||
await window['app'].loadGraphData(workflow)
|
||||
return { success: true }
|
||||
}
|
||||
return { success: false, error: 'No workflow found' }
|
||||
} catch (error) {
|
||||
console.error('Failed to load workflow from history:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}, TEST_PROMPT_ID)
|
||||
|
||||
expect(loadResult.success).toBe(true)
|
||||
|
||||
// Verify workflow loaded correctly
|
||||
await comfyPage.nextFrame()
|
||||
const nodeInfo = await comfyPage.page.evaluate(() => {
|
||||
try {
|
||||
const graph = window['app'].graph
|
||||
return {
|
||||
success: true,
|
||||
nodeCount: graph.nodes?.length || 0,
|
||||
firstNodeType: graph.nodes?.[0]?.type || null
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
expect(nodeInfo.success).toBe(true)
|
||||
expect(nodeInfo.nodeCount).toBe(1)
|
||||
expect(nodeInfo.firstNodeType).toBe('TestNode')
|
||||
})
|
||||
|
||||
test('Handles missing workflow data gracefully', async ({ comfyPage }) => {
|
||||
// Set up empty history routes
|
||||
await comfyPage.setupHistory().setupRoutes()
|
||||
|
||||
// Test loading from history with invalid prompt_id
|
||||
const result = await comfyPage.page.evaluate(async () => {
|
||||
try {
|
||||
const workflow =
|
||||
await window['app'].api.getWorkflowFromHistory('invalid-id')
|
||||
return { success: true, workflow }
|
||||
} catch (error) {
|
||||
console.error('Expected error for invalid prompt_id:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
// Should handle gracefully without throwing
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.workflow).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -24,14 +24,8 @@ test.describe('Minimap', () => {
|
||||
const minimapViewport = minimapContainer.locator('.minimap-viewport')
|
||||
await expect(minimapViewport).toBeVisible()
|
||||
|
||||
await expect(minimapContainer).toHaveCSS('position', 'relative')
|
||||
|
||||
// position and z-index validation moved to the parent container of the minimap
|
||||
const minimapMainContainer = comfyPage.page.locator(
|
||||
'.minimap-main-container'
|
||||
)
|
||||
await expect(minimapMainContainer).toHaveCSS('position', 'absolute')
|
||||
await expect(minimapMainContainer).toHaveCSS('z-index', '1000')
|
||||
await expect(minimapContainer).toHaveCSS('position', 'absolute')
|
||||
await expect(minimapContainer).toHaveCSS('z-index', '1000')
|
||||
})
|
||||
|
||||
test('Validate minimap toggle button state', async ({ comfyPage }) => {
|
||||
|
||||
@@ -100,29 +100,4 @@ test.describe('LiteGraph Native Reroute Node', () => {
|
||||
'native_reroute_context_menu.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can delete link that is connected to two reroutes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695
|
||||
await comfyPage.loadWorkflow(
|
||||
'reroute/single-native-reroute-default-workflow'
|
||||
)
|
||||
|
||||
// To find the clickable midpoint button, we use the hardcoded value from the browser logs
|
||||
// since the link is a bezier curve and not a straight line.
|
||||
const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 }
|
||||
|
||||
// Click the middle point of the link to open the context menu.
|
||||
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
|
||||
|
||||
// Click the "Delete" context menu option.
|
||||
await comfyPage.page
|
||||
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' })
|
||||
.click()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'native_reroute_delete_from_midpoint_context_menu.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 92 KiB |
@@ -187,14 +187,12 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow5.json'
|
||||
])
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow5.json'
|
||||
])
|
||||
|
||||
@@ -704,103 +704,4 @@ test.describe('Subgraph Operations', () => {
|
||||
expect(finalCount).toBe(parentCount)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Navigation Hotkeys', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test('Navigation hotkey can be customized', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Change the Exit Subgraph keybinding from Escape to Alt+Q
|
||||
await comfyPage.setSetting('Comfy.Keybinding.NewBindings', [
|
||||
{
|
||||
commandId: 'Comfy.Graph.ExitSubgraph',
|
||||
combo: {
|
||||
key: 'q',
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
shift: false
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
await comfyPage.setSetting('Comfy.Keybinding.UnsetBindings', [
|
||||
{
|
||||
commandId: 'Comfy.Graph.ExitSubgraph',
|
||||
combo: {
|
||||
key: 'Escape',
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// Reload the page
|
||||
await comfyPage.page.reload()
|
||||
await comfyPage.page.waitForTimeout(1024)
|
||||
|
||||
// Navigate into subgraph
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)
|
||||
|
||||
// Verify we're in a subgraph
|
||||
expect(await isInSubgraph(comfyPage)).toBe(true)
|
||||
|
||||
// Test that Escape no longer exits subgraph
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
if (!(await isInSubgraph(comfyPage))) {
|
||||
throw new Error('Not in subgraph')
|
||||
}
|
||||
|
||||
// Test that Alt+Q now exits subgraph
|
||||
await comfyPage.page.keyboard.press('Alt+q')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await isInSubgraph(comfyPage)).toBe(false)
|
||||
})
|
||||
|
||||
test('Escape prioritizes closing dialogs over exiting subgraph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)
|
||||
|
||||
// Verify we're in a subgraph
|
||||
if (!(await isInSubgraph(comfyPage))) {
|
||||
throw new Error('Not in subgraph')
|
||||
}
|
||||
|
||||
// Open settings dialog using hotkey
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
await comfyPage.page.waitForSelector('.settings-container', {
|
||||
state: 'visible'
|
||||
})
|
||||
|
||||
// Press Escape - should close dialog, not exit subgraph
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Dialog should be closed
|
||||
await expect(
|
||||
comfyPage.page.locator('.settings-container')
|
||||
).not.toBeVisible()
|
||||
|
||||
// Should still be in subgraph
|
||||
expect(await isInSubgraph(comfyPage)).toBe(true)
|
||||
|
||||
// Press Escape again - now should exit subgraph
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await isInSubgraph(comfyPage)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
160
docs/SCHEMA_GENERATION.md
Normal file
160
docs/SCHEMA_GENERATION.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# ComfyUI Workflow Schema Generation
|
||||
|
||||
This document describes the process for generating and maintaining JSON Schema definitions for ComfyUI workflows.
|
||||
|
||||
## Overview
|
||||
|
||||
ComfyUI uses **Zod schemas** in TypeScript to define workflow structure, which are converted to **JSON Schema** format for external consumption, documentation, and validation.
|
||||
|
||||
### Schema Versions
|
||||
|
||||
- **Version 0.4** (Legacy): Original workflow format with array-based links
|
||||
- **Version 1.0** (Current): Modern format with object-based links, subgraphs, and reroutes
|
||||
|
||||
## Schema Generation Process
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Ensure all dependencies are installed: `npm install`
|
||||
2. Verify TypeScript compilation: `npm run typecheck`
|
||||
|
||||
### Command
|
||||
|
||||
```bash
|
||||
npm run json-schema
|
||||
```
|
||||
|
||||
This runs the generation script: `scripts/generate-json-schema.ts`
|
||||
|
||||
### Output
|
||||
|
||||
Generated schemas are written to `./schemas/` directory:
|
||||
|
||||
- `workflow-0_4.json` (~80KB) - Legacy workflow format
|
||||
- `workflow-1_0.json` (~82KB) - Current workflow format
|
||||
- `node-def-v1.json` - Node definition schema v1
|
||||
- `node-def-v2.json` - Node definition schema v2
|
||||
|
||||
**Note**: The `./schemas/` directory is gitignored and not committed to the repository.
|
||||
|
||||
## When to Regenerate Schemas
|
||||
|
||||
### Required Regeneration
|
||||
|
||||
Run schema generation when:
|
||||
|
||||
1. **Schema Changes**: Any modifications to files in `/src/schemas/`
|
||||
2. **Breaking Changes**: Changes that affect data structure or validation
|
||||
3. **New Features**: Adding new workflow capabilities (subgraphs, reroutes, etc.)
|
||||
4. **Documentation Updates**: When schema descriptions change
|
||||
|
||||
### Schema Version Bumping
|
||||
|
||||
Only increment schema version for **breaking changes**:
|
||||
|
||||
- Data structure changes (field renames, type changes)
|
||||
- Required field additions
|
||||
- Format changes that break backward compatibility
|
||||
|
||||
**Non-breaking changes** (documentation, optional fields) should NOT bump the version.
|
||||
|
||||
## Schema Sources
|
||||
|
||||
### Primary Schemas (Zod-based)
|
||||
|
||||
#### Workflow Schemas
|
||||
- **File**: `src/schemas/comfyWorkflowSchema.ts`
|
||||
- **Exports**: `zComfyWorkflow` (v0.4), `zComfyWorkflow1` (v1.0)
|
||||
- **Purpose**: Defines workflow JSON structure for both legacy and modern formats
|
||||
|
||||
#### Node Definition Schemas
|
||||
- **Files**:
|
||||
- `src/schemas/nodeDefSchema.ts` (v1)
|
||||
- `src/schemas/nodeDef/nodeDefSchemaV2.ts` (v2)
|
||||
- **Purpose**: Defines node definition structure for different versions
|
||||
|
||||
### Generation Configuration
|
||||
|
||||
The generation script uses:
|
||||
- **Library**: `zod-to-json-schema`
|
||||
- **Strategy**: `$refStrategy: 'none'` (inlines all references)
|
||||
- **Output**: Formatted JSON with proper naming
|
||||
|
||||
## Standard Operating Procedure (SOP)
|
||||
|
||||
### For Schema Updates
|
||||
|
||||
1. **Make Changes**: Modify Zod schemas in `/src/schemas/`
|
||||
2. **Test Locally**: Ensure changes work with existing workflows
|
||||
3. **Run Generation**: `npm run json-schema`
|
||||
4. **Validate Output**: Check generated schemas for correctness
|
||||
5. **Test Integration**: Verify schemas work with external tools
|
||||
6. **Document Changes**: Update this document if process changes
|
||||
|
||||
### For Version Bumps
|
||||
|
||||
1. **Assess Breaking Changes**: Determine if changes are truly breaking
|
||||
2. **Update Version**: Increment version number in schema definition
|
||||
3. **Update Validation**: Modify `validateComfyWorkflow()` function
|
||||
4. **Update Serialization**: Update LGraph serialization if needed
|
||||
5. **Test Backward Compatibility**: Ensure old workflows still load
|
||||
6. **Document Migration**: Provide migration guide if needed
|
||||
|
||||
### For External Publishing
|
||||
|
||||
**Current Status**: No automated publishing to docs.comfy.org exists yet.
|
||||
|
||||
**Recommended Process**:
|
||||
1. Generate schemas locally
|
||||
2. Copy to appropriate documentation repository
|
||||
3. Update documentation with new schema versions
|
||||
4. Coordinate with docs team for publication
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Lodash Import Errors
|
||||
**Error**: `SyntaxError: The requested module 'lodash' does not provide an export named 'clamp'`
|
||||
|
||||
**Solution**: Use the simplified generation script that avoids litegraph imports:
|
||||
```bash
|
||||
npx tsx scripts/generate-json-schema-simple.ts
|
||||
```
|
||||
|
||||
#### Recursive Reference Warnings
|
||||
**Warning**: `Recursive reference detected... Defaulting to any`
|
||||
|
||||
**Explanation**: This is expected for subgraph definitions and doesn't affect functionality.
|
||||
|
||||
### Schema Validation Issues
|
||||
|
||||
1. **Check Source Schema**: Verify Zod schema is valid
|
||||
2. **Test TypeScript**: Run `npm run typecheck`
|
||||
3. **Check Dependencies**: Ensure all imports resolve correctly
|
||||
4. **Validate Output**: Test generated JSON Schema with online validators
|
||||
|
||||
## Integration Points
|
||||
|
||||
### External Documentation
|
||||
- **docs.comfy.org**: Official ComfyUI documentation site
|
||||
- **Workflow Spec**: https://docs.comfy.org/specs/workflow_json
|
||||
|
||||
### Development Tools
|
||||
- **IDE Support**: Generated schemas provide autocomplete and validation
|
||||
- **API Validation**: External tools can validate workflow JSON
|
||||
- **Documentation Generation**: Schemas document workflow structure
|
||||
|
||||
## Future Improvements
|
||||
|
||||
1. **Automated Publishing**: Set up CI/CD to publish schemas to docs.comfy.org
|
||||
2. **Version Management**: Implement semantic versioning for schema changes
|
||||
3. **Migration Tools**: Create utilities to migrate between schema versions
|
||||
4. **Testing**: Add automated tests for schema generation and validation
|
||||
5. **Documentation**: Generate human-readable documentation from schemas
|
||||
|
||||
## References
|
||||
|
||||
- **Zod Documentation**: https://zod.dev/
|
||||
- **JSON Schema Specification**: https://json-schema.org/
|
||||
- **ComfyUI Workflow Specification**: https://docs.comfy.org/specs/workflow_json
|
||||
@@ -1,75 +0,0 @@
|
||||
import type { KnipConfig } from 'knip'
|
||||
|
||||
const config: KnipConfig = {
|
||||
entry: [
|
||||
'src/main.ts',
|
||||
'vite.config.mts',
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
'eslint.config.js',
|
||||
'tailwind.config.js',
|
||||
'postcss.config.js',
|
||||
'playwright.config.ts',
|
||||
'playwright.i18n.config.ts',
|
||||
'vitest.config.ts',
|
||||
'scripts/**/*.{js,ts}'
|
||||
],
|
||||
project: [
|
||||
'src/**/*.{js,ts,vue}',
|
||||
'tests-ui/**/*.{js,ts,vue}',
|
||||
'browser_tests/**/*.{js,ts}',
|
||||
'scripts/**/*.{js,ts}'
|
||||
],
|
||||
ignore: [
|
||||
// Generated files
|
||||
'dist/**',
|
||||
'types/**',
|
||||
'node_modules/**',
|
||||
// Config files that might not show direct usage
|
||||
'.husky/**',
|
||||
// Temporary or cache files
|
||||
'.vite/**',
|
||||
'coverage/**',
|
||||
// i18n config
|
||||
'.i18nrc.cjs',
|
||||
// Test setup files
|
||||
'browser_tests/globalSetup.ts',
|
||||
'browser_tests/globalTeardown.ts',
|
||||
'browser_tests/utils/**',
|
||||
// Scripts
|
||||
'scripts/**',
|
||||
// Vite config files
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
// Auto generated manager types
|
||||
'src/types/generatedManagerTypes.ts'
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
// Vue-specific configuration
|
||||
vue: true,
|
||||
// Only check for unused files, disable all other rules
|
||||
// TODO: Gradually enable other rules - see https://github.com/Comfy-Org/ComfyUI_frontend/issues/4888
|
||||
rules: {
|
||||
binaries: 'off',
|
||||
classMembers: 'off',
|
||||
dependencies: 'off',
|
||||
devDependencies: 'off',
|
||||
duplicates: 'off',
|
||||
enumMembers: 'off',
|
||||
exports: 'off',
|
||||
nsExports: 'off',
|
||||
nsTypes: 'off',
|
||||
types: 'off',
|
||||
unlisted: 'off'
|
||||
},
|
||||
// Include dependencies analysis
|
||||
includeEntryExports: true,
|
||||
// Workspace configuration for monorepo-like structure
|
||||
workspaces: {
|
||||
'.': {
|
||||
entry: ['src/main.ts']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default config
|
||||
544
package-lock.json
generated
544
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.2",
|
||||
"version": "1.26.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.2",
|
||||
"version": "1.26.1",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@@ -80,7 +80,6 @@
|
||||
"happy-dom": "^15.11.0",
|
||||
"husky": "^9.0.11",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"knip": "^5.62.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.3.2",
|
||||
@@ -1002,40 +1001,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
|
||||
"integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.0.4",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz",
|
||||
"integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz",
|
||||
"integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
@@ -3104,19 +3069,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz",
|
||||
"integrity": "sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.5",
|
||||
"@emnapi/runtime": "^1.4.5",
|
||||
"@tybys/wasm-util": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -3168,275 +3120,6 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-android-arm-eabi": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.6.1.tgz",
|
||||
"integrity": "sha512-Ma/kg29QJX1Jzelv0Q/j2iFuUad1WnjgPjpThvjqPjpOyLjCUaiFCCnshhmWjyS51Ki1Iol3fjf1qAzObf8GIA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-android-arm64": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.6.1.tgz",
|
||||
"integrity": "sha512-xjL/FKKc5p8JkFWiH7pJWSzsewif3fRf1rw2qiRxRvq1uIa6l7Zoa14Zq2TNWEsqDjdeOrlJtfWiPNRnevK0oQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-darwin-arm64": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.6.1.tgz",
|
||||
"integrity": "sha512-u0yrJ3NHE0zyCjiYpIyz4Vmov21MA0yFKbhHgixDU/G6R6nvC8ZpuSFql3+7C8ttAK9p8WpqOGweepfcilH5Bw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-darwin-x64": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.6.1.tgz",
|
||||
"integrity": "sha512-2lox165h1EhzxcC8edUy0znXC/hnAbUPaMpYKVlzLpB2AoYmgU4/pmofFApj+axm2FXpNamjcppld8EoHo06rw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-freebsd-x64": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.6.1.tgz",
|
||||
"integrity": "sha512-F45MhEQ7QbHfsvZtVNuA/9obu3il7QhpXYmCMfxn7Zt9nfAOw4pQ8hlS5DroHVp3rW35u9F7x0sixk/QEAi3qQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.6.1.tgz",
|
||||
"integrity": "sha512-r+3+MTTl0tD4NoWbfTIItAxJvuyIU7V0fwPDXrv7Uj64vZ3OYaiyV+lVaeU89Bk/FUUQxeUpWBwdKNKHjyRNQw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-arm-musleabihf": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.6.1.tgz",
|
||||
"integrity": "sha512-TBTZ63otsWZ72Z8ZNK2JVS0HW1w9zgOixJTFDNrYPUUW1pXGa28KAjQ1yGawj242WLAdu3lwdNIWtkxeO2BLxQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-arm64-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-SjwhNynjSG2yMdyA0f7wz7Yvo3ppejO+ET7n2oiI7ApCXrwxMzeRWjBzQt+oVWr2HzVOfaEcDS9rMtnR83ulig==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-arm64-musl": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.6.1.tgz",
|
||||
"integrity": "sha512-f4EMidK6rosInBzPMnJ0Ri4RttFCvvLNUNDFUBtELW/MFkBwPTDlvbsmW0u0Mk/ruBQ2WmRfOZ6tT62kWMcX2Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-ppc64-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-1umENVKeUsrWnf5IlF/6SM7DCv8G6CoKI2LnYR6qhZuLYDPS4PBZ0Jow3UDV9Rtbv5KRPcA3/uXjI88ntWIcOQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-riscv64-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-Hjyp1FRdJhsEpIxsZq5VcDuFc8abC0Bgy8DWEa31trCKoTz7JqA7x3E2dkFbrAKsEFmZZ0NvuG5Ip3oIRARhow==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-riscv64-musl": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.6.1.tgz",
|
||||
"integrity": "sha512-ODJOJng6f3QxpAXhLel3kyWs8rPsJeo9XIZHzA7p//e+5kLMDU7bTVk4eZnUHuxsqsB8MEvPCicJkKCEuur5Ag==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-s390x-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-hCzRiLhqe1ZOpHTsTGKp7gnMJRORlbCthawBueer2u22RVAka74pV/+4pP1tqM07mSlQn7VATuWaDw9gCl+cVg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-x64-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-JansPD8ftOzMYIC3NfXJ68tt63LEcIAx44Blx6BAd7eY880KX7A0KN3hluCrelCz5aQkPaD95g8HBiJmKaEi2w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-x64-musl": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.6.1.tgz",
|
||||
"integrity": "sha512-R78ES1rd4z2x5NrFPtSWb/ViR1B8wdl+QN2X8DdtoYcqZE/4tvWtn9ZTCXMEzUp23tchJ2wUB+p6hXoonkyLpA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-wasm32-wasi": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.6.1.tgz",
|
||||
"integrity": "sha512-qAR3tYIf3afkij/XYunZtlz3OH2Y4ni10etmCFIJB5VRGsqJyI6Hl+2dXHHGJNwbwjXjSEH/KWJBpVroF3TxBw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-win32-arm64-msvc": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.6.1.tgz",
|
||||
"integrity": "sha512-QqygWygIuemGkaBA48POOTeinbVvlamqh6ucm8arGDGz/mB5O00gXWxed12/uVrYEjeqbMkla/CuL3fjL3EKvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-win32-ia32-msvc": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.6.1.tgz",
|
||||
"integrity": "sha512-N2+kkWwt/bk0JTCxhPuK8t8JMp3nd0n2OhwOkU8KO4a7roAJEa4K1SZVjMv5CqUIr5sx2CxtXRBoFDiORX5oBg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-win32-x64-msvc": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.6.1.tgz",
|
||||
"integrity": "sha512-DfMg3cU9bJUbN62Prbp4fGCtLgexuwyEaQGtZAp8xmi1Ii26uflOGx0FJkFTF6lVMSFoIRFvIL8gsw5/ZdHrMw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@pinia/testing": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-0.1.5.tgz",
|
||||
@@ -4803,17 +4486,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
|
||||
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/argparse": {
|
||||
"version": "1.0.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz",
|
||||
@@ -8797,17 +8469,16 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.8"
|
||||
"micromatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
@@ -8880,16 +8551,6 @@
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-package-json": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz",
|
||||
"integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"walk-up-path": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
@@ -9185,22 +8846,6 @@
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/formatly": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/formatly/-/formatly-0.2.4.tgz",
|
||||
"integrity": "sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fd-package-json": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"formatly": "bin/index.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||
@@ -10872,109 +10517,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/knip": {
|
||||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-5.62.0.tgz",
|
||||
"integrity": "sha512-hfTUVzmrMNMT1khlZfAYmBABeehwWUUrizLQoLamoRhSFkygsGIXWx31kaWKBgEaIVL77T3Uz7IxGvSw+CvQ6A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/webpro"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/knip"
|
||||
},
|
||||
{
|
||||
"type": "polar",
|
||||
"url": "https://polar.sh/webpro-nl"
|
||||
}
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"fast-glob": "^3.3.3",
|
||||
"formatly": "^0.2.4",
|
||||
"jiti": "^2.4.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimist": "^1.2.8",
|
||||
"oxc-resolver": "^11.1.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.1",
|
||||
"smol-toml": "^1.3.4",
|
||||
"strip-json-comments": "5.0.2",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^3.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"knip": "bin/knip.js",
|
||||
"knip-bun": "bin/knip-bun.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18",
|
||||
"typescript": ">=5.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/knip/node_modules/jiti": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/strip-json-comments": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.2.tgz",
|
||||
"integrity": "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/kolorist": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
|
||||
@@ -12846,22 +12388,6 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz",
|
||||
"integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"napi-postinstall": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/napi-postinstall"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -13215,41 +12741,6 @@
|
||||
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oxc-resolver": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.6.1.tgz",
|
||||
"integrity": "sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"napi-postinstall": "^0.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@oxc-resolver/binding-android-arm-eabi": "11.6.1",
|
||||
"@oxc-resolver/binding-android-arm64": "11.6.1",
|
||||
"@oxc-resolver/binding-darwin-arm64": "11.6.1",
|
||||
"@oxc-resolver/binding-darwin-x64": "11.6.1",
|
||||
"@oxc-resolver/binding-freebsd-x64": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-arm-gnueabihf": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-arm-musleabihf": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-arm64-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-arm64-musl": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-ppc64-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-riscv64-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-riscv64-musl": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-s390x-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-x64-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-x64-musl": "11.6.1",
|
||||
"@oxc-resolver/binding-wasm32-wasi": "11.6.1",
|
||||
"@oxc-resolver/binding-win32-arm64-msvc": "11.6.1",
|
||||
"@oxc-resolver/binding-win32-ia32-msvc": "11.6.1",
|
||||
"@oxc-resolver/binding-win32-x64-msvc": "11.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
@@ -15313,19 +14804,6 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/smol-toml": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.4.2.tgz",
|
||||
"integrity": "sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/cyyynthia"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -17922,16 +17400,6 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/walk-up-path": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz",
|
||||
"integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "4.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.26.2",
|
||||
"version": "1.26.1",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -23,7 +23,6 @@
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"knip": "knip",
|
||||
"locale": "lobe-i18n locale",
|
||||
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts"
|
||||
@@ -57,7 +56,6 @@
|
||||
"happy-dom": "^15.11.0",
|
||||
"husky": "^9.0.11",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"knip": "^5.62.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.3.2",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import Breadcrumb from 'primevue/breadcrumb'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { computed, onUpdated, ref, watch } from 'vue'
|
||||
@@ -97,6 +98,18 @@ const home = computed(() => ({
|
||||
}
|
||||
}))
|
||||
|
||||
// Escape exits from the current subgraph.
|
||||
useEventListener(document, 'keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
||||
|
||||
canvas.setGraph(
|
||||
navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Check for overflow on breadcrumb items and collapse/expand the breadcrumb to fit
|
||||
let overflowObserver: ReturnType<typeof useOverflowObserver> | undefined
|
||||
watch(breadcrumbElement, (el) => {
|
||||
|
||||
75
src/components/common/ApiNodesCostBreakdown.vue
Normal file
75
src/components/common/ApiNodesCostBreakdown.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3 h-full">
|
||||
<div class="flex justify-between text-xs">
|
||||
<div>{{ t('apiNodesCostBreakdown.title') }}</div>
|
||||
<div>{{ t('apiNodesCostBreakdown.costPerRun') }}</div>
|
||||
</div>
|
||||
<ScrollPanel class="flex-grow h-0">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="node in nodes"
|
||||
:key="node.name"
|
||||
class="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--p-content-border-color)]"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-base font-medium leading-tight">{{
|
||||
node.name
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<span class="text-base font-medium leading-tight">
|
||||
{{ node.cost.toFixed(costPrecision) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
<template v-if="showTotal && nodes.length > 1">
|
||||
<Divider class="my-2" />
|
||||
<div class="flex justify-between items-center border-t px-3">
|
||||
<span class="text-sm">{{ t('apiNodesCostBreakdown.totalCost') }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-yellow-500 p-1"
|
||||
/>
|
||||
<span>{{ totalCost.toFixed(costPrecision) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ApiNodeCost } from '@/types/apiNodeTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const {
|
||||
nodes,
|
||||
showTotal = true,
|
||||
costPrecision = 3
|
||||
} = defineProps<{
|
||||
nodes: ApiNodeCost[]
|
||||
showTotal?: boolean
|
||||
costPrecision?: number
|
||||
}>()
|
||||
|
||||
const totalCost = computed(() =>
|
||||
nodes.reduce((sum, node) => sum + node.cost, 0)
|
||||
)
|
||||
</script>
|
||||
31
src/components/common/ApiNodesList.vue
Normal file
31
src/components/common/ApiNodesList.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3 h-full">
|
||||
<div class="flex text-xs">
|
||||
<div>{{ t('apiNodesCostBreakdown.title') }}</div>
|
||||
</div>
|
||||
<ScrollPanel class="flex-grow h-0">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="nodeName in nodeNames"
|
||||
:key="nodeName"
|
||||
class="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--p-content-border-color)]"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-base font-medium leading-tight">{{
|
||||
nodeName
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { nodeNames } = defineProps<{ nodeNames: string[] }>()
|
||||
</script>
|
||||
@@ -36,7 +36,6 @@ import ListBox from 'primevue/listbox'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ElectronFileDownload from '@/components/common/ElectronFileDownload.vue'
|
||||
import FileDownload from '@/components/common/FileDownload.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="w-[100%] flex justify-between items-center">
|
||||
<div class="flex justify-start items-center">
|
||||
<div class="w-1 h-6 rounded-md" />
|
||||
<div class="w-6 h-6 relative overflow-hidden">
|
||||
<i class="pi pi-box text-xl text-muted" style="opacity: 0.6" />
|
||||
</div>
|
||||
<div class="px-3 py-2 rounded-md flex justify-start items-start gap-2.5">
|
||||
<div class="text-right justify-start text-sm font-bold leading-none">
|
||||
{{ $t('manager.nodePack') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex justify-start items-center gap-3">
|
||||
<div
|
||||
v-if="nodePack.downloads"
|
||||
class="flex items-center text-sm text-muted tracking-tighter"
|
||||
>
|
||||
<i class="pi pi-download mr-2" />
|
||||
{{ $n(nodePack.downloads) }}
|
||||
</div>
|
||||
<template v-if="isInstalled">
|
||||
<PackEnableToggle :node-pack="nodePack" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<PackInstallButton :node-packs="[nodePack]" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
</script>
|
||||
@@ -15,9 +15,9 @@
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag'
|
||||
|
||||
import { isProductionEnvironment } from '@/config/environment'
|
||||
|
||||
const isStaging = !isProductionEnvironment()
|
||||
// Global variable from vite build defined in global.d.ts
|
||||
// eslint-disable-next-line no-undef
|
||||
const isStaging = !__USE_PROD_CONFIG__
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,68 +1,39 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible && initialized"
|
||||
class="minimap-main-container flex absolute bottom-[20px] right-[90px] z-[1000]"
|
||||
ref="containerRef"
|
||||
class="litegraph-minimap absolute right-[90px] z-[1000]"
|
||||
:class="{
|
||||
'bottom-[20px]': !bottomPanelStore.bottomPanelVisible,
|
||||
'bottom-[280px]': bottomPanelStore.bottomPanelVisible
|
||||
}"
|
||||
:style="containerStyles"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerup="handlePointerUp"
|
||||
@pointerleave="handlePointerUp"
|
||||
@wheel="handleWheel"
|
||||
>
|
||||
<MiniMapPanel
|
||||
v-if="showOptionsPanel"
|
||||
:panel-styles="panelStyles"
|
||||
:node-colors="nodeColors"
|
||||
:show-links="showLinks"
|
||||
:show-groups="showGroups"
|
||||
:render-bypass="renderBypass"
|
||||
:render-error="renderError"
|
||||
@update-option="updateOption"
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
:width="width"
|
||||
:height="height"
|
||||
class="minimap-canvas"
|
||||
/>
|
||||
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="litegraph-minimap relative"
|
||||
:style="containerStyles"
|
||||
>
|
||||
<Button
|
||||
class="absolute z-10"
|
||||
size="small"
|
||||
text
|
||||
severity="secondary"
|
||||
@click.stop="toggleOptionsPanel"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:settings-2 />
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
:width="width"
|
||||
:height="height"
|
||||
class="minimap-canvas"
|
||||
/>
|
||||
|
||||
<div class="minimap-viewport" :style="viewportStyles" />
|
||||
|
||||
<div
|
||||
class="absolute inset-0"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerup="handlePointerUp"
|
||||
@pointerleave="handlePointerUp"
|
||||
@wheel="handleWheel"
|
||||
/>
|
||||
</div>
|
||||
<div class="minimap-viewport" :style="viewportStyles" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
import { useMinimap } from '@/composables/useMinimap'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
import MiniMapPanel from './MiniMapPanel.vue'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
|
||||
const minimap = useMinimap()
|
||||
const canvasStore = useCanvasStore()
|
||||
const bottomPanelStore = useBottomPanelStore()
|
||||
|
||||
const {
|
||||
initialized,
|
||||
@@ -73,13 +44,6 @@ const {
|
||||
viewportStyles,
|
||||
width,
|
||||
height,
|
||||
panelStyles,
|
||||
nodeColors,
|
||||
showLinks,
|
||||
showGroups,
|
||||
renderBypass,
|
||||
renderError,
|
||||
updateOption,
|
||||
init,
|
||||
destroy,
|
||||
handlePointerDown,
|
||||
@@ -88,12 +52,6 @@ const {
|
||||
handleWheel
|
||||
} = minimap
|
||||
|
||||
const showOptionsPanel = ref(false)
|
||||
|
||||
const toggleOptionsPanel = () => {
|
||||
showOptionsPanel.value = !showOptionsPanel.value
|
||||
}
|
||||
|
||||
watch(
|
||||
() => canvasStore.canvas,
|
||||
async (canvas) => {
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="minimap-panel p-3 mr-2 flex flex-col gap-3 text-sm"
|
||||
:style="panelStyles"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="node-colors"
|
||||
name="node-colors"
|
||||
:model-value="nodeColors"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) => $emit('updateOption', 'Comfy.Minimap.NodeColors', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:palette />
|
||||
<label for="node-colors">{{ $t('minimap.nodeColors') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="show-links"
|
||||
name="show-links"
|
||||
:model-value="showLinks"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) => $emit('updateOption', 'Comfy.Minimap.ShowLinks', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:route />
|
||||
<label for="show-links">{{ $t('minimap.showLinks') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="show-groups"
|
||||
name="show-groups"
|
||||
:model-value="showGroups"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) => $emit('updateOption', 'Comfy.Minimap.ShowGroups', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:frame />
|
||||
<label for="show-groups">{{ $t('minimap.showGroups') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="render-bypass"
|
||||
name="render-bypass"
|
||||
:model-value="renderBypass"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) =>
|
||||
$emit('updateOption', 'Comfy.Minimap.RenderBypassState', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:circle-slash-2 />
|
||||
<label for="render-bypass">{{ $t('minimap.renderBypassState') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="render-error"
|
||||
name="render-error"
|
||||
:model-value="renderError"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) =>
|
||||
$emit('updateOption', 'Comfy.Minimap.RenderErrorState', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:message-circle-warning />
|
||||
<label for="render-error">{{ $t('minimap.renderErrorState') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
|
||||
import { MinimapOptionKey } from '@/composables/useMinimap'
|
||||
|
||||
defineProps<{
|
||||
panelStyles: any
|
||||
nodeColors: boolean
|
||||
showLinks: boolean
|
||||
showGroups: boolean
|
||||
renderBypass: boolean
|
||||
renderError: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
updateOption: [key: MinimapOptionKey, value: boolean]
|
||||
}>()
|
||||
</script>
|
||||
@@ -106,8 +106,8 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import { ComfyNode } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
ResultItemImpl,
|
||||
@@ -126,7 +126,6 @@ const toast = useToast()
|
||||
const queueStore = useQueueStore()
|
||||
const settingStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const workflowService = useWorkflowService()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Expanded view: show all outputs in a flat list.
|
||||
@@ -209,16 +208,8 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
{
|
||||
label: t('g.loadWorkflow'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: () => {
|
||||
if (menuTargetTask.value) {
|
||||
void workflowService.loadTaskWorkflow(menuTargetTask.value)
|
||||
}
|
||||
},
|
||||
disabled: !(
|
||||
menuTargetTask.value?.workflow ||
|
||||
(menuTargetTask.value?.isHistory &&
|
||||
menuTargetTask.value?.prompt.prompt_id)
|
||||
)
|
||||
command: () => menuTargetTask.value?.loadWorkflow(app),
|
||||
disabled: !menuTargetTask.value?.workflow
|
||||
},
|
||||
{
|
||||
label: t('g.goToNode'),
|
||||
|
||||
@@ -23,7 +23,6 @@ import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { type ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
@@ -806,21 +805,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: () => {
|
||||
bottomPanelStore.togglePanel('shortcuts')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.ExitSubgraph',
|
||||
icon: 'pi pi-arrow-up',
|
||||
label: 'Exit Subgraph',
|
||||
versionAdded: '1.20.1',
|
||||
function: () => {
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
if (!canvas.graph) return
|
||||
|
||||
canvas.setGraph(
|
||||
navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -2,14 +2,13 @@ import { useRafFn, useThrottleFn } from '@vueuse/core'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
import { LGraphEventMode, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
|
||||
interface GraphCallbacks {
|
||||
onNodeAdded?: (node: LGraphNode) => void
|
||||
@@ -17,13 +16,6 @@ interface GraphCallbacks {
|
||||
onConnectionChange?: (node: LGraphNode) => void
|
||||
}
|
||||
|
||||
export type MinimapOptionKey =
|
||||
| 'Comfy.Minimap.NodeColors'
|
||||
| 'Comfy.Minimap.ShowLinks'
|
||||
| 'Comfy.Minimap.ShowGroups'
|
||||
| 'Comfy.Minimap.RenderBypassState'
|
||||
| 'Comfy.Minimap.RenderErrorState'
|
||||
|
||||
export function useMinimap() {
|
||||
const settingStore = useSettingStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
@@ -35,27 +27,6 @@ export function useMinimap() {
|
||||
|
||||
const visible = ref(true)
|
||||
|
||||
const nodeColors = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.NodeColors')
|
||||
)
|
||||
const showLinks = computed(() => settingStore.get('Comfy.Minimap.ShowLinks'))
|
||||
const showGroups = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.ShowGroups')
|
||||
)
|
||||
const renderBypass = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.RenderBypassState')
|
||||
)
|
||||
const renderError = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.RenderErrorState')
|
||||
)
|
||||
|
||||
const updateOption = async (key: MinimapOptionKey, value: boolean) => {
|
||||
await settingStore.set(key, value)
|
||||
|
||||
needsFullRedraw.value = true
|
||||
updateMinimap()
|
||||
}
|
||||
|
||||
const initialized = ref(false)
|
||||
const bounds = ref({
|
||||
minX: 0,
|
||||
@@ -92,19 +63,10 @@ export function useMinimap() {
|
||||
const nodeColor = computed(
|
||||
() => (isLightTheme.value ? '#3DA8E099' : '#0B8CE999') // lighter blue for light theme
|
||||
)
|
||||
const nodeColorDefault = computed(
|
||||
() => (isLightTheme.value ? '#D9D9D9' : '#353535') // this is the default node color when using nodeColors setting
|
||||
)
|
||||
const linkColor = computed(
|
||||
() => (isLightTheme.value ? '#616161' : '#B3B3B3') // lighter orange for light theme
|
||||
() => (isLightTheme.value ? '#FFB347' : '#F99614') // lighter orange for light theme
|
||||
)
|
||||
const slotColor = computed(() => linkColor.value)
|
||||
const groupColor = computed(() =>
|
||||
isLightTheme.value ? '#A2D3EC' : '#1F547A'
|
||||
)
|
||||
const bypassColor = computed(() =>
|
||||
isLightTheme.value ? '#DBDBDB' : '#4B184B'
|
||||
)
|
||||
|
||||
const containerRect = ref({
|
||||
left: 0,
|
||||
@@ -154,14 +116,6 @@ export function useMinimap() {
|
||||
borderRadius: '8px'
|
||||
}))
|
||||
|
||||
const panelStyles = computed(() => ({
|
||||
width: `210px`,
|
||||
height: `${height}px`,
|
||||
backgroundColor: isLightTheme.value ? '#FAF9F5' : '#15161C',
|
||||
border: `1px solid ${isLightTheme.value ? '#ccc' : '#333'}`,
|
||||
borderRadius: '8px'
|
||||
}))
|
||||
|
||||
const viewportStyles = computed(() => ({
|
||||
transform: `translate(${viewportTransform.value.x}px, ${viewportTransform.value.y}px)`,
|
||||
width: `${viewportTransform.value.width}px`,
|
||||
@@ -235,25 +189,6 @@ export function useMinimap() {
|
||||
return Math.min(scaleX, scaleY) * 0.9
|
||||
}
|
||||
|
||||
const renderGroups = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
) => {
|
||||
const g = graph.value
|
||||
if (!g || !g._groups || g._groups.length === 0) return
|
||||
|
||||
for (const group of g._groups) {
|
||||
const x = (group.pos[0] - bounds.value.minX) * scale.value + offsetX
|
||||
const y = (group.pos[1] - bounds.value.minY) * scale.value + offsetY
|
||||
const w = group.size[0] * scale.value
|
||||
const h = group.size[1] * scale.value
|
||||
|
||||
ctx.fillStyle = groupColor.value
|
||||
ctx.fillRect(x, y, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
const renderNodes = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
offsetX: number,
|
||||
@@ -268,29 +203,9 @@ export function useMinimap() {
|
||||
const w = node.size[0] * scale.value
|
||||
const h = node.size[1] * scale.value
|
||||
|
||||
let color = nodeColor.value
|
||||
|
||||
if (renderBypass.value && node.mode === LGraphEventMode.BYPASS) {
|
||||
color = bypassColor.value
|
||||
} else if (nodeColors.value) {
|
||||
color = nodeColorDefault.value
|
||||
|
||||
if (node.bgcolor) {
|
||||
color = isLightTheme.value
|
||||
? adjustColor(node.bgcolor, { lightness: 0.5 })
|
||||
: node.bgcolor
|
||||
}
|
||||
}
|
||||
|
||||
// Render solid node blocks
|
||||
ctx.fillStyle = color
|
||||
ctx.fillStyle = nodeColor.value
|
||||
ctx.fillRect(x, y, w, h)
|
||||
|
||||
if (renderError.value && node.has_errors) {
|
||||
ctx.strokeStyle = '#FF0000'
|
||||
ctx.lineWidth = 0.3
|
||||
ctx.strokeRect(x, y, w, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,9 +218,9 @@ export function useMinimap() {
|
||||
if (!g) return
|
||||
|
||||
ctx.strokeStyle = linkColor.value
|
||||
ctx.lineWidth = 0.3
|
||||
ctx.lineWidth = 1.4
|
||||
|
||||
const slotRadius = Math.max(scale.value, 0.5) // Larger slots that scale
|
||||
const slotRadius = 3.7 * Math.max(scale.value, 0.5) // Larger slots that scale
|
||||
const connections: Array<{
|
||||
x1: number
|
||||
y1: number
|
||||
@@ -389,15 +304,8 @@ export function useMinimap() {
|
||||
const offsetX = (width - bounds.value.width * scale.value) / 2
|
||||
const offsetY = (height - bounds.value.height * scale.value) / 2
|
||||
|
||||
if (showGroups.value) {
|
||||
renderGroups(ctx, offsetX, offsetY)
|
||||
}
|
||||
|
||||
if (showLinks.value) {
|
||||
renderConnections(ctx, offsetX, offsetY)
|
||||
}
|
||||
|
||||
renderNodes(ctx, offsetX, offsetY)
|
||||
renderConnections(ctx, offsetX, offsetY)
|
||||
|
||||
needsFullRedraw.value = false
|
||||
updateFlags.value.nodes = false
|
||||
@@ -782,16 +690,9 @@ export function useMinimap() {
|
||||
canvasRef,
|
||||
containerStyles,
|
||||
viewportStyles,
|
||||
panelStyles,
|
||||
width,
|
||||
height,
|
||||
|
||||
nodeColors,
|
||||
showLinks,
|
||||
showGroups,
|
||||
renderBypass,
|
||||
renderError,
|
||||
|
||||
init,
|
||||
destroy,
|
||||
toggle,
|
||||
@@ -800,7 +701,6 @@ export function useMinimap() {
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
handleWheel,
|
||||
setMinimapRef,
|
||||
updateOption
|
||||
setMinimapRef
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { IWidget } from '@/lib/litegraph/src/litegraph'
|
||||
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const MAX_RETRIES = 5
|
||||
const TIMEOUT = 4096
|
||||
@@ -59,21 +58,10 @@ const fetchData = async (
|
||||
controller: AbortController
|
||||
) => {
|
||||
const { route, response_key, query_params, timeout = TIMEOUT } = config
|
||||
|
||||
// Get auth header from Firebase
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authHeader = await authStore.getAuthHeader()
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
if (authHeader) {
|
||||
Object.assign(headers, authHeader)
|
||||
}
|
||||
|
||||
const res = await axios.get(route, {
|
||||
params: query_params,
|
||||
signal: controller.signal,
|
||||
timeout,
|
||||
headers
|
||||
timeout
|
||||
})
|
||||
return response_key ? res.data[response_key] : res.data
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { isProductionEnvironment } from './environment'
|
||||
|
||||
export const COMFY_API_BASE_URL = isProductionEnvironment()
|
||||
export const COMFY_API_BASE_URL = __USE_PROD_CONFIG__
|
||||
? 'https://api.comfy.org'
|
||||
: 'https://stagingapi.comfy.org'
|
||||
|
||||
export const COMFY_PLATFORM_BASE_URL = isProductionEnvironment()
|
||||
export const COMFY_PLATFORM_BASE_URL = __USE_PROD_CONFIG__
|
||||
? 'https://platform.comfy.org'
|
||||
: 'https://stagingplatform.comfy.org'
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Runtime environment configuration that determines if we're in production or staging
|
||||
* based on the hostname. Replaces the build-time __USE_PROD_CONFIG__ constant.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if the application is running in production environment
|
||||
* @returns true if hostname is cloud.comfy.org (production), false otherwise (staging)
|
||||
*/
|
||||
export function isProductionEnvironment(): boolean {
|
||||
// In SSR/Node.js environments or during build, use the environment variable
|
||||
if (typeof window === 'undefined') {
|
||||
return process.env.USE_PROD_CONFIG === 'true'
|
||||
}
|
||||
|
||||
// In browser, check the hostname
|
||||
return window.location.hostname === 'cloud.comfy.org'
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { FirebaseOptions } from 'firebase/app'
|
||||
|
||||
import { isProductionEnvironment } from './environment'
|
||||
|
||||
const DEV_CONFIG: FirebaseOptions = {
|
||||
apiKey: 'AIzaSyDa_YMeyzV0SkVe92vBZ1tVikWBmOU5KVE',
|
||||
authDomain: 'dreamboothy-dev.firebaseapp.com',
|
||||
@@ -25,6 +23,6 @@ const PROD_CONFIG: FirebaseOptions = {
|
||||
}
|
||||
|
||||
// To test with prod config while using dev server, set USE_PROD_CONFIG=true in .env
|
||||
export const FIREBASE_CONFIG: FirebaseOptions = isProductionEnvironment()
|
||||
export const FIREBASE_CONFIG: FirebaseOptions = __USE_PROD_CONFIG__
|
||||
? PROD_CONFIG
|
||||
: DEV_CONFIG
|
||||
|
||||
@@ -190,11 +190,5 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
key: 'k'
|
||||
},
|
||||
commandId: 'Workspace.ToggleBottomPanel.Shortcuts'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'Escape'
|
||||
},
|
||||
commandId: 'Comfy.Graph.ExitSubgraph'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -300,8 +300,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
{ value: 'ja', text: '日本語' },
|
||||
{ value: 'ko', text: '한국어' },
|
||||
{ value: 'fr', text: 'Français' },
|
||||
{ value: 'es', text: 'Español' },
|
||||
{ value: 'ar', text: 'عربي' }
|
||||
{ value: 'es', text: 'Español' }
|
||||
],
|
||||
defaultValue: () => navigator.language.split('-')[0] || 'en'
|
||||
},
|
||||
@@ -831,41 +830,6 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: true,
|
||||
versionAdded: '1.25.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.NodeColors',
|
||||
name: 'Display node with its original color on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.ShowLinks',
|
||||
name: 'Display links on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.ShowGroups',
|
||||
name: 'Display node groups on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.RenderBypassState',
|
||||
name: 'Render bypass state on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.RenderErrorState',
|
||||
name: 'Render error state on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Workflow.AutoSaveDelay',
|
||||
name: 'Auto Save Delay (ms)',
|
||||
|
||||
438
src/constants/coreTemplates.ts
Normal file
438
src/constants/coreTemplates.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
export const CORE_TEMPLATES = [
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Basics',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'default',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images from text descriptions.'
|
||||
},
|
||||
{
|
||||
name: 'image2image',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Transform existing images using text prompts.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/img2img/'
|
||||
},
|
||||
{
|
||||
name: 'lora',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Apply LoRA models for specialized styles or subjects.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/lora/'
|
||||
},
|
||||
{
|
||||
name: 'inpaint_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Edit specific parts of images seamlessly.',
|
||||
thumbnailVariant: 'compareSlider',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/inpaint/'
|
||||
},
|
||||
{
|
||||
name: 'inpain_model_outpainting',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Extend images beyond their original boundaries.',
|
||||
thumbnailVariant: 'compareSlider',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/inpaint/#outpainting'
|
||||
},
|
||||
{
|
||||
name: 'embedding_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Use textual inversion for consistent styles',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/textual_inversion_embeddings/'
|
||||
},
|
||||
{
|
||||
name: 'gligen_textbox_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Specify the location and size of objects.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/gligen/'
|
||||
},
|
||||
{
|
||||
name: 'lora_multiple',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Combine multiple LoRA models for unique results.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/lora/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Flux',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'flux_dev_checkpoint_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create images using Flux development models.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-dev-1'
|
||||
},
|
||||
{
|
||||
name: 'flux_schnell',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images quickly with Flux Schnell.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-schnell-1'
|
||||
},
|
||||
{
|
||||
name: 'flux_fill_inpaint_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Fill in missing parts of images.',
|
||||
thumbnailVariant: 'compareSlider',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#fill-inpainting-model'
|
||||
},
|
||||
{
|
||||
name: 'flux_fill_outpaint_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Extend images using Flux outpainting.',
|
||||
thumbnailVariant: 'compareSlider',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#fill-inpainting-model'
|
||||
},
|
||||
{
|
||||
name: 'flux_canny_model_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images from edge detection.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#canny-and-depth'
|
||||
},
|
||||
{
|
||||
name: 'flux_depth_lora_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create images with depth-aware LoRA.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#canny-and-depth'
|
||||
},
|
||||
{
|
||||
name: 'flux_redux_model_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Transfer style from a reference image to guide image generation with Flux.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#redux'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'ControlNet',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'controlnet_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Control image generation with reference images.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/'
|
||||
},
|
||||
{
|
||||
name: '2_pass_pose_worship',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images from pose references.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#pose-controlnet'
|
||||
},
|
||||
{
|
||||
name: 'depth_controlnet',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create images with depth-aware generation.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#t2i-adapter-vs-controlnets'
|
||||
},
|
||||
{
|
||||
name: 'depth_t2i_adapter',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Quickly generate depth-aware images with a T2I adapter.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#t2i-adapter-vs-controlnets'
|
||||
},
|
||||
{
|
||||
name: 'mixing_controlnets',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Combine multiple ControlNet models together.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#mixing-controlnets'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Upscaling',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'hiresfix_latent_workflow',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Enhance image quality in latent space.',
|
||||
thumbnailVariant: 'zoomHover',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/'
|
||||
},
|
||||
{
|
||||
name: 'esrgan_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Use upscale models to enhance image quality.',
|
||||
thumbnailVariant: 'zoomHover',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/'
|
||||
},
|
||||
{
|
||||
name: 'hiresfix_esrgan_workflow',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Use upscale models during intermediate steps.',
|
||||
thumbnailVariant: 'zoomHover',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/#non-latent-upscaling'
|
||||
},
|
||||
{
|
||||
name: 'latent_upscale_different_prompt_model',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Upscale and change prompt across passes',
|
||||
thumbnailVariant: 'zoomHover',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/#more-examples'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Video',
|
||||
type: 'video',
|
||||
templates: [
|
||||
{
|
||||
name: 'ltxv_text_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate videos from text descriptions.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/ltxv/#text-to-video'
|
||||
},
|
||||
{
|
||||
name: 'ltxv_image_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Convert still images into videos.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/ltxv/#image-to-video'
|
||||
},
|
||||
{
|
||||
name: 'mochi_text_to_video_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create videos with Mochi model.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/mochi/'
|
||||
},
|
||||
{
|
||||
name: 'hunyuan_video_text_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate videos using Hunyuan model.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_video/'
|
||||
},
|
||||
{
|
||||
name: 'image_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Transform images into animated videos.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/video/#image-to-video'
|
||||
},
|
||||
{
|
||||
name: 'txt_to_image_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Generate images from text and then convert them into videos.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/video/#image-to-video'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'SD3.5',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'sd3.5_simple_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images with SD 3.5.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35'
|
||||
},
|
||||
{
|
||||
name: 'sd3.5_large_canny_controlnet_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Use edge detection to guide image generation with SD 3.5.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35-controlnets'
|
||||
},
|
||||
{
|
||||
name: 'sd3.5_large_depth',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create depth-aware images with SD 3.5.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35-controlnets'
|
||||
},
|
||||
{
|
||||
name: 'sd3.5_large_blur',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Generate images from blurred reference images with SD 3.5.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35-controlnets'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'SDXL',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'sdxl_simple_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create high-quality images with SDXL.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/'
|
||||
},
|
||||
{
|
||||
name: 'sdxl_refiner_prompt_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Enhance SDXL outputs with refiners.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/'
|
||||
},
|
||||
{
|
||||
name: 'sdxl_revision_text_prompts',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Transfer concepts from reference images to guide image generation with SDXL.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/#revision'
|
||||
},
|
||||
{
|
||||
name: 'sdxl_revision_zero_positive',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Add text prompts alongside reference images to guide image generation with SDXL.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/#revision'
|
||||
},
|
||||
{
|
||||
name: 'sdxlturbo_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images in a single step with SDXL Turbo.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sdturbo/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Area Composition',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'area_composition',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Control image composition with areas.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/area_composition/'
|
||||
},
|
||||
{
|
||||
name: 'area_composition_reversed',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Reverse area composition workflow.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/area_composition/'
|
||||
},
|
||||
{
|
||||
name: 'area_composition_square_area_for_subject',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create consistent subject placement.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/area_composition/#increasing-consistency-of-images-with-area-composition'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: '3D',
|
||||
type: 'video',
|
||||
templates: [
|
||||
{
|
||||
name: 'stable_zero123_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate 3D views from single images.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/3d/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Audio',
|
||||
type: 'audio',
|
||||
templates: [
|
||||
{
|
||||
name: 'stable_audio_example',
|
||||
mediaType: 'audio',
|
||||
mediaSubtype: 'mp3',
|
||||
description: 'Generate audio from text descriptions.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/audio/'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
Taken from: https://github.com/gkjohnson/threejs-sandbox/tree/master/conditional-lines
|
||||
under MIT license
|
||||
*/
|
||||
import { BufferAttribute, BufferGeometry, Vector3 } from 'three'
|
||||
|
||||
const vec = new Vector3()
|
||||
export class OutsideEdgesGeometry extends BufferGeometry {
|
||||
constructor(geometry) {
|
||||
super()
|
||||
|
||||
const edgeInfo = {}
|
||||
const index = geometry.index
|
||||
const position = geometry.attributes.position
|
||||
for (let i = 0, l = index.count; i < l; i += 3) {
|
||||
const indices = [index.getX(i + 0), index.getX(i + 1), index.getX(i + 2)]
|
||||
|
||||
for (let j = 0; j < 3; j++) {
|
||||
const index0 = indices[j]
|
||||
const index1 = indices[(j + 1) % 3]
|
||||
|
||||
const hash = `${index0}_${index1}`
|
||||
const reverseHash = `${index1}_${index0}`
|
||||
if (reverseHash in edgeInfo) {
|
||||
delete edgeInfo[reverseHash]
|
||||
} else {
|
||||
edgeInfo[hash] = [index0, index1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const edgePositions = []
|
||||
for (const key in edgeInfo) {
|
||||
const [i0, i1] = edgeInfo[key]
|
||||
|
||||
vec.fromBufferAttribute(position, i0)
|
||||
edgePositions.push(vec.x, vec.y, vec.z)
|
||||
|
||||
vec.fromBufferAttribute(position, i1)
|
||||
edgePositions.push(vec.x, vec.y, vec.z)
|
||||
}
|
||||
|
||||
this.setAttribute(
|
||||
'position',
|
||||
new BufferAttribute(new Float32Array(edgePositions), 3, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import arCommands from './locales/ar/commands.json'
|
||||
import ar from './locales/ar/main.json'
|
||||
import arNodes from './locales/ar/nodeDefs.json'
|
||||
import arSettings from './locales/ar/settings.json'
|
||||
import enCommands from './locales/en/commands.json'
|
||||
import en from './locales/en/main.json'
|
||||
import enNodes from './locales/en/nodeDefs.json'
|
||||
@@ -54,8 +50,7 @@ const messages = {
|
||||
ja: buildLocale(ja, jaNodes, jaCommands, jaSettings),
|
||||
ko: buildLocale(ko, koNodes, koCommands, koSettings),
|
||||
fr: buildLocale(fr, frNodes, frCommands, frSettings),
|
||||
es: buildLocale(es, esNodes, esCommands, esSettings),
|
||||
ar: buildLocale(ar, arNodes, arCommands, arSettings)
|
||||
es: buildLocale(es, esNodes, esCommands, esSettings)
|
||||
}
|
||||
|
||||
export const i18n = createI18n({
|
||||
|
||||
@@ -495,16 +495,6 @@
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.graphmenu-entry.danger,
|
||||
.litemenu-entry.danger {
|
||||
color: var(--error-text) !important;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry.danger:hover:not(.disabled) {
|
||||
color: var(--error-text) !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.graphmenu-entry.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { toString } from 'lodash'
|
||||
|
||||
import {
|
||||
SUBGRAPH_INPUT_ID,
|
||||
SUBGRAPH_OUTPUT_ID
|
||||
@@ -36,6 +34,7 @@ import {
|
||||
alignToContainer,
|
||||
createBounds
|
||||
} from './measure'
|
||||
import { stringOrEmpty } from './strings'
|
||||
import { SubgraphInput } from './subgraph/SubgraphInput'
|
||||
import { SubgraphInputNode } from './subgraph/SubgraphInputNode'
|
||||
import { SubgraphOutput } from './subgraph/SubgraphOutput'
|
||||
@@ -2028,7 +2027,7 @@ export class LGraph
|
||||
if (url instanceof Blob || url instanceof File) {
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener('load', (event) => {
|
||||
const result = toString(event.target?.result)
|
||||
const result = stringOrEmpty(event.target?.result)
|
||||
const data = JSON.parse(result)
|
||||
this.configure(data)
|
||||
callback?.()
|
||||
@@ -2332,9 +2331,6 @@ export class Subgraph
|
||||
nodes: this.nodes.map((node) => node.serialize()),
|
||||
groups: this.groups.map((group) => group.serialize()),
|
||||
links: [...this.links.values()].map((x) => x.asSerialisable()),
|
||||
reroutes: this.reroutes.size
|
||||
? [...this.reroutes.values()].map((x) => x.asSerialisable())
|
||||
: undefined,
|
||||
extra: this.extra
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { toString } from 'lodash'
|
||||
|
||||
import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||
|
||||
import { CanvasPointer } from './CanvasPointer'
|
||||
@@ -57,6 +55,7 @@ import {
|
||||
snapPoint
|
||||
} from './measure'
|
||||
import { NodeInputSlot } from './node/NodeInputSlot'
|
||||
import { stringOrEmpty } from './strings'
|
||||
import { Subgraph } from './subgraph/Subgraph'
|
||||
import { SubgraphIONodeBase } from './subgraph/SubgraphIONodeBase'
|
||||
import { SubgraphInputNode } from './subgraph/SubgraphInputNode'
|
||||
@@ -1245,7 +1244,7 @@ export class LGraphCanvas
|
||||
value = LGraphCanvas.getPropertyPrintableValue(value, info.values)
|
||||
|
||||
// value could contain invalid html characters, clean that
|
||||
value = LGraphCanvas.decodeHTML(toString(value))
|
||||
value = LGraphCanvas.decodeHTML(stringOrEmpty(value))
|
||||
entries.push({
|
||||
content:
|
||||
`<span class='property_name'>${info.label || i}</span>` +
|
||||
@@ -6066,7 +6065,7 @@ export class LGraphCanvas
|
||||
}
|
||||
ctx.fillStyle = '#FFF'
|
||||
ctx.fillText(
|
||||
toString(node.order),
|
||||
stringOrEmpty(node.order),
|
||||
node.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * -0.5,
|
||||
node.pos[1] - 6
|
||||
)
|
||||
@@ -6234,17 +6233,9 @@ export class LGraphCanvas
|
||||
break
|
||||
}
|
||||
|
||||
case 'Delete': {
|
||||
// segment can be a Reroute object, in which case segment.id is the reroute id
|
||||
const linkId =
|
||||
segment instanceof Reroute
|
||||
? segment.linkIds.values().next().value
|
||||
: segment.id
|
||||
if (linkId !== undefined) {
|
||||
graph.removeLink(linkId)
|
||||
}
|
||||
case 'Delete':
|
||||
graph.removeLink(segment.id)
|
||||
break
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -8253,9 +8244,7 @@ export class LGraphCanvas
|
||||
if (_slot.removable) {
|
||||
menu_info.push(null)
|
||||
menu_info.push(
|
||||
_slot.locked
|
||||
? 'Cannot remove'
|
||||
: { content: 'Remove Slot', slot, className: 'danger' }
|
||||
_slot.locked ? 'Cannot remove' : { content: 'Remove Slot', slot }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { EmptySubgraphInput } from '@/lib/litegraph/src/subgraph/EmptySubgraphInput'
|
||||
import { EmptySubgraphOutput } from '@/lib/litegraph/src/subgraph/EmptySubgraphOutput'
|
||||
import { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
|
||||
import { SubgraphInputNode } from '@/lib/litegraph/src/subgraph/SubgraphInputNode'
|
||||
@@ -640,64 +638,20 @@ export class LinkConnector {
|
||||
|
||||
if (connectingTo === 'input' && ioNode instanceof SubgraphOutputNode) {
|
||||
const output = ioNode.getSlotInPosition(canvasX, canvasY)
|
||||
if (!output) {
|
||||
this.dropOnNothing(event)
|
||||
return
|
||||
}
|
||||
|
||||
// Track the actual slot to use for all connections
|
||||
let targetSlot = output
|
||||
if (!output) throw new Error('No output slot found for link.')
|
||||
|
||||
for (const link of renderLinks) {
|
||||
link.connectToSubgraphOutput(targetSlot, this.events)
|
||||
|
||||
// If we just connected to an EmptySubgraphOutput, check if we should reuse the slot
|
||||
if (output instanceof EmptySubgraphOutput && ioNode.slots.length > 0) {
|
||||
// Get the last created slot (newest one)
|
||||
const createdSlot = ioNode.slots[ioNode.slots.length - 1]
|
||||
|
||||
// Only reuse the slot if the next link's type would be compatible
|
||||
// Otherwise, keep using EmptySubgraphOutput to create a new slot
|
||||
const nextLink = renderLinks[renderLinks.indexOf(link) + 1]
|
||||
if (nextLink && link.fromSlot.type === nextLink.fromSlot.type) {
|
||||
targetSlot = createdSlot
|
||||
} else {
|
||||
// Reset to EmptySubgraphOutput for different types
|
||||
targetSlot = output
|
||||
}
|
||||
}
|
||||
link.connectToSubgraphOutput(output, this.events)
|
||||
}
|
||||
} else if (
|
||||
connectingTo === 'output' &&
|
||||
ioNode instanceof SubgraphInputNode
|
||||
) {
|
||||
const input = ioNode.getSlotInPosition(canvasX, canvasY)
|
||||
if (!input) {
|
||||
this.dropOnNothing(event)
|
||||
return
|
||||
}
|
||||
|
||||
// Same logic for SubgraphInputNode if needed
|
||||
let targetSlot = input
|
||||
if (!input) throw new Error('No input slot found for link.')
|
||||
|
||||
for (const link of renderLinks) {
|
||||
link.connectToSubgraphInput(targetSlot, this.events)
|
||||
|
||||
// If we just connected to an EmptySubgraphInput, check if we should reuse the slot
|
||||
if (input instanceof EmptySubgraphInput && ioNode.slots.length > 0) {
|
||||
// Get the last created slot (newest one)
|
||||
const createdSlot = ioNode.slots[ioNode.slots.length - 1]
|
||||
|
||||
// Only reuse the slot if the next link's type would be compatible
|
||||
// Otherwise, keep using EmptySubgraphInput to create a new slot
|
||||
const nextLink = renderLinks[renderLinks.indexOf(link) + 1]
|
||||
if (nextLink && link.fromSlot.type === nextLink.fromSlot.type) {
|
||||
targetSlot = createdSlot
|
||||
} else {
|
||||
// Reset to EmptySubgraphInput for different types
|
||||
targetSlot = input
|
||||
}
|
||||
}
|
||||
link.connectToSubgraphInput(input, this.events)
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
import type { ISlotType } from './litegraph'
|
||||
|
||||
/**
|
||||
* Uses the standard String() function to coerce to string, unless the value is null or undefined - then null.
|
||||
* @param value The value to convert
|
||||
* @returns String(value) or null
|
||||
*/
|
||||
export function stringOrNull(value: unknown): string | null {
|
||||
return value == null ? null : String(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the standard String() function to coerce to string, unless the value is null or undefined - then an empty string
|
||||
* @param value The value to convert
|
||||
* @returns String(value) or ""
|
||||
*/
|
||||
export function stringOrEmpty(value: unknown): string {
|
||||
return value == null ? '' : String(value)
|
||||
}
|
||||
|
||||
export function parseSlotTypes(type: ISlotType): string[] {
|
||||
return type == '' || type == '0'
|
||||
? ['*']
|
||||
|
||||
@@ -191,7 +191,7 @@ export abstract class SubgraphIONodeBase<
|
||||
* @param event The event that triggered the context menu.
|
||||
*/
|
||||
protected showSlotContextMenu(slot: TSlot, event: CanvasPointerEvent): void {
|
||||
const options: (IContextMenuValue | null)[] = this.#getSlotMenuOptions(slot)
|
||||
const options: IContextMenuValue[] = this.#getSlotMenuOptions(slot)
|
||||
if (!(options.length > 0)) return
|
||||
|
||||
new LiteGraph.ContextMenu(options, {
|
||||
@@ -208,26 +208,20 @@ export abstract class SubgraphIONodeBase<
|
||||
* @param slot The slot to get the context menu options for.
|
||||
* @returns The context menu options.
|
||||
*/
|
||||
#getSlotMenuOptions(slot: TSlot): (IContextMenuValue | null)[] {
|
||||
const options: (IContextMenuValue | null)[] = []
|
||||
#getSlotMenuOptions(slot: TSlot): IContextMenuValue[] {
|
||||
const options: IContextMenuValue[] = []
|
||||
|
||||
// Disconnect option if slot has connections
|
||||
if (slot !== this.emptySlot && slot.linkIds.length > 0) {
|
||||
options.push({ content: 'Disconnect Links', value: 'disconnect' })
|
||||
}
|
||||
|
||||
// Rename slot option (except for the empty slot)
|
||||
// Remove / rename slot option (except for the empty slot)
|
||||
if (slot !== this.emptySlot) {
|
||||
options.push({ content: 'Rename Slot', value: 'rename' })
|
||||
}
|
||||
|
||||
if (slot !== this.emptySlot) {
|
||||
options.push(null) // separator
|
||||
options.push({
|
||||
content: 'Remove Slot',
|
||||
value: 'remove',
|
||||
className: 'danger'
|
||||
})
|
||||
options.push(
|
||||
{ content: 'Remove Slot', value: 'remove' },
|
||||
{ content: 'Rename Slot', value: 'rename' }
|
||||
)
|
||||
}
|
||||
|
||||
return options
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { pull } from 'lodash'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
|
||||
@@ -11,6 +9,7 @@ import type {
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { removeFromArray } from '@/lib/litegraph/src/utils/collections'
|
||||
|
||||
import type { SubgraphInput } from './SubgraphInput'
|
||||
import type { SubgraphOutputNode } from './SubgraphOutputNode'
|
||||
@@ -60,7 +59,7 @@ export class SubgraphOutput extends SubgraphSlot {
|
||||
existingLink.disconnect(subgraph, 'input')
|
||||
const resolved = existingLink.resolve(subgraph)
|
||||
const links = resolved.output?.links
|
||||
if (links) pull(links, existingLink.id)
|
||||
if (links) removeFromArray(links, existingLink.id)
|
||||
}
|
||||
|
||||
const link = new LLink(
|
||||
|
||||
@@ -112,3 +112,11 @@ export function findFreeSlotOfType<T extends { type: ISlotType }>(
|
||||
}
|
||||
return wildSlot ?? occupiedSlot ?? occupiedWildSlot
|
||||
}
|
||||
|
||||
export function removeFromArray<T>(array: T[], value: T): boolean {
|
||||
const index = array.indexOf(value)
|
||||
const found = index !== -1
|
||||
|
||||
if (found) array.splice(index, 1)
|
||||
return found
|
||||
}
|
||||
|
||||
289
src/lib/litegraph/test/subgraph/fixtures/advancedEventHelpers.ts
Normal file
289
src/lib/litegraph/test/subgraph/fixtures/advancedEventHelpers.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import { expect } from 'vitest'
|
||||
|
||||
import type { CapturedEvent } from './subgraphHelpers'
|
||||
|
||||
/**
|
||||
* Extended captured event with additional metadata not in the base infrastructure
|
||||
*/
|
||||
export interface ExtendedCapturedEvent<T = unknown> extends CapturedEvent<T> {
|
||||
defaultPrevented: boolean
|
||||
bubbles: boolean
|
||||
cancelable: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an enhanced event capture that includes additional event properties
|
||||
* This extends the basic createEventCapture with more metadata
|
||||
*/
|
||||
export function createExtendedEventCapture<T = unknown>(
|
||||
eventTarget: EventTarget,
|
||||
eventTypes: string[]
|
||||
) {
|
||||
const capturedEvents: ExtendedCapturedEvent<T>[] = []
|
||||
const listeners: Array<() => void> = []
|
||||
|
||||
for (const eventType of eventTypes) {
|
||||
const listener = (event: Event) => {
|
||||
capturedEvents.push({
|
||||
type: eventType,
|
||||
detail: (event as CustomEvent<T>).detail,
|
||||
timestamp: Date.now(),
|
||||
defaultPrevented: event.defaultPrevented,
|
||||
bubbles: event.bubbles,
|
||||
cancelable: event.cancelable
|
||||
})
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(eventType, listener)
|
||||
listeners.push(() => eventTarget.removeEventListener(eventType, listener))
|
||||
}
|
||||
|
||||
return {
|
||||
events: capturedEvents,
|
||||
clear: () => {
|
||||
capturedEvents.length = 0
|
||||
},
|
||||
cleanup: () => {
|
||||
for (const cleanup of listeners) cleanup()
|
||||
},
|
||||
getEventsByType: (type: string) =>
|
||||
capturedEvents.filter((e) => e.type === type),
|
||||
getLatestEvent: () => capturedEvents.at(-1),
|
||||
getFirstEvent: () => capturedEvents[0],
|
||||
|
||||
/**
|
||||
* Wait for a specific event type to be captured
|
||||
*/
|
||||
async waitForEvent(
|
||||
type: string,
|
||||
timeoutMs: number = 1000
|
||||
): Promise<ExtendedCapturedEvent<T>> {
|
||||
const existingEvent = capturedEvents.find((e) => e.type === type)
|
||||
if (existingEvent) return existingEvent
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
eventTarget.removeEventListener(type, eventListener)
|
||||
reject(new Error(`Event ${type} not received within ${timeoutMs}ms`))
|
||||
}, timeoutMs)
|
||||
|
||||
const eventListener = (_event: Event) => {
|
||||
const capturedEvent = capturedEvents.find((e) => e.type === type)
|
||||
if (capturedEvent) {
|
||||
clearTimeout(timeout)
|
||||
eventTarget.removeEventListener(type, eventListener)
|
||||
resolve(capturedEvent)
|
||||
}
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(type, eventListener)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Wait for a sequence of events to occur in order
|
||||
*/
|
||||
async waitForSequence(
|
||||
expectedSequence: string[],
|
||||
timeoutMs: number = 1000
|
||||
): Promise<ExtendedCapturedEvent<T>[]> {
|
||||
// Check if sequence is already complete
|
||||
if (capturedEvents.length >= expectedSequence.length) {
|
||||
const actualSequence = capturedEvents
|
||||
.slice(0, expectedSequence.length)
|
||||
.map((e) => e.type)
|
||||
if (
|
||||
JSON.stringify(actualSequence) === JSON.stringify(expectedSequence)
|
||||
) {
|
||||
return capturedEvents.slice(0, expectedSequence.length)
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup()
|
||||
const actual = capturedEvents.map((e) => e.type).join(', ')
|
||||
const expected = expectedSequence.join(', ')
|
||||
reject(
|
||||
new Error(
|
||||
`Event sequence not completed within ${timeoutMs}ms. Expected: ${expected}, Got: ${actual}`
|
||||
)
|
||||
)
|
||||
}, timeoutMs)
|
||||
|
||||
const checkSequence = () => {
|
||||
if (capturedEvents.length >= expectedSequence.length) {
|
||||
const actualSequence = capturedEvents
|
||||
.slice(0, expectedSequence.length)
|
||||
.map((e) => e.type)
|
||||
if (
|
||||
JSON.stringify(actualSequence) ===
|
||||
JSON.stringify(expectedSequence)
|
||||
) {
|
||||
cleanup()
|
||||
resolve(capturedEvents.slice(0, expectedSequence.length))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const eventListener = () => checkSequence()
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout)
|
||||
for (const type of expectedSequence) {
|
||||
eventTarget.removeEventListener(type, eventListener)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for all expected event types
|
||||
for (const type of expectedSequence) {
|
||||
eventTarget.addEventListener(type, eventListener)
|
||||
}
|
||||
|
||||
// Initial check in case events already exist
|
||||
checkSequence()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for memory leak testing
|
||||
*/
|
||||
export interface MemoryLeakTestOptions {
|
||||
cycles?: number
|
||||
instancesPerCycle?: number
|
||||
gcAfterEach?: boolean
|
||||
maxMemoryGrowth?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a memory leak test factory
|
||||
* Useful for testing that event listeners and references are properly cleaned up
|
||||
*/
|
||||
export function createMemoryLeakTest<T>(
|
||||
// @ts-expect-error TODO: Fix after merge - T does not satisfy constraint 'object'
|
||||
setupFn: () => { ref: WeakRef<T>; cleanup: () => void },
|
||||
options: MemoryLeakTestOptions = {}
|
||||
) {
|
||||
const {
|
||||
cycles = 1,
|
||||
instancesPerCycle = 1,
|
||||
gcAfterEach = true,
|
||||
maxMemoryGrowth = 0
|
||||
} = options
|
||||
|
||||
return async () => {
|
||||
// @ts-expect-error Type 'T' does not satisfy the constraint 'object'
|
||||
const refs: WeakRef<T>[] = []
|
||||
const initialMemory = process.memoryUsage?.()?.heapUsed || 0
|
||||
|
||||
for (let cycle = 0; cycle < cycles; cycle++) {
|
||||
// @ts-expect-error Type 'T' does not satisfy the constraint 'object'
|
||||
const cycleRefs: WeakRef<T>[] = []
|
||||
|
||||
for (let instance = 0; instance < instancesPerCycle; instance++) {
|
||||
const { ref, cleanup } = setupFn()
|
||||
cycleRefs.push(ref)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
refs.push(...cycleRefs)
|
||||
|
||||
if (gcAfterEach && global.gc) {
|
||||
global.gc()
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
}
|
||||
}
|
||||
|
||||
// Final garbage collection
|
||||
if (global.gc) {
|
||||
global.gc()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Check if objects were collected
|
||||
const uncollectedRefs = refs.filter((ref) => ref.deref() !== undefined)
|
||||
if (uncollectedRefs.length > 0) {
|
||||
console.warn(
|
||||
`${uncollectedRefs.length} objects were not garbage collected`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Memory growth check
|
||||
if (maxMemoryGrowth > 0 && process.memoryUsage) {
|
||||
const finalMemory = process.memoryUsage().heapUsed
|
||||
const memoryGrowth = finalMemory - initialMemory
|
||||
|
||||
if (memoryGrowth > maxMemoryGrowth) {
|
||||
throw new Error(
|
||||
`Memory growth ${memoryGrowth} bytes exceeds limit ${maxMemoryGrowth} bytes`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return refs
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a performance monitor for event operations
|
||||
*/
|
||||
export function createEventPerformanceMonitor() {
|
||||
const measurements: Array<{
|
||||
operation: string
|
||||
duration: number
|
||||
timestamp: number
|
||||
}> = []
|
||||
|
||||
return {
|
||||
measure: <T>(operation: string, fn: () => T): T => {
|
||||
const start = performance.now()
|
||||
const result = fn()
|
||||
const end = performance.now()
|
||||
|
||||
measurements.push({
|
||||
operation,
|
||||
duration: end - start,
|
||||
timestamp: start
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
getMeasurements: () => [...measurements],
|
||||
|
||||
getAverageDuration: (operation: string) => {
|
||||
const operationMeasurements = measurements.filter(
|
||||
(m) => m.operation === operation
|
||||
)
|
||||
if (operationMeasurements.length === 0) return 0
|
||||
|
||||
const totalDuration = operationMeasurements.reduce(
|
||||
(sum, m) => sum + m.duration,
|
||||
0
|
||||
)
|
||||
return totalDuration / operationMeasurements.length
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
measurements.length = 0
|
||||
},
|
||||
|
||||
assertPerformance: (operation: string, maxDuration: number) => {
|
||||
// @ts-expect-error 'this' implicitly has type 'any'
|
||||
const measurements = this.getMeasurements()
|
||||
const relevantMeasurements = measurements.filter(
|
||||
// @ts-expect-error Parameter 'm' implicitly has an 'any' type
|
||||
(m) => m.operation === operation
|
||||
)
|
||||
if (relevantMeasurements.length === 0) return
|
||||
|
||||
const avgDuration =
|
||||
// @ts-expect-error Parameter 'sum' and 'm' implicitly have 'any' type
|
||||
relevantMeasurements.reduce((sum, m) => sum + m.duration, 0) /
|
||||
relevantMeasurements.length
|
||||
expect(avgDuration).toBeLessThan(maxDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "التحقق من التحديثات"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "فتح مجلد العقد المخصصة"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "فتح مجلد المدخلات"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "فتح مجلد السجلات"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "فتح extra_model_paths.yaml"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "فتح مجلد النماذج"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "فتح مجلد المخرجات"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "فتح أدوات المطور"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "دليل المستخدم لسطح المكتب"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "خروج"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "إعادة التثبيت"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "إعادة التشغيل"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "فتح عارض ثلاثي الأبعاد (بيتا) للعقدة المحددة"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "تصفح القوالب"
|
||||
},
|
||||
"Comfy_Canvas_AddEditModelStep": {
|
||||
"label": "إضافة خطوة تحرير النموذج"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "حذف العناصر المحددة"
|
||||
},
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "تعديل العرض ليناسب العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Down": {
|
||||
"label": "تحريك العقد المحددة للأسفل"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Left": {
|
||||
"label": "تحريك العقد المحددة لليسار"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Right": {
|
||||
"label": "تحريك العقد المحددة لليمين"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Up": {
|
||||
"label": "تحريك العقد المحددة للأعلى"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "إعادة تعيين العرض"
|
||||
},
|
||||
"Comfy_Canvas_Resize": {
|
||||
"label": "تغيير حجم العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleLinkVisibility": {
|
||||
"label": "تبديل رؤية الروابط في اللوحة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "تبديل القفل في اللوحة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "تبديل الخريطة المصغرة في اللوحة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "تجاوز/إلغاء تجاوز العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
|
||||
"label": "طي/توسيع العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
|
||||
"label": "كتم/إلغاء كتم العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
|
||||
"label": "تثبيت/إلغاء تثبيت العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelected_Pin": {
|
||||
"label": "تثبيت/إلغاء تثبيت العناصر المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ZoomIn": {
|
||||
"label": "تكبير"
|
||||
},
|
||||
"Comfy_Canvas_ZoomOut": {
|
||||
"label": "تصغير"
|
||||
},
|
||||
"Comfy_ClearPendingTasks": {
|
||||
"label": "مسح المهام المعلقة"
|
||||
},
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "مسح سير العمل"
|
||||
},
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "الاتصال بالدعم"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "تكرار سير العمل الحالي"
|
||||
},
|
||||
"Comfy_ExportWorkflow": {
|
||||
"label": "تصدير سير العمل"
|
||||
},
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "تصدير سير العمل (تنسيق API)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "إرسال ملاحظات"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "تحويل التحديد إلى رسم فرعي"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "الخروج من الرسم البياني الفرعي"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "ضبط المجموعة على المحتويات"
|
||||
},
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "تجميع العقد المحددة"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "تحويل العقد المحددة إلى عقدة مجموعة"
|
||||
},
|
||||
"Comfy_GroupNode_ManageGroupNodes": {
|
||||
"label": "إدارة عقد المجموعات"
|
||||
},
|
||||
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
|
||||
"label": "إلغاء تجميع عقد المجموعات المحددة"
|
||||
},
|
||||
"Comfy_Help_AboutComfyUI": {
|
||||
"label": "حول ComfyUI"
|
||||
},
|
||||
"Comfy_Help_OpenComfyOrgDiscord": {
|
||||
"label": "فتح خادم Comfy-Org على Discord"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "فتح مستندات ComfyUI"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "فتح منتدى ComfyUI"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "فتح مشكلات ComfyUI"
|
||||
},
|
||||
"Comfy_Interrupt": {
|
||||
"label": "إيقاف مؤقت"
|
||||
},
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "تحميل سير العمل الافتراضي"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "تبديل مدير العقد المخصصة"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "تبديل شريط تقدم مدير العقد المخصصة"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "تقليل حجم الفرشاة في محرر القناع"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "زيادة حجم الفرشاة في محرر القناع"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "فتح محرر القناع للعقدة المحددة"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "سير عمل جديد فارغ"
|
||||
},
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "Clipspace"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "فتح سير عمل"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "إضافة الأمر إلى قائمة الانتظار"
|
||||
},
|
||||
"Comfy_QueuePromptFront": {
|
||||
"label": "إضافة الأمر إلى مقدمة قائمة الانتظار"
|
||||
},
|
||||
"Comfy_QueueSelectedOutputNodes": {
|
||||
"label": "إدراج عقد الإخراج المحددة في قائمة الانتظار"
|
||||
},
|
||||
"Comfy_Redo": {
|
||||
"label": "إعادة"
|
||||
},
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "تحديث تعريفات العقد"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "حفظ سير العمل"
|
||||
},
|
||||
"Comfy_SaveWorkflowAs": {
|
||||
"label": "حفظ سير العمل باسم"
|
||||
},
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "عرض نافذة الإعدادات"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "تبديل النمط (فاتح/داكن)"
|
||||
},
|
||||
"Comfy_Undo": {
|
||||
"label": "تراجع"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "فتح نافذة تسجيل الدخول"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "تسجيل الخروج"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "إغلاق سير العمل الحالي"
|
||||
},
|
||||
"Workspace_NextOpenedWorkflow": {
|
||||
"label": "سير العمل التالي المفتوح"
|
||||
},
|
||||
"Workspace_PreviousOpenedWorkflow": {
|
||||
"label": "سير العمل السابق المفتوح"
|
||||
},
|
||||
"Workspace_SearchBox_Toggle": {
|
||||
"label": "تبديل مربع البحث"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel": {
|
||||
"label": "تبديل اللوحة السفلية"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_command-terminal": {
|
||||
"label": "تبديل لوحة الطرفية السفلية"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_logs-terminal": {
|
||||
"label": "تبديل لوحة السجلات السفلية"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
|
||||
"label": "تبديل اللوحة السفلية الأساسية"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
|
||||
"label": "تبديل لوحة تحكم العرض السفلية"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel_Shortcuts": {
|
||||
"label": "عرض مربع حوار اختصارات لوحة المفاتيح"
|
||||
},
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "تبديل وضع التركيز"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "تبديل الشريط الجانبي لمكتبة النماذج",
|
||||
"tooltip": "مكتبة النماذج"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_node-library": {
|
||||
"label": "تبديل الشريط الجانبي لمكتبة العقد",
|
||||
"tooltip": "مكتبة العقد"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "تبديل الشريط الجانبي لقائمة الانتظار",
|
||||
"tooltip": "قائمة الانتظار"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "تبديل الشريط الجانبي لسير العمل",
|
||||
"tooltip": "سير العمل"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,416 +0,0 @@
|
||||
{
|
||||
"Comfy-Desktop_AutoUpdate": {
|
||||
"name": "التحقق تلقائيًا من التحديثات"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "إرسال إحصائيات الاستخدام المجهولة"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "مرآة تثبيت Pypi",
|
||||
"tooltip": "مرآة التثبيت الافتراضية لـ pip"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "مرآة تثبيت بايثون",
|
||||
"tooltip": "يتم تحميل تثبيتات بايثون المدارة من مشروع Astral python-build-standalone. يمكن تعيين هذا المتغير إلى عنوان مرآة لاستخدام مصدر مختلف لتثبيتات بايثون. سيحل العنوان المقدم محل https://github.com/astral-sh/python-build-standalone/releases/download في، مثلاً، https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz. يمكن قراءة التوزيعات من دليل محلي باستخدام نظام ملفات file://."
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "مرآة تثبيت Torch",
|
||||
"tooltip": "مرآة تثبيت pip لـ pytorch"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "نمط النافذة",
|
||||
"options": {
|
||||
"custom": "مخصص",
|
||||
"default": "افتراضي"
|
||||
},
|
||||
"tooltip": "مخصص: استبدال شريط عنوان النظام بالقائمة العلوية لـ ComfyUI"
|
||||
},
|
||||
"Comfy_Canvas_BackgroundImage": {
|
||||
"name": "صورة خلفية اللوحة",
|
||||
"tooltip": "رابط صورة لخلفية اللوحة. يمكنك النقر بزر الفأرة الأيمن على صورة في لوحة النتائج واختيار \"تعيين كخلفية\" لاستخدامها، أو رفع صورتك الخاصة باستخدام زر الرفع."
|
||||
},
|
||||
"Comfy_Canvas_NavigationMode": {
|
||||
"name": "وضع تنقل اللوحة",
|
||||
"options": {
|
||||
"Left-Click Pan (Legacy)": "سحب بالنقر الأيسر (قديم)",
|
||||
"Standard (New)": "قياسي (جديد)"
|
||||
}
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
"name": "عرض صندوق أدوات التحديد"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "طلب التأكيد عند مسح سير العمل"
|
||||
},
|
||||
"Comfy_DOMClippingEnabled": {
|
||||
"name": "تمكين قص عناصر DOM (قد يقلل التمكين من الأداء)"
|
||||
},
|
||||
"Comfy_DevMode": {
|
||||
"name": "تمكين خيارات وضع المطور (حفظ API، إلخ)"
|
||||
},
|
||||
"Comfy_DisableFloatRounding": {
|
||||
"name": "تعطيل تقريب عناصر التحكم العائمة الافتراضية",
|
||||
"tooltip": "(يتطلب إعادة تحميل الصفحة) لا يمكن تعطيل التقريب عندما يتم تعيينه من العقدة في الخلفية."
|
||||
},
|
||||
"Comfy_DisableSliders": {
|
||||
"name": "تعطيل منزلقات أدوات العقد"
|
||||
},
|
||||
"Comfy_EditAttention_Delta": {
|
||||
"name": "دقة تحكم +Ctrl فوق/تحت"
|
||||
},
|
||||
"Comfy_EnableTooltips": {
|
||||
"name": "تمكين التلميحات"
|
||||
},
|
||||
"Comfy_EnableWorkflowViewRestore": {
|
||||
"name": "حفظ واستعادة موقع اللوحة ومستوى التكبير في سير العمل"
|
||||
},
|
||||
"Comfy_FloatRoundingPrecision": {
|
||||
"name": "عدد أرقام التقريب العشرية لأدوات التحكم العائمة [0 = تلقائي]",
|
||||
"tooltip": "(يتطلب إعادة تحميل الصفحة)"
|
||||
},
|
||||
"Comfy_Graph_CanvasInfo": {
|
||||
"name": "عرض معلومات اللوحة في الزاوية السفلى اليسرى (الإطارات في الثانية، إلخ)"
|
||||
},
|
||||
"Comfy_Graph_CanvasMenu": {
|
||||
"name": "عرض قائمة لوحة الرسم البياني"
|
||||
},
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "تمكين اختصار التكبير السريع (Ctrl + Shift + سحب)"
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "علامات منتصف الروابط",
|
||||
"options": {
|
||||
"Arrow": "سهم",
|
||||
"Circle": "دائرة",
|
||||
"None": "لا شيء"
|
||||
}
|
||||
},
|
||||
"Comfy_Graph_ZoomSpeed": {
|
||||
"name": "سرعة تكبير اللوحة"
|
||||
},
|
||||
"Comfy_GroupSelectedNodes_Padding": {
|
||||
"name": "تباعد حول العقد المحددة في المجموعة"
|
||||
},
|
||||
"Comfy_Group_DoubleClickTitleToEdit": {
|
||||
"name": "انقر مزدوج على عنوان المجموعة للتحرير"
|
||||
},
|
||||
"Comfy_LinkRelease_Action": {
|
||||
"name": "الإجراء عند تحرير الرابط (بدون مفتاح تعديل)",
|
||||
"options": {
|
||||
"context menu": "قائمة السياق",
|
||||
"no action": "لا إجراء",
|
||||
"search box": "صندوق البحث"
|
||||
}
|
||||
},
|
||||
"Comfy_LinkRelease_ActionShift": {
|
||||
"name": "الإجراء عند تحرير الرابط (Shift)",
|
||||
"options": {
|
||||
"context menu": "قائمة السياق",
|
||||
"no action": "لا إجراء",
|
||||
"search box": "صندوق البحث"
|
||||
}
|
||||
},
|
||||
"Comfy_LinkRenderMode": {
|
||||
"name": "وضع عرض الروابط",
|
||||
"options": {
|
||||
"Hidden": "مخفي",
|
||||
"Linear": "خطي",
|
||||
"Spline": "منحنى",
|
||||
"Straight": "مستقيم"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "تمكين عارض ثلاثي الأبعاد (تجريبي)",
|
||||
"tooltip": "تمكين عارض ثلاثي الأبعاد (تجريبي) للعقد المحددة. تتيح هذه الميزة عرض النماذج ثلاثية الأبعاد والتفاعل معها مباشرة داخل العارض ثلاثي الأبعاد بحجمه الكامل."
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "لون الخلفية الابتدائي",
|
||||
"tooltip": "يحدد لون الخلفية الافتراضي للمشهد ثلاثي الأبعاد. يمكن تعديل هذا اللون لكل عنصر ثلاثي الأبعاد بعد الإنشاء."
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "نوع الكاميرا الابتدائي",
|
||||
"options": {
|
||||
"orthographic": "متعامد",
|
||||
"perspective": "منظور"
|
||||
},
|
||||
"tooltip": "يحدد ما إذا كانت الكاميرا منظور أو متعامدة بشكل افتراضي عند إنشاء عنصر ثلاثي الأبعاد جديد. يمكن تعديل هذا الإعداد لكل عنصر بعد الإنشاء."
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "زيادة تعديل الضوء",
|
||||
"tooltip": "يتحكم في حجم الخطوة عند تعديل شدة الإضاءة في المشاهد ثلاثية الأبعاد. قيمة أصغر تسمح بتحكم أدق، وأكبر قيمة تعطي تغييرات أكثر وضوحًا."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "شدة الإضاءة الابتدائية",
|
||||
"tooltip": "يحدد مستوى سطوع الإضاءة الافتراضي في المشهد ثلاثي الأبعاد. يمكن تعديله لكل عنصر بعد الإنشاء."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "أقصى شدة إضاءة",
|
||||
"tooltip": "يحدد الحد الأقصى المسموح به لشدة الإضاءة في المشاهد ثلاثية الأبعاد."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "أدنى شدة إضاءة",
|
||||
"tooltip": "يحدد الحد الأدنى المسموح به لشدة الإضاءة في المشاهد ثلاثية الأبعاد."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "رؤية الشبكة الابتدائية",
|
||||
"tooltip": "يتحكم في ظهور الشبكة بشكل افتراضي عند إنشاء عنصر ثلاثي الأبعاد جديد."
|
||||
},
|
||||
"Comfy_Load3D_ShowPreview": {
|
||||
"name": "رؤية المعاينة الابتدائية",
|
||||
"tooltip": "يتحكم في ظهور شاشة المعاينة بشكل افتراضي عند إنشاء عنصر ثلاثي الأبعاد جديد."
|
||||
},
|
||||
"Comfy_Locale": {
|
||||
"name": "اللغة"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
|
||||
"name": "مضاعف سرعة تعديل الفرشاة",
|
||||
"tooltip": "يتحكم في سرعة تغير حجم الفرشاة وصلابتها أثناء التعديل. القيم الأعلى تعني تغييرات أسرع."
|
||||
},
|
||||
"Comfy_MaskEditor_UseDominantAxis": {
|
||||
"name": "تقييد تعديل الفرشاة إلى المحور السائد",
|
||||
"tooltip": "عند التمكين، تؤثر التعديلات على الحجم أو الصلابة فقط بناءً على الاتجاه الذي تتحرك فيه أكثر."
|
||||
},
|
||||
"Comfy_MaskEditor_UseNewEditor": {
|
||||
"name": "استخدام محرر القناع الجديد",
|
||||
"tooltip": "التحويل إلى واجهة محرر القناع الجديدة"
|
||||
},
|
||||
"Comfy_ModelLibrary_AutoLoadAll": {
|
||||
"name": "تحميل جميع مجلدات النماذج تلقائيًا",
|
||||
"tooltip": "إذا كانت صحيحة، سيتم تحميل جميع المجلدات عند فتح مكتبة النماذج (قد يسبب تأخيرًا أثناء التحميل). إذا كانت خاطئة، يتم تحميل مجلدات النماذج على مستوى الجذر فقط عند النقر عليها."
|
||||
},
|
||||
"Comfy_ModelLibrary_NameFormat": {
|
||||
"name": "اسم العرض في شجرة مكتبة النماذج",
|
||||
"options": {
|
||||
"filename": "اسم الملف",
|
||||
"title": "العنوان"
|
||||
},
|
||||
"tooltip": "اختر \"اسم الملف\" لعرض اسم الملف المبسط بدون المجلد أو الامتداد \".safetensors\" في قائمة النماذج. اختر \"العنوان\" لعرض عنوان بيانات النموذج القابل للتكوين."
|
||||
},
|
||||
"Comfy_NodeBadge_NodeIdBadgeMode": {
|
||||
"name": "وضع شارة معرف العقدة",
|
||||
"options": {
|
||||
"None": "لا شيء",
|
||||
"Show all": "عرض الكل"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
|
||||
"name": "وضع شارة دورة حياة العقدة",
|
||||
"options": {
|
||||
"None": "لا شيء",
|
||||
"Show all": "عرض الكل"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeSourceBadgeMode": {
|
||||
"name": "وضع شارة مصدر العقدة",
|
||||
"options": {
|
||||
"Hide built-in": "إخفاء المدمج",
|
||||
"None": "لا شيء",
|
||||
"Show all": "عرض الكل"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_ShowApiPricing": {
|
||||
"name": "عرض شارة تسعير عقدة API"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "تنفيذ مربع بحث العقدة",
|
||||
"options": {
|
||||
"default": "افتراضي",
|
||||
"litegraph (legacy)": "لايت جراف (قديم)"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_NodePreview": {
|
||||
"name": "معاينة العقدة",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowCategory": {
|
||||
"name": "عرض فئة العقدة في نتائج البحث",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowIdName": {
|
||||
"name": "عرض اسم معرف العقدة في نتائج البحث",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
|
||||
"name": "عرض تكرار العقدة في نتائج البحث",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSuggestions_number": {
|
||||
"name": "عدد اقتراحات العقد",
|
||||
"tooltip": "خاص بمربع بحث / قائمة السياق في لايت جراف فقط"
|
||||
},
|
||||
"Comfy_Node_AllowImageSizeDraw": {
|
||||
"name": "عرض العرض × الارتفاع تحت معاينة الصورة"
|
||||
},
|
||||
"Comfy_Node_AutoSnapLinkToSlot": {
|
||||
"name": "التثبيت التلقائي للرابط إلى فتحة العقدة",
|
||||
"tooltip": "عند سحب رابط فوق عقدة، يتم تثبيت الرابط تلقائيًا على فتحة إدخال صالحة في العقدة"
|
||||
},
|
||||
"Comfy_Node_BypassAllLinksOnDelete": {
|
||||
"name": "الحفاظ على جميع الروابط عند حذف العقد",
|
||||
"tooltip": "عند حذف عقدة، حاول إعادة توصيل جميع روابط الإدخال والإخراج (تجاوز العقدة المحذوفة)"
|
||||
},
|
||||
"Comfy_Node_DoubleClickTitleToEdit": {
|
||||
"name": "النقر المزدوج على عنوان العقدة للتحرير"
|
||||
},
|
||||
"Comfy_Node_MiddleClickRerouteNode": {
|
||||
"name": "النقر الأوسط ينشئ عقدة إعادة توجيه جديدة"
|
||||
},
|
||||
"Comfy_Node_Opacity": {
|
||||
"name": "شفافية العقدة"
|
||||
},
|
||||
"Comfy_Node_ShowDeprecated": {
|
||||
"name": "عرض العقدة المهجورة في البحث",
|
||||
"tooltip": "العقد المهجورة مخفية افتراضيًا في واجهة المستخدم، لكنها تظل فعالة في سير العمل الحالي الذي يستخدمها."
|
||||
},
|
||||
"Comfy_Node_ShowExperimental": {
|
||||
"name": "عرض العقدة التجريبية في البحث",
|
||||
"tooltip": "يتم تمييز العقد التجريبية في واجهة المستخدم وقد تخضع لتغييرات كبيرة أو إزالتها في الإصدارات المستقبلية. استخدمها بحذر في سير العمل الإنتاجي."
|
||||
},
|
||||
"Comfy_Node_SnapHighlightsNode": {
|
||||
"name": "تثبيت يبرز العقدة",
|
||||
"tooltip": "عند سحب رابط فوق عقدة تحتوي على فتحة إدخال صالحة، يتم تمييز العقدة"
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "عرض تحديثات الإصدار",
|
||||
"tooltip": "عرض التحديثات للنماذج الجديدة والميزات الرئيسية."
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "تأخير انحراف نقرة المؤشر",
|
||||
"tooltip": "بعد الضغط على زر المؤشر، هذا هو الوقت الأقصى (بالملي ثانية) الذي يمكن تجاهل حركة المؤشر خلاله.\n\nيساعد على منع دفع الكائنات عن طريق الخطأ إذا تم تحريك المؤشر أثناء النقر."
|
||||
},
|
||||
"Comfy_Pointer_ClickDrift": {
|
||||
"name": "انحراف نقرة المؤشر (أقصى مسافة)",
|
||||
"tooltip": "إذا تحرك المؤشر أكثر من هذه المسافة أثناء الضغط على زر، يعتبر سحبًا بدلاً من نقرة.\n\nيساعد على منع دفع الكائنات عن طريق الخطأ إذا تم تحريك المؤشر أثناء النقر."
|
||||
},
|
||||
"Comfy_Pointer_DoubleClickTime": {
|
||||
"name": "فترة النقر المزدوج (قصوى)",
|
||||
"tooltip": "الوقت الأقصى بالملي ثانية بين النقرتين في النقر المزدوج. زيادة هذه القيمة قد تساعد إذا لم يتم تسجيل النقرات المزدوجة أحيانًا."
|
||||
},
|
||||
"Comfy_PreviewFormat": {
|
||||
"name": "تنسيق صورة المعاينة",
|
||||
"tooltip": "عند عرض معاينة في ويدجت الصورة، يتم تحويلها إلى صورة خفيفة الوزن، مثل webp، jpeg، webp;50، إلخ."
|
||||
},
|
||||
"Comfy_PromptFilename": {
|
||||
"name": "طلب اسم الملف عند حفظ سير العمل"
|
||||
},
|
||||
"Comfy_QueueButton_BatchCountLimit": {
|
||||
"name": "حد عدد الدُفعات",
|
||||
"tooltip": "العدد الأقصى للمهام التي تضاف إلى القائمة بنقرة زر واحدة"
|
||||
},
|
||||
"Comfy_Queue_MaxHistoryItems": {
|
||||
"name": "حجم تاريخ قائمة الانتظار",
|
||||
"tooltip": "العدد الأقصى للمهام المعروضة في تاريخ قائمة الانتظار."
|
||||
},
|
||||
"Comfy_Sidebar_Location": {
|
||||
"name": "موقع الشريط الجانبي",
|
||||
"options": {
|
||||
"left": "يسار",
|
||||
"right": "يمين"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_Size": {
|
||||
"name": "حجم الشريط الجانبي",
|
||||
"options": {
|
||||
"normal": "عادي",
|
||||
"small": "صغير"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "عرض موحد للشريط الجانبي"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "حجم الالتصاق بالشبكة",
|
||||
"tooltip": "عند سحب وتغيير حجم العقد مع الضغط على shift، يتم محاذاتها إلى الشبكة، هذا يتحكم في حجم تلك الشبكة."
|
||||
},
|
||||
"Comfy_TextareaWidget_FontSize": {
|
||||
"name": "حجم خط ويدجت منطقة النص"
|
||||
},
|
||||
"Comfy_TextareaWidget_Spellcheck": {
|
||||
"name": "التحقق من الإملاء في ويدجت منطقة النص"
|
||||
},
|
||||
"Comfy_TreeExplorer_ItemPadding": {
|
||||
"name": "حشو عناصر مستعرض الشجرة"
|
||||
},
|
||||
"Comfy_UseNewMenu": {
|
||||
"name": "استخدام القائمة الجديدة",
|
||||
"options": {
|
||||
"Bottom": "أسفل",
|
||||
"Disabled": "معطل",
|
||||
"Top": "أعلى"
|
||||
},
|
||||
"tooltip": "موقع شريط القائمة. على الأجهزة المحمولة، تُعرض القائمة دائمًا في الأعلى."
|
||||
},
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "التحقق من صحة سير العمل"
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "وضع التحكم في الودجت",
|
||||
"options": {
|
||||
"after": "بعد",
|
||||
"before": "قبل"
|
||||
},
|
||||
"tooltip": "يتحكم في متى يتم تحديث قيم الودجت (توليد عشوائي/زيادة/نقصان)، إما قبل إدراج الطلب في الطابور أو بعده."
|
||||
},
|
||||
"Comfy_Window_UnloadConfirmation": {
|
||||
"name": "عرض تأكيد عند إغلاق النافذة"
|
||||
},
|
||||
"Comfy_Workflow_AutoSave": {
|
||||
"name": "الحفظ التلقائي",
|
||||
"options": {
|
||||
"after delay": "بعد تأخير",
|
||||
"off": "إيقاف"
|
||||
}
|
||||
},
|
||||
"Comfy_Workflow_AutoSaveDelay": {
|
||||
"name": "تأخير الحفظ التلقائي (بالملي ثانية)",
|
||||
"tooltip": "ينطبق فقط إذا تم تعيين الحفظ التلقائي إلى \"بعد تأخير\"."
|
||||
},
|
||||
"Comfy_Workflow_ConfirmDelete": {
|
||||
"name": "عرض تأكيد عند حذف سير العمل"
|
||||
},
|
||||
"Comfy_Workflow_Persist": {
|
||||
"name": "الاحتفاظ بحالة سير العمل واستعادتها عند (إعادة) تحميل الصفحة"
|
||||
},
|
||||
"Comfy_Workflow_ShowMissingModelsWarning": {
|
||||
"name": "عرض تحذير النماذج المفقودة"
|
||||
},
|
||||
"Comfy_Workflow_ShowMissingNodesWarning": {
|
||||
"name": "عرض تحذير العقد المفقودة"
|
||||
},
|
||||
"Comfy_Workflow_SortNodeIdOnSave": {
|
||||
"name": "ترتيب معرفات العقد عند حفظ سير العمل"
|
||||
},
|
||||
"Comfy_Workflow_WorkflowTabsPosition": {
|
||||
"name": "موضع تبويبات سير العمل المفتوحة",
|
||||
"options": {
|
||||
"Sidebar": "الشريط الجانبي",
|
||||
"Topbar": "شريط الأعلى",
|
||||
"Topbar (2nd-row)": "شريط الأعلى (الصف الثاني)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "عتبة التكبير للرسم بجودة منخفضة",
|
||||
"tooltip": "عرض أشكال بجودة منخفضة عند التكبير للخارج"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "الحد الأقصى للإطارات في الثانية",
|
||||
"tooltip": "الحد الأقصى لعدد الإطارات في الثانية التي يسمح للرسم أن يعرضها. يحد من استخدام GPU على حساب السلاسة. إذا كانت 0، يتم استخدام معدل تحديث الشاشة. الافتراضي: 0"
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "تغيير مقياس قوائم ودجت كومبو العقدة عند التكبير"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "تصغير العقد الجديدة دائمًا",
|
||||
"tooltip": "تغيير حجم العقد إلى أصغر حجم ممكن عند الإنشاء. عند التعطيل، يتم توسيع العقدة المضافة حديثًا قليلاً لإظهار قيم الودجت."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "تأخير التلميح"
|
||||
},
|
||||
"LiteGraph_Reroute_SplineOffset": {
|
||||
"name": "إزاحة منحنى إعادة التوجيه",
|
||||
"tooltip": "إزاحة نقطة تحكم بيزير من نقطة مركز إعادة التوجيه"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "الالتصاق بالشبكة دائمًا"
|
||||
}
|
||||
}
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convert Selection to Subgraph"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Exit Subgraph"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Fit Group To Contents"
|
||||
},
|
||||
|
||||
@@ -975,7 +975,6 @@
|
||||
"Export (API)": "Export (API)",
|
||||
"Give Feedback": "Give Feedback",
|
||||
"Convert Selection to Subgraph": "Convert Selection to Subgraph",
|
||||
"Exit Subgraph": "Exit Subgraph",
|
||||
"Fit Group To Contents": "Fit Group To Contents",
|
||||
"Group Selected Nodes": "Group Selected Nodes",
|
||||
"Convert selected nodes to group node": "Convert selected nodes to group node",
|
||||
@@ -1670,12 +1669,5 @@
|
||||
"view": "View",
|
||||
"panelControls": "Panel Controls"
|
||||
}
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Node Colors",
|
||||
"showLinks": "Show Links",
|
||||
"showGroups": "Show Frames/Groups",
|
||||
"renderBypassState": "Render Bypass State",
|
||||
"renderErrorState": "Render Error State"
|
||||
}
|
||||
}
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convertir selección en subgrafo"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Salir de subgrafo"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Ajustar grupo al contenido"
|
||||
},
|
||||
|
||||
@@ -784,7 +784,6 @@
|
||||
"Desktop User Guide": "Guía de usuario de escritorio",
|
||||
"Duplicate Current Workflow": "Duplicar flujo de trabajo actual",
|
||||
"Edit": "Editar",
|
||||
"Exit Subgraph": "Salir de subgrafo",
|
||||
"Export": "Exportar",
|
||||
"Export (API)": "Exportar (API)",
|
||||
"Fit Group To Contents": "Ajustar grupo a contenidos",
|
||||
@@ -852,13 +851,6 @@
|
||||
"Zoom In": "Acercar",
|
||||
"Zoom Out": "Alejar"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Colores de nodos",
|
||||
"renderBypassState": "Mostrar estado de omisión",
|
||||
"renderErrorState": "Mostrar estado de error",
|
||||
"showGroups": "Mostrar marcos/grupos",
|
||||
"showLinks": "Mostrar enlaces"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "No mostrar esto de nuevo",
|
||||
"missingModels": "Modelos faltantes",
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convertir la sélection en sous-graphe"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Quitter le sous-graphe"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Ajuster le groupe au contenu"
|
||||
},
|
||||
|
||||
@@ -784,7 +784,6 @@
|
||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
|
||||
"Edit": "Éditer",
|
||||
"Exit Subgraph": "Quitter le sous-graphe",
|
||||
"Export": "Exporter",
|
||||
"Export (API)": "Exporter (API)",
|
||||
"Fit Group To Contents": "Ajuster le groupe au contenu",
|
||||
@@ -852,13 +851,6 @@
|
||||
"Zoom In": "Zoom avant",
|
||||
"Zoom Out": "Zoom arrière"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Couleurs des nœuds",
|
||||
"renderBypassState": "Afficher l’état de contournement",
|
||||
"renderErrorState": "Afficher l’état d’erreur",
|
||||
"showGroups": "Afficher les cadres/groupes",
|
||||
"showLinks": "Afficher les liens"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Ne plus afficher ce message",
|
||||
"missingModels": "Modèles manquants",
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "選択範囲をサブグラフに変換"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "サブグラフを終了"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "グループを内容に合わせて調整"
|
||||
},
|
||||
|
||||
@@ -784,7 +784,6 @@
|
||||
"Desktop User Guide": "デスクトップユーザーガイド",
|
||||
"Duplicate Current Workflow": "現在のワークフローを複製",
|
||||
"Edit": "編集",
|
||||
"Exit Subgraph": "サブグラフを終了",
|
||||
"Export": "エクスポート",
|
||||
"Export (API)": "エクスポート (API)",
|
||||
"Fit Group To Contents": "グループを内容に合わせる",
|
||||
@@ -852,13 +851,6 @@
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "ノードの色",
|
||||
"renderBypassState": "バイパス状態を表示",
|
||||
"renderErrorState": "エラー状態を表示",
|
||||
"showGroups": "フレーム/グループを表示",
|
||||
"showLinks": "リンクを表示"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "再度表示しない",
|
||||
"missingModels": "モデルが見つかりません",
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "선택 영역을 서브그래프로 변환"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "서브그래프 종료"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "그룹을 내용에 맞게 맞추기"
|
||||
},
|
||||
|
||||
@@ -784,7 +784,6 @@
|
||||
"Desktop User Guide": "데스크톱 사용자 가이드",
|
||||
"Duplicate Current Workflow": "현재 워크플로 복제",
|
||||
"Edit": "편집",
|
||||
"Exit Subgraph": "서브그래프 종료",
|
||||
"Export": "내보내기",
|
||||
"Export (API)": "내보내기 (API)",
|
||||
"Fit Group To Contents": "그룹을 내용에 맞게 조정",
|
||||
@@ -852,13 +851,6 @@
|
||||
"Zoom In": "확대",
|
||||
"Zoom Out": "축소"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "노드 색상",
|
||||
"renderBypassState": "바이패스 상태 렌더링",
|
||||
"renderErrorState": "에러 상태 렌더링",
|
||||
"showGroups": "프레임/그룹 표시",
|
||||
"showLinks": "링크 표시"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "다시 보지 않기",
|
||||
"missingModels": "모델이 없습니다",
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Преобразовать выделенное в подграф"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Выйти из подграфа"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Подогнать группу к содержимому"
|
||||
},
|
||||
|
||||
@@ -784,7 +784,6 @@
|
||||
"Desktop User Guide": "Руководство пользователя для настольных ПК",
|
||||
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
|
||||
"Edit": "Редактировать",
|
||||
"Exit Subgraph": "Выйти из подграфа",
|
||||
"Export": "Экспортировать",
|
||||
"Export (API)": "Экспорт (API)",
|
||||
"Fit Group To Contents": "Подогнать группу под содержимое",
|
||||
@@ -852,13 +851,6 @@
|
||||
"Zoom In": "Увеличить",
|
||||
"Zoom Out": "Уменьшить"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Цвета узлов",
|
||||
"renderBypassState": "Отображать состояние обхода",
|
||||
"renderErrorState": "Отображать состояние ошибки",
|
||||
"showGroups": "Показать фреймы/группы",
|
||||
"showLinks": "Показать связи"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Больше не показывать это",
|
||||
"missingModels": "Отсутствующие модели",
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "將選取內容轉換為子圖"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "離開子圖"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "調整群組以符合內容"
|
||||
},
|
||||
|
||||
@@ -784,7 +784,6 @@
|
||||
"Desktop User Guide": "桌面應用程式使用指南",
|
||||
"Duplicate Current Workflow": "複製目前工作流程",
|
||||
"Edit": "編輯",
|
||||
"Exit Subgraph": "離開子圖",
|
||||
"Export": "匯出",
|
||||
"Export (API)": "匯出(API)",
|
||||
"Fit Group To Contents": "群組貼合內容",
|
||||
@@ -852,13 +851,6 @@
|
||||
"Zoom In": "放大",
|
||||
"Zoom Out": "縮小"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "節點顏色",
|
||||
"renderBypassState": "顯示繞過狀態",
|
||||
"renderErrorState": "顯示錯誤狀態",
|
||||
"showGroups": "顯示框架/群組",
|
||||
"showLinks": "顯示連結"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不要再顯示此訊息",
|
||||
"missingModels": "缺少模型",
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "将选区转换为子图"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "退出子圖"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "适应节点框到内容"
|
||||
},
|
||||
|
||||
@@ -784,7 +784,6 @@
|
||||
"Desktop User Guide": "桌面端用户指南",
|
||||
"Duplicate Current Workflow": "复制当前工作流",
|
||||
"Edit": "编辑",
|
||||
"Exit Subgraph": "退出子圖",
|
||||
"Export": "导出",
|
||||
"Export (API)": "导出 (API)",
|
||||
"Fit Group To Contents": "适应组内容",
|
||||
@@ -852,13 +851,6 @@
|
||||
"Zoom In": "放大画面",
|
||||
"Zoom Out": "缩小画面"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "節點顏色",
|
||||
"renderBypassState": "顯示繞過狀態",
|
||||
"renderErrorState": "顯示錯誤狀態",
|
||||
"showGroups": "顯示框架/群組",
|
||||
"showLinks": "顯示連結"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不再显示此消息",
|
||||
"missingModels": "缺少模型",
|
||||
|
||||
@@ -36,8 +36,11 @@ Sentry.init({
|
||||
dsn: __SENTRY_DSN__,
|
||||
enabled: __SENTRY_ENABLED__,
|
||||
release: __COMFYUI_FRONTEND_VERSION__,
|
||||
integrations: [],
|
||||
autoSessionTracking: false,
|
||||
defaultIntegrations: false,
|
||||
normalizeDepth: 8,
|
||||
tracesSampleRate: 1.0
|
||||
tracesSampleRate: 0
|
||||
})
|
||||
app.directive('tooltip', Tooltip)
|
||||
app
|
||||
|
||||
@@ -6,12 +6,11 @@ import {
|
||||
createWebHistory
|
||||
} from 'vue-router'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import LayoutDefault from '@/views/layouts/LayoutDefault.vue'
|
||||
|
||||
import { useUserStore } from './stores/userStore'
|
||||
import { isElectron } from './utils/envUtil'
|
||||
|
||||
const isFileProtocol = window.location.protocol === 'file:'
|
||||
const basePath = isElectron() ? '/' : window.location.pathname
|
||||
|
||||
@@ -131,41 +130,4 @@ const router = createRouter({
|
||||
}
|
||||
})
|
||||
|
||||
// Global authentication guard
|
||||
router.beforeEach(async (_to, _from, next) => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
// Wait for Firebase auth to initialize
|
||||
if (!authStore.isInitialized) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const unwatch = authStore.$subscribe((_, state) => {
|
||||
if (state.isInitialized) {
|
||||
unwatch()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Check if user is authenticated (Firebase or API key)
|
||||
const authHeader = await authStore.getAuthHeader()
|
||||
|
||||
if (!authHeader) {
|
||||
// User is not authenticated, show sign-in dialog
|
||||
const dialogService = useDialogService()
|
||||
const loginSuccess = await dialogService.showSignInDialog()
|
||||
|
||||
if (loginSuccess) {
|
||||
// After successful login, proceed to the intended route
|
||||
next()
|
||||
} else {
|
||||
// User cancelled login, stay on current page or redirect to home
|
||||
next(false)
|
||||
}
|
||||
} else {
|
||||
// User is authenticated, proceed
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -112,11 +112,6 @@ const zDisplayComponentWsMessage = z.object({
|
||||
props: z.record(z.string(), z.any()).optional()
|
||||
})
|
||||
|
||||
const zNotificationWsMessage = z.object({
|
||||
value: z.string(),
|
||||
id: z.string().optional()
|
||||
})
|
||||
|
||||
const zTerminalSize = z.object({
|
||||
cols: z.number(),
|
||||
row: z.number()
|
||||
@@ -158,9 +153,15 @@ export type DisplayComponentWsMessage = z.infer<
|
||||
export type NodeProgressState = z.infer<typeof zNodeProgressState>
|
||||
export type ProgressStateWsMessage = z.infer<typeof zProgressStateWsMessage>
|
||||
export type FeatureFlagsWsMessage = z.infer<typeof zFeatureFlagsWsMessage>
|
||||
export type NotificationWsMessage = z.infer<typeof zNotificationWsMessage>
|
||||
// End of ws messages
|
||||
|
||||
const zPromptInputItem = z.object({
|
||||
inputs: z.record(z.string(), z.any()),
|
||||
class_type: zNodeType
|
||||
})
|
||||
|
||||
const zPromptInputs = z.record(zPromptInputItem)
|
||||
|
||||
const zExtraPngInfo = z
|
||||
.object({
|
||||
workflow: zComfyWorkflow
|
||||
@@ -172,6 +173,7 @@ const zExtraData = z.object({
|
||||
extra_pnginfo: zExtraPngInfo.optional(),
|
||||
client_id: z.string()
|
||||
})
|
||||
const zOutputsToExecute = z.array(zNodeId)
|
||||
|
||||
const zExecutionStartMessage = z.tuple([
|
||||
z.literal('execution_start'),
|
||||
@@ -212,11 +214,13 @@ const zStatus = z.object({
|
||||
messages: z.array(zStatusMessage)
|
||||
})
|
||||
|
||||
const zTaskPrompt = z.object({
|
||||
priority: zQueueIndex,
|
||||
prompt_id: zPromptId,
|
||||
extra_data: zExtraData
|
||||
})
|
||||
const zTaskPrompt = z.tuple([
|
||||
zQueueIndex,
|
||||
zPromptId,
|
||||
zPromptInputs,
|
||||
zExtraData,
|
||||
zOutputsToExecute
|
||||
])
|
||||
|
||||
const zRunningTaskItem = z.object({
|
||||
taskType: z.literal('Running'),
|
||||
@@ -252,20 +256,6 @@ const zHistoryTaskItem = z.object({
|
||||
meta: zTaskMeta.optional()
|
||||
})
|
||||
|
||||
// Raw history item from backend (without taskType)
|
||||
const zRawHistoryItem = z.object({
|
||||
prompt_id: zPromptId,
|
||||
prompt: zTaskPrompt,
|
||||
status: zStatus.optional(),
|
||||
outputs: zTaskOutput,
|
||||
meta: zTaskMeta.optional()
|
||||
})
|
||||
|
||||
// New API response format: { history: [{prompt_id: "...", ...}, ...] }
|
||||
const zHistoryResponse = z.object({
|
||||
history: z.array(zRawHistoryItem)
|
||||
})
|
||||
|
||||
const zTaskItem = z.union([
|
||||
zRunningTaskItem,
|
||||
zPendingTaskItem,
|
||||
@@ -288,8 +278,6 @@ export type RunningTaskItem = z.infer<typeof zRunningTaskItem>
|
||||
export type PendingTaskItem = z.infer<typeof zPendingTaskItem>
|
||||
// `/history`
|
||||
export type HistoryTaskItem = z.infer<typeof zHistoryTaskItem>
|
||||
export type RawHistoryItem = z.infer<typeof zRawHistoryItem>
|
||||
export type HistoryResponse = z.infer<typeof zHistoryResponse>
|
||||
export type TaskItem = z.infer<typeof zTaskItem>
|
||||
|
||||
export function validateTaskItem(taskItem: unknown) {
|
||||
@@ -483,11 +471,6 @@ const zSettings = z.object({
|
||||
'Comfy.InstalledVersion': z.string().nullable(),
|
||||
'Comfy.Node.AllowImageSizeDraw': z.boolean(),
|
||||
'Comfy.Minimap.Visible': z.boolean(),
|
||||
'Comfy.Minimap.NodeColors': z.boolean(),
|
||||
'Comfy.Minimap.ShowLinks': z.boolean(),
|
||||
'Comfy.Minimap.ShowGroups': z.boolean(),
|
||||
'Comfy.Minimap.RenderBypassState': z.boolean(),
|
||||
'Comfy.Minimap.RenderErrorState': z.boolean(),
|
||||
'Comfy.Canvas.NavigationMode': z.string(),
|
||||
'Comfy-Desktop.AutoUpdate': z.boolean(),
|
||||
'Comfy-Desktop.SendStatistics': z.boolean(),
|
||||
|
||||
@@ -4,49 +4,101 @@ import { fromZodError } from 'zod-validation-error'
|
||||
// GroupNode is hacking node id to be a string, so we need to allow that.
|
||||
// innerNode.id = `${this.node.id}:${i}`
|
||||
// Remove it after GroupNode is redesigned.
|
||||
export const zNodeId = z.union([z.number().int(), z.string()])
|
||||
export const zNodeInputName = z.string()
|
||||
/**
|
||||
* Node identifier that can be either a number or string.
|
||||
* Numeric IDs are standard, string IDs are used for GroupNodes.
|
||||
*/
|
||||
export const zNodeId = z
|
||||
.union([z.number().int(), z.string()])
|
||||
.describe('Unique identifier for a node in the workflow')
|
||||
|
||||
/** Name of a node input slot */
|
||||
export const zNodeInputName = z
|
||||
.string()
|
||||
.describe('The name of a node input parameter')
|
||||
|
||||
export type NodeId = z.infer<typeof zNodeId>
|
||||
export const zSlotIndex = z.union([
|
||||
z.number().int(),
|
||||
z
|
||||
.string()
|
||||
.transform((val) => parseInt(val))
|
||||
.refine((val) => !isNaN(val), {
|
||||
message: 'Invalid number'
|
||||
})
|
||||
])
|
||||
|
||||
/**
|
||||
* Index of a slot on a node (input or output).
|
||||
* Can be number or string that parses to a number.
|
||||
*/
|
||||
export const zSlotIndex = z
|
||||
.union([
|
||||
z.number().int(),
|
||||
z
|
||||
.string()
|
||||
.transform((val) => parseInt(val))
|
||||
.refine((val) => !isNaN(val), {
|
||||
message: 'Invalid number'
|
||||
})
|
||||
])
|
||||
.describe('Index of an input or output slot on a node')
|
||||
|
||||
// TODO: Investigate usage of array and number as data type usage in custom nodes.
|
||||
// Known usage:
|
||||
// - https://github.com/rgthree/rgthree-comfy Context Big node is using array as type.
|
||||
export const zDataType = z.union([z.string(), z.array(z.string()), z.number()])
|
||||
/**
|
||||
* Data type for node inputs/outputs. Can be string, array of strings, or number.
|
||||
* Most common types are strings like 'IMAGE', 'LATENT', 'MODEL', etc.
|
||||
*/
|
||||
export const zDataType = z
|
||||
.union([z.string(), z.array(z.string()), z.number()])
|
||||
.describe('Data type specification for node connections')
|
||||
|
||||
const zVector2 = z.union([
|
||||
z
|
||||
.object({ 0: z.number(), 1: z.number() })
|
||||
.passthrough()
|
||||
.transform((v) => [v[0], v[1]] as [number, number]),
|
||||
z.tuple([z.number(), z.number()])
|
||||
])
|
||||
/**
|
||||
* 2D position or size vector [x, y].
|
||||
* Can be array tuple or object with numeric indices.
|
||||
*/
|
||||
const zVector2 = z
|
||||
.union([
|
||||
z
|
||||
.object({ 0: z.number(), 1: z.number() })
|
||||
.passthrough()
|
||||
.transform((v) => [v[0], v[1]] as [number, number]),
|
||||
z.tuple([z.number(), z.number()])
|
||||
])
|
||||
.describe('2D coordinate or size vector')
|
||||
|
||||
// Definition of an AI model file used in the workflow.
|
||||
const zModelFile = z.object({
|
||||
name: z.string(),
|
||||
url: z.string().url(),
|
||||
hash: z.string().optional(),
|
||||
hash_type: z.string().optional(),
|
||||
directory: z.string()
|
||||
})
|
||||
/**
|
||||
* AI model file definition used in the workflow.
|
||||
* Contains metadata for downloading and verifying model files.
|
||||
*/
|
||||
const zModelFile = z
|
||||
.object({
|
||||
/** Model file name */
|
||||
name: z.string().describe('Model file name'),
|
||||
/** Download URL for the model */
|
||||
url: z.string().url().describe('Download URL for the model'),
|
||||
/** File hash for integrity verification */
|
||||
hash: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('File hash for integrity verification'),
|
||||
/** Hash algorithm type (e.g., 'sha256') */
|
||||
hash_type: z.string().optional().describe('Hash algorithm type'),
|
||||
/** Directory where model should be stored */
|
||||
directory: z.string().describe('Directory where model should be stored')
|
||||
})
|
||||
.describe('AI model file metadata')
|
||||
|
||||
/**
|
||||
* Graph state tracking for ID generation in schema version 1.
|
||||
* Maintains counters for generating unique IDs for new elements.
|
||||
*/
|
||||
const zGraphState = z
|
||||
.object({
|
||||
lastGroupId: z.number(),
|
||||
lastNodeId: z.number(),
|
||||
lastLinkId: z.number(),
|
||||
lastRerouteId: z.number()
|
||||
/** Last assigned group ID */
|
||||
lastGroupId: z.number().describe('Last assigned group ID'),
|
||||
/** Last assigned node ID */
|
||||
lastNodeId: z.number().describe('Last assigned node ID'),
|
||||
/** Last assigned link ID */
|
||||
lastLinkId: z.number().describe('Last assigned link ID'),
|
||||
/** Last assigned reroute ID */
|
||||
lastRerouteId: z.number().describe('Last assigned reroute ID')
|
||||
})
|
||||
.passthrough()
|
||||
.describe('Graph state tracking for ID generation')
|
||||
|
||||
const zComfyLink = z.tuple([
|
||||
z.number(), // Link id
|
||||
@@ -287,24 +339,52 @@ export const zBaseExportableGraph = z.object({
|
||||
subgraphs: z.array(zSubgraphInstance).optional()
|
||||
})
|
||||
|
||||
/** Schema version 0.4 */
|
||||
/**
|
||||
* ComfyUI Workflow JSON Schema version 0.4 (legacy).
|
||||
* This is the original workflow format used by ComfyUI.
|
||||
*/
|
||||
export const zComfyWorkflow = zBaseExportableGraph
|
||||
.extend({
|
||||
id: z.string().uuid().optional(),
|
||||
revision: z.number().optional(),
|
||||
last_node_id: zNodeId,
|
||||
last_link_id: z.number(),
|
||||
nodes: z.array(zComfyNode),
|
||||
links: z.array(zComfyLink),
|
||||
floatingLinks: z.array(zComfyLinkObject).optional(),
|
||||
groups: z.array(zGroup).optional(),
|
||||
config: zConfig.optional().nullable(),
|
||||
extra: zExtra.optional().nullable(),
|
||||
version: z.number(),
|
||||
models: z.array(zModelFile).optional(),
|
||||
definitions: zGraphDefinitions.optional()
|
||||
/** Unique workflow identifier */
|
||||
id: z.string().uuid().optional().describe('Unique workflow identifier'),
|
||||
/** Workflow revision number */
|
||||
revision: z.number().optional().describe('Workflow revision number'),
|
||||
/** Highest node ID used in this workflow */
|
||||
last_node_id: zNodeId.describe('Highest node ID used in this workflow'),
|
||||
/** Highest link ID used in this workflow */
|
||||
last_link_id: z.number().describe('Highest link ID used in this workflow'),
|
||||
/** All nodes in the workflow */
|
||||
nodes: z.array(zComfyNode).describe('All nodes in the workflow'),
|
||||
/** Node connections (legacy tuple format) */
|
||||
links: z
|
||||
.array(zComfyLink)
|
||||
.describe('Node connections in legacy tuple format'),
|
||||
/** Floating links (unconnected endpoints) */
|
||||
floatingLinks: z
|
||||
.array(zComfyLinkObject)
|
||||
.optional()
|
||||
.describe('Floating links with unconnected endpoints'),
|
||||
/** Visual groupings of nodes */
|
||||
groups: z.array(zGroup).optional().describe('Visual groupings of nodes'),
|
||||
/** Workflow configuration settings */
|
||||
config: zConfig
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe('Workflow configuration settings'),
|
||||
/** Extra metadata and extensions */
|
||||
extra: zExtra
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe('Extra metadata and extensions'),
|
||||
/** Schema version number */
|
||||
version: z.number().describe('Schema version number (0.4)'),
|
||||
/** Required model files */
|
||||
models: z.array(zModelFile).optional().describe('Required AI model files'),
|
||||
/** Subgraph definitions */
|
||||
definitions: zGraphDefinitions.optional().describe('Subgraph definitions')
|
||||
})
|
||||
.passthrough()
|
||||
.describe('ComfyUI Workflow JSON Schema v0.4')
|
||||
|
||||
/** Required for recursive definition of subgraphs. */
|
||||
interface ComfyWorkflow1BaseType {
|
||||
@@ -339,37 +419,76 @@ interface ComfyWorkflow1BaseOutput extends ComfyWorkflow1BaseType {
|
||||
}
|
||||
}
|
||||
|
||||
/** Schema version 1 */
|
||||
/**
|
||||
* ComfyUI Workflow JSON Schema version 1 (current).
|
||||
* This is the modern workflow format with improved structure and features.
|
||||
*/
|
||||
export const zComfyWorkflow1 = zBaseExportableGraph
|
||||
.extend({
|
||||
id: z.string().uuid().optional(),
|
||||
revision: z.number().optional(),
|
||||
version: z.literal(1),
|
||||
config: zConfig.optional().nullable(),
|
||||
state: zGraphState,
|
||||
groups: z.array(zGroup).optional(),
|
||||
nodes: z.array(zComfyNode),
|
||||
links: z.array(zComfyLinkObject).optional(),
|
||||
floatingLinks: z.array(zComfyLinkObject).optional(),
|
||||
reroutes: z.array(zReroute).optional(),
|
||||
extra: zExtra.optional().nullable(),
|
||||
models: z.array(zModelFile).optional(),
|
||||
/** Unique workflow identifier */
|
||||
id: z.string().uuid().optional().describe('Unique workflow identifier'),
|
||||
/** Workflow revision number for tracking changes */
|
||||
revision: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Workflow revision number for tracking changes'),
|
||||
/** Schema version (always 1 for this format) */
|
||||
version: z.literal(1).describe('Schema version number (1)'),
|
||||
/** Workflow configuration settings */
|
||||
config: zConfig
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe('Workflow configuration settings'),
|
||||
/** Graph state for ID tracking and generation */
|
||||
state: zGraphState.describe('Graph state for ID tracking and generation'),
|
||||
/** Visual groupings of nodes */
|
||||
groups: z.array(zGroup).optional().describe('Visual groupings of nodes'),
|
||||
/** All nodes in the workflow */
|
||||
nodes: z.array(zComfyNode).describe('All nodes in the workflow'),
|
||||
/** Node connections (modern object format) */
|
||||
links: z
|
||||
.array(zComfyLinkObject)
|
||||
.optional()
|
||||
.describe('Node connections in modern object format'),
|
||||
/** Floating links (unconnected endpoints) */
|
||||
floatingLinks: z
|
||||
.array(zComfyLinkObject)
|
||||
.optional()
|
||||
.describe('Floating links with unconnected endpoints'),
|
||||
/** Reroute nodes for organizing connections */
|
||||
reroutes: z
|
||||
.array(zReroute)
|
||||
.optional()
|
||||
.describe('Reroute nodes for organizing connections'),
|
||||
/** Extra metadata and extensions */
|
||||
extra: zExtra
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe('Extra metadata and extensions'),
|
||||
/** Required AI model files */
|
||||
models: z.array(zModelFile).optional().describe('Required AI model files'),
|
||||
/** Subgraph definitions */
|
||||
definitions: z
|
||||
.object({
|
||||
subgraphs: z.lazy(
|
||||
(): z.ZodArray<
|
||||
z.ZodType<
|
||||
SubgraphDefinitionBase<ComfyWorkflow1BaseOutput>,
|
||||
z.ZodTypeDef,
|
||||
SubgraphDefinitionBase<ComfyWorkflow1BaseInput>
|
||||
>,
|
||||
'many'
|
||||
> => z.array(zSubgraphDefinition)
|
||||
)
|
||||
/** Nested subgraph definitions */
|
||||
subgraphs: z
|
||||
.lazy(
|
||||
(): z.ZodArray<
|
||||
z.ZodType<
|
||||
SubgraphDefinitionBase<ComfyWorkflow1BaseOutput>,
|
||||
z.ZodTypeDef,
|
||||
SubgraphDefinitionBase<ComfyWorkflow1BaseInput>
|
||||
>,
|
||||
'many'
|
||||
> => z.array(zSubgraphDefinition)
|
||||
)
|
||||
.describe('Nested subgraph definitions')
|
||||
})
|
||||
.optional()
|
||||
.describe('Subgraph definitions')
|
||||
})
|
||||
.passthrough()
|
||||
.describe('ComfyUI Workflow JSON Schema v1')
|
||||
|
||||
export const zExportedSubgraphIONode = z.object({
|
||||
id: zNodeId,
|
||||
@@ -481,6 +600,14 @@ const zWorkflowVersion = z.object({
|
||||
version: z.number()
|
||||
})
|
||||
|
||||
/**
|
||||
* Validates a ComfyUI workflow JSON against the appropriate schema version.
|
||||
* Supports both legacy (v0.4) and modern (v1) workflow formats.
|
||||
*
|
||||
* @param data - The workflow data to validate
|
||||
* @param onError - Error callback function for validation failures
|
||||
* @returns Parsed and validated workflow data or null if invalid
|
||||
*/
|
||||
export async function validateComfyWorkflow(
|
||||
data: unknown,
|
||||
onError: (error: string) => void = console.warn
|
||||
@@ -489,17 +616,18 @@ export async function validateComfyWorkflow(
|
||||
|
||||
let result: SafeParseReturnType<unknown, ComfyWorkflowJSON>
|
||||
if (!versionResult.success) {
|
||||
// Invalid workflow
|
||||
// Invalid workflow - missing or invalid version
|
||||
const error = fromZodError(versionResult.error)
|
||||
onError(`Workflow does not contain a valid version. Zod error:\n${error}`)
|
||||
return null
|
||||
} else if (versionResult.data.version === 1) {
|
||||
// Schema version 1
|
||||
// Modern schema version 1 (current)
|
||||
result = await zComfyWorkflow1.safeParseAsync(data)
|
||||
} else {
|
||||
// Unknown or old version: 0.4
|
||||
// Legacy or unknown version: defaults to 0.4 format
|
||||
result = await zComfyWorkflow.safeParseAsync(data)
|
||||
}
|
||||
|
||||
if (result.success) return result.data
|
||||
|
||||
const error = fromZodError(result.error)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import axios from 'axios'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
import defaultClientFeatureFlags from '@/config/clientFeatureFlags.json'
|
||||
import type {
|
||||
@@ -14,11 +13,9 @@ import type {
|
||||
ExecutionSuccessWsMessage,
|
||||
ExtensionsResponse,
|
||||
FeatureFlagsWsMessage,
|
||||
HistoryResponse,
|
||||
HistoryTaskItem,
|
||||
LogsRawResponse,
|
||||
LogsWsMessage,
|
||||
NotificationWsMessage,
|
||||
PendingTaskItem,
|
||||
ProgressStateWsMessage,
|
||||
ProgressTextWsMessage,
|
||||
@@ -29,7 +26,6 @@ import type {
|
||||
StatusWsMessage,
|
||||
StatusWsMessageStatus,
|
||||
SystemStats,
|
||||
TaskPrompt,
|
||||
User,
|
||||
UserDataFullInfo
|
||||
} from '@/schemas/apiSchema'
|
||||
@@ -39,8 +35,6 @@ import type {
|
||||
NodeId
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import { WorkflowTemplates } from '@/types/workflowTemplateTypes'
|
||||
|
||||
@@ -136,7 +130,6 @@ interface BackendApiCalls {
|
||||
progress_state: ProgressStateWsMessage
|
||||
display_component: DisplayComponentWsMessage
|
||||
feature_flags: FeatureFlagsWsMessage
|
||||
notification: NotificationWsMessage
|
||||
}
|
||||
|
||||
/** Dictionary of all api calls */
|
||||
@@ -279,81 +272,6 @@ export class ComfyApi extends EventTarget {
|
||||
*/
|
||||
serverFeatureFlags: Record<string, unknown> = {}
|
||||
|
||||
/**
|
||||
* Map of notification toasts by ID
|
||||
*/
|
||||
#notificationToasts = new Map<string, any>()
|
||||
|
||||
/**
|
||||
* Map of timers for auto-hiding notifications by ID
|
||||
*/
|
||||
#notificationTimers = new Map<string, number>()
|
||||
|
||||
/**
|
||||
* Handle notification messages (with optional ID for multiple parallel notifications)
|
||||
*/
|
||||
#handleNotification(value: string, id?: string) {
|
||||
try {
|
||||
const toastStore = useToastStore()
|
||||
const notificationId = id || 'default'
|
||||
|
||||
console.log(`Updating notification (${notificationId}):`, value)
|
||||
|
||||
// Get existing toast for this ID
|
||||
const existingToast = this.#notificationToasts.get(notificationId)
|
||||
|
||||
if (existingToast) {
|
||||
// Update existing toast by removing and re-adding with new content
|
||||
console.log(`Updating existing notification toast: ${notificationId}`)
|
||||
toastStore.remove(existingToast)
|
||||
|
||||
// Update the detail text
|
||||
existingToast.detail = value
|
||||
toastStore.add(existingToast)
|
||||
} else {
|
||||
// Create new persistent notification toast
|
||||
console.log(`Creating new notification toast: ${notificationId}`)
|
||||
const newToast = {
|
||||
severity: 'info' as const,
|
||||
summary: 'Notification',
|
||||
detail: value,
|
||||
closable: true
|
||||
// No 'life' property means it won't auto-hide
|
||||
}
|
||||
this.#notificationToasts.set(notificationId, newToast)
|
||||
toastStore.add(newToast)
|
||||
}
|
||||
|
||||
// Clear existing timer for this ID and set new one
|
||||
const existingTimer = this.#notificationTimers.get(notificationId)
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer)
|
||||
}
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
const toast = this.#notificationToasts.get(notificationId)
|
||||
if (toast) {
|
||||
console.log(`Auto-hiding notification toast: ${notificationId}`)
|
||||
toastStore.remove(toast)
|
||||
this.#notificationToasts.delete(notificationId)
|
||||
this.#notificationTimers.delete(notificationId)
|
||||
}
|
||||
}, 3000)
|
||||
|
||||
this.#notificationTimers.set(notificationId, timer)
|
||||
console.log('Toast updated successfully')
|
||||
} catch (error) {
|
||||
console.error('Error handling notification:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounced notification handler to avoid rapid toast updates
|
||||
*/
|
||||
#debouncedNotificationHandler = debounce((value: string, id?: string) => {
|
||||
this.#handleNotification(value, id)
|
||||
}, 300) // 300ms debounce delay
|
||||
|
||||
/**
|
||||
* The auth token for the comfy org account if the user is logged in.
|
||||
* This is only used for {@link queuePrompt} now. It is not directly
|
||||
@@ -393,27 +311,7 @@ export class ComfyApi extends EventTarget {
|
||||
return this.api_base + route
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for Firebase auth to be initialized before proceeding
|
||||
*/
|
||||
async #waitForAuthInitialization(): Promise<void> {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
if (authStore.isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
const unwatch = authStore.$subscribe((_, state) => {
|
||||
if (state.isInitialized) {
|
||||
unwatch()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async fetchApi(route: string, options?: RequestInit) {
|
||||
fetchApi(route: string, options?: RequestInit) {
|
||||
if (!options) {
|
||||
options = {}
|
||||
}
|
||||
@@ -424,30 +322,6 @@ export class ComfyApi extends EventTarget {
|
||||
options.cache = 'no-cache'
|
||||
}
|
||||
|
||||
// Wait for Firebase auth to be initialized before making any API request
|
||||
await this.#waitForAuthInitialization()
|
||||
|
||||
// Add Firebase JWT token if user is logged in
|
||||
try {
|
||||
const authHeader = await useFirebaseAuthStore().getAuthHeader()
|
||||
if (authHeader) {
|
||||
if (Array.isArray(options.headers)) {
|
||||
for (const [key, value] of Object.entries(authHeader)) {
|
||||
options.headers.push([key, value])
|
||||
}
|
||||
} else if (options.headers instanceof Headers) {
|
||||
for (const [key, value] of Object.entries(authHeader)) {
|
||||
options.headers.set(key, value)
|
||||
}
|
||||
} else {
|
||||
Object.assign(options.headers, authHeader)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore auth errors to avoid breaking API calls
|
||||
console.warn('Failed to get auth header:', error)
|
||||
}
|
||||
|
||||
if (Array.isArray(options.headers)) {
|
||||
options.headers.push(['Comfy-User', this.user])
|
||||
} else if (options.headers instanceof Headers) {
|
||||
@@ -677,16 +551,6 @@ export class ComfyApi extends EventTarget {
|
||||
this.serverFeatureFlags
|
||||
)
|
||||
break
|
||||
case 'notification':
|
||||
// Display notification in toast with debouncing
|
||||
console.log(
|
||||
'Received notification message:',
|
||||
msg.data.value,
|
||||
msg.data.id ? `(ID: ${msg.data.id})` : ''
|
||||
)
|
||||
this.#debouncedNotificationHandler(msg.data.value, msg.data.id)
|
||||
this.dispatchCustomEvent(msg.type, msg.data)
|
||||
break
|
||||
default:
|
||||
if (this.#registered.has(msg.type)) {
|
||||
// Fallback for custom types - calls super direct.
|
||||
@@ -712,35 +576,6 @@ export class ComfyApi extends EventTarget {
|
||||
this.#createSocket()
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method to simulate a notification message (for development/testing)
|
||||
*/
|
||||
testNotification(message: string = 'Test notification message', id?: string) {
|
||||
console.log(
|
||||
'Testing notification with message:',
|
||||
message,
|
||||
id ? `(ID: ${id})` : ''
|
||||
)
|
||||
const mockEvent = {
|
||||
data: JSON.stringify({
|
||||
type: 'notification',
|
||||
data: { value: message, id }
|
||||
})
|
||||
}
|
||||
|
||||
// Simulate the websocket message handler
|
||||
const msg = JSON.parse(mockEvent.data)
|
||||
if (msg.type === 'notification') {
|
||||
console.log(
|
||||
'Received notification message:',
|
||||
msg.data.value,
|
||||
msg.data.id ? `(ID: ${msg.data.id})` : ''
|
||||
)
|
||||
this.#debouncedNotificationHandler(msg.data.value, msg.data.id)
|
||||
this.dispatchCustomEvent(msg.type, msg.data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of extension urls
|
||||
*/
|
||||
@@ -904,28 +739,6 @@ export class ComfyApi extends EventTarget {
|
||||
return this.getHistory()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses queue prompt data from array or object format
|
||||
* @param rawPrompt The raw prompt data from the API
|
||||
* @returns Normalized TaskPrompt object
|
||||
*/
|
||||
private parseQueuePrompt(rawPrompt: any): TaskPrompt {
|
||||
if (Array.isArray(rawPrompt)) {
|
||||
// Queue format: [priority, prompt_id, workflow, outputs]
|
||||
const [priority, prompt_id, workflow] = rawPrompt
|
||||
return {
|
||||
priority,
|
||||
prompt_id,
|
||||
extra_data: workflow?.extra_data || {
|
||||
client_id: '',
|
||||
extra_pnginfo: workflow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rawPrompt as TaskPrompt
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current state of the queue
|
||||
* @returns The currently running and queued items
|
||||
@@ -939,17 +752,15 @@ export class ComfyApi extends EventTarget {
|
||||
const data = await res.json()
|
||||
return {
|
||||
// Running action uses a different endpoint for cancelling
|
||||
Running: data.queue_running.map((prompt: any) => ({
|
||||
Running: data.queue_running.map((prompt: Record<number, any>) => ({
|
||||
taskType: 'Running',
|
||||
prompt: this.parseQueuePrompt(prompt),
|
||||
remove: {
|
||||
name: 'Cancel',
|
||||
cb: () => api.interrupt(this.parseQueuePrompt(prompt).prompt_id)
|
||||
}
|
||||
prompt,
|
||||
// prompt[1] is the prompt id
|
||||
remove: { name: 'Cancel', cb: () => api.interrupt(prompt[1]) }
|
||||
})),
|
||||
Pending: data.queue_pending.map((prompt: any) => ({
|
||||
Pending: data.queue_pending.map((prompt: Record<number, any>) => ({
|
||||
taskType: 'Pending',
|
||||
prompt: this.parseQueuePrompt(prompt)
|
||||
prompt
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -966,17 +777,13 @@ export class ComfyApi extends EventTarget {
|
||||
max_items: number = 200
|
||||
): Promise<{ History: HistoryTaskItem[] }> {
|
||||
try {
|
||||
const res = await this.fetchApi(`/history_v2?max_items=${max_items}`)
|
||||
const json: HistoryResponse = await res.json()
|
||||
|
||||
// Extract history data from new format: { history: [{prompt_id: "...", ...}, ...] }
|
||||
const res = await this.fetchApi(`/history?max_items=${max_items}`)
|
||||
const json: Promise<HistoryTaskItem[]> = await res.json()
|
||||
return {
|
||||
History: json.history.map(
|
||||
(item): HistoryTaskItem => ({
|
||||
...item,
|
||||
taskType: 'History'
|
||||
})
|
||||
)
|
||||
History: Object.values(json).map((item) => ({
|
||||
...item,
|
||||
taskType: 'History'
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -984,33 +791,6 @@ export class ComfyApi extends EventTarget {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets workflow data for a specific prompt from history
|
||||
* @param prompt_id The prompt ID to fetch workflow for
|
||||
* @returns Workflow data for the specific prompt
|
||||
*/
|
||||
async getWorkflowFromHistory(
|
||||
prompt_id: string
|
||||
): Promise<ComfyWorkflowJSON | null> {
|
||||
try {
|
||||
const res = await this.fetchApi(`/history_v2/${prompt_id}`)
|
||||
const json = await res.json()
|
||||
|
||||
// The /history_v2/{prompt_id} endpoint returns data for a specific prompt
|
||||
// The response format is: { prompt_id: { prompt: {priority, prompt_id, extra_data}, outputs: {...}, status: {...} } }
|
||||
const historyItem = json[prompt_id]
|
||||
if (!historyItem) return null
|
||||
|
||||
// Extract workflow from the prompt object
|
||||
// prompt.extra_data contains extra_pnginfo.workflow
|
||||
const workflow = historyItem.prompt?.extra_data?.extra_pnginfo?.workflow
|
||||
return workflow || null
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch workflow for prompt ${prompt_id}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets system & device stats
|
||||
* @returns System stats such as python version, OS, per device info
|
||||
|
||||
@@ -323,14 +323,6 @@ export class ComfyApp {
|
||||
return '&rand=' + Math.random()
|
||||
}
|
||||
|
||||
getClientIdParam() {
|
||||
const clientId = window.name
|
||||
if (clientId) {
|
||||
return '&client_id=' + clientId
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
static onClipspaceEditorSave() {
|
||||
if (ComfyApp.clipspace_return_node) {
|
||||
ComfyApp.pasteFromClipspace(ComfyApp.clipspace_return_node)
|
||||
|
||||
@@ -264,21 +264,15 @@ class ComfyList {
|
||||
? item.remove
|
||||
: {
|
||||
name: 'Delete',
|
||||
cb: () =>
|
||||
api.deleteItem(
|
||||
this.#type,
|
||||
Array.isArray(item.prompt)
|
||||
? item.prompt[1]
|
||||
: item.prompt.prompt_id
|
||||
)
|
||||
cb: () => api.deleteItem(this.#type, item.prompt[1])
|
||||
}
|
||||
return $el('div', { textContent: item.prompt.priority + ': ' }, [
|
||||
return $el('div', { textContent: item.prompt[0] + ': ' }, [
|
||||
$el('button', {
|
||||
textContent: 'Load',
|
||||
onclick: async () => {
|
||||
await app.loadGraphData(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
item.prompt.extra_data.extra_pnginfo.workflow,
|
||||
item.prompt[3].extra_pnginfo.workflow,
|
||||
true,
|
||||
false
|
||||
)
|
||||
|
||||
34
src/scripts/ui/spinner.css
Normal file
34
src/scripts/ui/spinner.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.lds-ring {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
.lds-ring div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0.15em solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
}
|
||||
.lds-ring div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
.lds-ring div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.lds-ring div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
@keyframes lds-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
7
src/scripts/ui/spinner.ts
Normal file
7
src/scripts/ui/spinner.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import './spinner.css'
|
||||
|
||||
export function createSpinner() {
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`
|
||||
return div.firstElementChild
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
KeyComboImpl,
|
||||
KeybindingImpl,
|
||||
@@ -12,7 +11,6 @@ export const useKeybindingService = () => {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const settingStore = useSettingStore()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const keybindHandler = async function (event: KeyboardEvent) {
|
||||
const keyCombo = KeyComboImpl.fromEvent(event)
|
||||
@@ -34,19 +32,6 @@ export const useKeybindingService = () => {
|
||||
|
||||
const keybinding = keybindingStore.getKeybinding(keyCombo)
|
||||
if (keybinding && keybinding.targetElementId !== 'graph-canvas') {
|
||||
// Special handling for Escape key - let dialogs handle it first
|
||||
if (
|
||||
event.key === 'Escape' &&
|
||||
!event.ctrlKey &&
|
||||
!event.altKey &&
|
||||
!event.metaKey
|
||||
) {
|
||||
// If dialogs are open, don't execute the keybinding - let the dialog handle it
|
||||
if (dialogStore.dialogStack.length > 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent default browser behavior first, then execute the command
|
||||
event.preventDefault()
|
||||
await commandStore.execute(keybinding.commandId)
|
||||
|
||||
@@ -5,12 +5,10 @@ import { t } from '@/i18n'
|
||||
import { LGraph, LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import type { SerialisableGraph, Vector2 } from '@/lib/litegraph/src/litegraph'
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
|
||||
import { downloadBlob } from '@/scripts/utils'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { TaskItemImpl } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||
@@ -156,32 +154,6 @@ export const useWorkflowService = () => {
|
||||
await app.loadGraphData(blankGraph)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a workflow from a task item (queue/history)
|
||||
* For history items, fetches workflow data from /history_v2/{prompt_id}
|
||||
* @param task The task item to load the workflow from
|
||||
*/
|
||||
const loadTaskWorkflow = async (task: TaskItemImpl) => {
|
||||
let workflowData = task.workflow
|
||||
|
||||
// History items don't include workflow data - fetch from API
|
||||
if (task.isHistory) {
|
||||
const promptId = task.prompt.prompt_id
|
||||
if (promptId) {
|
||||
workflowData = (await api.getWorkflowFromHistory(promptId)) || undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (!workflowData) {
|
||||
return
|
||||
}
|
||||
|
||||
await app.loadGraphData(toRaw(workflowData))
|
||||
if (task.outputs) {
|
||||
app.nodeOutputs = toRaw(task.outputs)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the current workflow
|
||||
* This is used to refresh the node definitions update, e.g. when the locale changes.
|
||||
@@ -430,7 +402,6 @@ export const useWorkflowService = () => {
|
||||
saveWorkflow,
|
||||
loadDefaultWorkflow,
|
||||
loadBlankWorkflow,
|
||||
loadTaskWorkflow,
|
||||
reloadCurrentWorkflow,
|
||||
openWorkflow,
|
||||
closeWorkflow,
|
||||
|
||||
@@ -83,16 +83,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
currentUser.value = user
|
||||
isInitialized.value = true
|
||||
|
||||
if (user && (window as any).mixpanel) {
|
||||
;(window as any).mixpanel
|
||||
.identify(user.uid)(window as any)
|
||||
.mixpanel.people.set({
|
||||
$email: user.email,
|
||||
$name: user.displayName,
|
||||
$created: user.metadata.creationTime
|
||||
})
|
||||
}
|
||||
|
||||
// Reset balance when auth state changes
|
||||
balance.value = null
|
||||
lastBalanceUpdateTime.value = null
|
||||
|
||||
@@ -90,13 +90,10 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
|
||||
const rand = app.getRandParam()
|
||||
const previewParam = getPreviewParam(node, outputs)
|
||||
const clientIdParam = app.getClientIdParam()
|
||||
|
||||
return outputs.images.map((image) => {
|
||||
const imgUrlPart = new URLSearchParams(image)
|
||||
return api.apiURL(
|
||||
`/view?${imgUrlPart}${previewParam}${rand}${clientIdParam}`
|
||||
)
|
||||
return api.apiURL(`/view?${imgUrlPart}${previewParam}${rand}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -59,11 +59,6 @@ export class ResultItemImpl {
|
||||
params.set('type', this.type)
|
||||
params.set('subfolder', this.subfolder)
|
||||
|
||||
const clientId = window.name
|
||||
if (clientId) {
|
||||
params.set('client_id', clientId)
|
||||
}
|
||||
|
||||
if (this.format) {
|
||||
params.set('format', this.format)
|
||||
}
|
||||
@@ -276,15 +271,23 @@ export class TaskItemImpl {
|
||||
}
|
||||
|
||||
get queueIndex() {
|
||||
return this.prompt.priority
|
||||
return this.prompt[0]
|
||||
}
|
||||
|
||||
get promptId() {
|
||||
return this.prompt.prompt_id
|
||||
return this.prompt[1]
|
||||
}
|
||||
|
||||
get promptInputs() {
|
||||
return this.prompt[2]
|
||||
}
|
||||
|
||||
get extraData() {
|
||||
return this.prompt.extra_data
|
||||
return this.prompt[3]
|
||||
}
|
||||
|
||||
get outputsToExecute() {
|
||||
return this.prompt[4]
|
||||
}
|
||||
|
||||
get extraPngInfo() {
|
||||
@@ -400,11 +403,13 @@ export class TaskItemImpl {
|
||||
(output: ResultItemImpl, i: number) =>
|
||||
new TaskItemImpl(
|
||||
this.taskType,
|
||||
{
|
||||
priority: this.queueIndex,
|
||||
prompt_id: `${this.promptId}-${i}`,
|
||||
extra_data: this.extraData
|
||||
},
|
||||
[
|
||||
this.queueIndex,
|
||||
`${this.promptId}-${i}`,
|
||||
this.promptInputs,
|
||||
this.extraData,
|
||||
this.outputsToExecute
|
||||
],
|
||||
this.status,
|
||||
{
|
||||
[output.nodeId]: {
|
||||
@@ -469,11 +474,11 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
pendingTasks.value = toClassAll(queue.Pending)
|
||||
|
||||
const allIndex = new Set<number>(
|
||||
history.History.map((item: TaskItem) => item.prompt.priority)
|
||||
history.History.map((item: TaskItem) => item.prompt[0])
|
||||
)
|
||||
const newHistoryItems = toClassAll(
|
||||
history.History.filter(
|
||||
(item) => item.prompt.priority > lastHistoryQueueIndex.value
|
||||
(item) => item.prompt[0] > lastHistoryQueueIndex.value
|
||||
)
|
||||
)
|
||||
const existingHistoryItems = historyTasks.value.filter((item) =>
|
||||
@@ -481,11 +486,7 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
)
|
||||
historyTasks.value = [...newHistoryItems, ...existingHistoryItems]
|
||||
.slice(0, maxHistoryItems.value)
|
||||
.sort((a, b) => {
|
||||
const aTime = a.executionStartTimestamp ?? 0
|
||||
const bTime = b.executionStartTimestamp ?? 0
|
||||
return bTime - aTime
|
||||
})
|
||||
.sort((a, b) => b.queueIndex - a.queueIndex)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
24
src/types/apiNodeTypes.ts
Normal file
24
src/types/apiNodeTypes.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface ApiNodeCost {
|
||||
name: string
|
||||
cost: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about an API node's cost and pricing details
|
||||
*/
|
||||
export interface ApiNodeCostData {
|
||||
/** The vendor/company providing the API service (e.g., 'OpenAI', 'Stability') */
|
||||
vendor: string
|
||||
/** The human-readable name of the node as displayed in the UI */
|
||||
nodeName: string
|
||||
/** Parameters that affect pricing (e.g., 'size | quality', 'duration', '-' if none) */
|
||||
pricingParams: string
|
||||
/** The price range per run (e.g., '$0.05', '$0.04 x n', 'dynamic') */
|
||||
pricePerRunRange: string
|
||||
/** Formatted price string for display in the UI */
|
||||
displayPrice: string
|
||||
/** URL to the vendor's pricing documentation page */
|
||||
rateDocumentationUrl?: string
|
||||
}
|
||||
|
||||
export type ApiNodeCostRecord = Record<string, ApiNodeCostData>
|
||||
@@ -189,10 +189,6 @@ const onStatus = async (e: CustomEvent<StatusWsMessageStatus>) => {
|
||||
await queueStore.update()
|
||||
}
|
||||
|
||||
const onExecutionSuccess = async () => {
|
||||
await queueStore.update()
|
||||
}
|
||||
|
||||
const reconnectingMessage: ToastMessageOptions = {
|
||||
severity: 'error',
|
||||
summary: t('g.reconnecting')
|
||||
@@ -218,7 +214,6 @@ const onReconnected = () => {
|
||||
|
||||
onMounted(() => {
|
||||
api.addEventListener('status', onStatus)
|
||||
api.addEventListener('execution_success', onExecutionSuccess)
|
||||
api.addEventListener('reconnecting', onReconnecting)
|
||||
api.addEventListener('reconnected', onReconnected)
|
||||
executionStore.bindExecutionEvents()
|
||||
@@ -232,7 +227,6 @@ onMounted(() => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
api.removeEventListener('status', onStatus)
|
||||
api.removeEventListener('execution_success', onExecutionSuccess)
|
||||
api.removeEventListener('reconnecting', onReconnecting)
|
||||
api.removeEventListener('reconnected', onReconnected)
|
||||
executionStore.unbindExecutionEvents()
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type {
|
||||
HistoryResponse,
|
||||
RawHistoryItem
|
||||
} from '../../../src/schemas/apiSchema'
|
||||
import type { ComfyWorkflowJSON } from '../../../src/schemas/comfyWorkflowSchema'
|
||||
import { ComfyApi } from '../../../src/scripts/api'
|
||||
|
||||
describe('ComfyApi getHistory', () => {
|
||||
let api: ComfyApi
|
||||
|
||||
beforeEach(() => {
|
||||
api = new ComfyApi()
|
||||
})
|
||||
|
||||
const mockHistoryItem: RawHistoryItem = {
|
||||
prompt_id: 'test_prompt_id',
|
||||
prompt: {
|
||||
priority: 0,
|
||||
prompt_id: 'test_prompt_id',
|
||||
extra_data: {
|
||||
extra_pnginfo: {
|
||||
workflow: {
|
||||
last_node_id: 1,
|
||||
last_link_id: 0,
|
||||
nodes: [],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
version: 0.4
|
||||
}
|
||||
},
|
||||
client_id: 'test_client_id'
|
||||
}
|
||||
},
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
|
||||
describe('history v2 API format', () => {
|
||||
it('should handle history array format from /history_v2', async () => {
|
||||
const historyResponse: HistoryResponse = {
|
||||
history: [
|
||||
{ ...mockHistoryItem, prompt_id: 'prompt_id_1' },
|
||||
{ ...mockHistoryItem, prompt_id: 'prompt_id_2' }
|
||||
]
|
||||
}
|
||||
|
||||
// Mock fetchApi to return the v2 format
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue(historyResponse)
|
||||
})
|
||||
api.fetchApi = mockFetchApi
|
||||
|
||||
const result = await api.getHistory(10)
|
||||
|
||||
expect(result.History).toHaveLength(2)
|
||||
expect(result.History[0]).toEqual({
|
||||
...mockHistoryItem,
|
||||
prompt_id: 'prompt_id_1',
|
||||
taskType: 'History'
|
||||
})
|
||||
expect(result.History[1]).toEqual({
|
||||
...mockHistoryItem,
|
||||
prompt_id: 'prompt_id_2',
|
||||
taskType: 'History'
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty history array', async () => {
|
||||
const historyResponse: HistoryResponse = {
|
||||
history: []
|
||||
}
|
||||
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue(historyResponse)
|
||||
})
|
||||
api.fetchApi = mockFetchApi
|
||||
|
||||
const result = await api.getHistory(10)
|
||||
|
||||
expect(result.History).toHaveLength(0)
|
||||
expect(result.History).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should return empty history on error', async () => {
|
||||
const mockFetchApi = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
api.fetchApi = mockFetchApi
|
||||
|
||||
const result = await api.getHistory()
|
||||
|
||||
expect(result.History).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('API call parameters', () => {
|
||||
it('should call fetchApi with correct v2 endpoint and parameters', async () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue({ history: [] })
|
||||
})
|
||||
api.fetchApi = mockFetchApi
|
||||
|
||||
await api.getHistory(50)
|
||||
|
||||
expect(mockFetchApi).toHaveBeenCalledWith('/history_v2?max_items=50')
|
||||
})
|
||||
|
||||
it('should use default max_items parameter with v2 endpoint', async () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue({ history: [] })
|
||||
})
|
||||
api.fetchApi = mockFetchApi
|
||||
|
||||
await api.getHistory()
|
||||
|
||||
expect(mockFetchApi).toHaveBeenCalledWith('/history_v2?max_items=200')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ComfyApi getWorkflowFromHistory', () => {
|
||||
let api: ComfyApi
|
||||
|
||||
beforeEach(() => {
|
||||
api = new ComfyApi()
|
||||
})
|
||||
|
||||
const mockWorkflow: ComfyWorkflowJSON = {
|
||||
last_node_id: 1,
|
||||
last_link_id: 0,
|
||||
nodes: [],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
version: 0.4
|
||||
}
|
||||
|
||||
it('should fetch workflow data for a specific prompt', async () => {
|
||||
const promptId = 'test_prompt_id'
|
||||
const mockResponse = {
|
||||
[promptId]: {
|
||||
prompt: {
|
||||
priority: 0,
|
||||
prompt_id: promptId,
|
||||
extra_data: {
|
||||
extra_pnginfo: {
|
||||
workflow: mockWorkflow
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue(mockResponse)
|
||||
})
|
||||
api.fetchApi = mockFetchApi
|
||||
|
||||
const result = await api.getWorkflowFromHistory(promptId)
|
||||
|
||||
expect(mockFetchApi).toHaveBeenCalledWith(`/history_v2/${promptId}`)
|
||||
expect(result).toEqual(mockWorkflow)
|
||||
})
|
||||
|
||||
it('should return null when prompt_id is not found', async () => {
|
||||
const promptId = 'non_existent_prompt'
|
||||
const mockResponse = {}
|
||||
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue(mockResponse)
|
||||
})
|
||||
api.fetchApi = mockFetchApi
|
||||
|
||||
const result = await api.getWorkflowFromHistory(promptId)
|
||||
|
||||
expect(mockFetchApi).toHaveBeenCalledWith(`/history_v2/${promptId}`)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null when workflow data is missing', async () => {
|
||||
const promptId = 'test_prompt_id'
|
||||
const mockResponse = {
|
||||
[promptId]: {
|
||||
prompt: {
|
||||
priority: 0,
|
||||
prompt_id: promptId,
|
||||
extra_data: {}
|
||||
},
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue(mockResponse)
|
||||
})
|
||||
api.fetchApi = mockFetchApi
|
||||
|
||||
const result = await api.getWorkflowFromHistory(promptId)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle API errors gracefully', async () => {
|
||||
const promptId = 'test_prompt_id'
|
||||
const mockFetchApi = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
api.fetchApi = mockFetchApi
|
||||
|
||||
const result = await api.getWorkflowFromHistory(promptId)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle malformed response gracefully', async () => {
|
||||
const promptId = 'test_prompt_id'
|
||||
const mockResponse = {
|
||||
[promptId]: null
|
||||
}
|
||||
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue(mockResponse)
|
||||
})
|
||||
api.fetchApi = mockFetchApi
|
||||
|
||||
const result = await api.getWorkflowFromHistory(promptId)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,202 +0,0 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
|
||||
import { useKeybindingService } from '@/services/keybindingService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
KeyComboImpl,
|
||||
KeybindingImpl,
|
||||
useKeybindingStore
|
||||
} from '@/stores/keybindingStore'
|
||||
|
||||
// Mock stores
|
||||
vi.mock('@/stores/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn(() => [])
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/dialogStore', () => ({
|
||||
useDialogStore: vi.fn(() => ({
|
||||
dialogStack: []
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('keybindingService - Escape key handling', () => {
|
||||
let keybindingService: ReturnType<typeof useKeybindingService>
|
||||
let mockCommandExecute: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
|
||||
// Mock command store execute
|
||||
mockCommandExecute = vi.fn()
|
||||
const commandStore = useCommandStore()
|
||||
commandStore.execute = mockCommandExecute
|
||||
|
||||
// Reset dialog store mock to empty
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: []
|
||||
} as any)
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
keybindingService.registerCoreKeybindings()
|
||||
})
|
||||
|
||||
it('should register Escape key for ExitSubgraph command', () => {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
|
||||
// Check that the Escape keybinding exists in core keybindings
|
||||
const escapeKeybinding = CORE_KEYBINDINGS.find(
|
||||
(kb) =>
|
||||
kb.combo.key === 'Escape' && kb.commandId === 'Comfy.Graph.ExitSubgraph'
|
||||
)
|
||||
expect(escapeKeybinding).toBeDefined()
|
||||
|
||||
// Check that it was registered in the store
|
||||
const registeredBinding = keybindingStore.getKeybinding(
|
||||
new KeyComboImpl({ key: 'Escape' })
|
||||
)
|
||||
expect(registeredBinding).toBeDefined()
|
||||
expect(registeredBinding?.commandId).toBe('Comfy.Graph.ExitSubgraph')
|
||||
})
|
||||
|
||||
it('should execute ExitSubgraph command when Escape is pressed', async () => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
// Mock event methods
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockCommandExecute).toHaveBeenCalledWith('Comfy.Graph.ExitSubgraph')
|
||||
})
|
||||
|
||||
it('should not execute command when Escape is pressed with modifiers', async () => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
ctrlKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not execute command when typing in input field', async () => {
|
||||
const inputElement = document.createElement('input')
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [inputElement])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should close dialogs when no keybinding is registered', async () => {
|
||||
// Remove the Escape keybinding
|
||||
const keybindingStore = useKeybindingStore()
|
||||
keybindingStore.unsetKeybinding(
|
||||
new KeybindingImpl({
|
||||
commandId: 'Comfy.Graph.ExitSubgraph',
|
||||
combo: { key: 'Escape' }
|
||||
})
|
||||
)
|
||||
|
||||
// Create a mock dialog
|
||||
const dialog = document.createElement('dialog')
|
||||
dialog.open = true
|
||||
dialog.close = vi.fn()
|
||||
document.body.appendChild(dialog)
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(dialog.close).toHaveBeenCalled()
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(dialog)
|
||||
})
|
||||
|
||||
it('should allow user to rebind Escape key to different command', async () => {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
|
||||
// Add a user keybinding for Escape to a different command
|
||||
keybindingStore.addUserKeybinding(
|
||||
new KeybindingImpl({
|
||||
commandId: 'Custom.Command',
|
||||
combo: { key: 'Escape' }
|
||||
})
|
||||
)
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockCommandExecute).toHaveBeenCalledWith('Custom.Command')
|
||||
expect(mockCommandExecute).not.toHaveBeenCalledWith(
|
||||
'Comfy.Graph.ExitSubgraph'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not execute Escape keybinding when dialogs are open', async () => {
|
||||
// Mock dialog store to have open dialogs
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: [{ key: 'test-dialog' }]
|
||||
} as any)
|
||||
|
||||
// Re-create keybinding service to pick up new mock
|
||||
keybindingService = useKeybindingService()
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
// Should not call preventDefault or execute command
|
||||
expect(event.preventDefault).not.toHaveBeenCalled()
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -3,94 +3,10 @@ import { describe, expect, it } from 'vitest'
|
||||
import { TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
describe('TaskItemImpl', () => {
|
||||
describe('prompt property accessors', () => {
|
||||
it('should correctly access queueIndex from priority', () => {
|
||||
const taskItem = new TaskItemImpl('Pending', {
|
||||
priority: 5,
|
||||
prompt_id: 'test-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
})
|
||||
|
||||
expect(taskItem.queueIndex).toBe(5)
|
||||
})
|
||||
|
||||
it('should correctly access promptId from prompt_id', () => {
|
||||
const taskItem = new TaskItemImpl('History', {
|
||||
priority: 0,
|
||||
prompt_id: 'unique-prompt-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
})
|
||||
|
||||
expect(taskItem.promptId).toBe('unique-prompt-id')
|
||||
})
|
||||
|
||||
it('should correctly access extraData', () => {
|
||||
const extraData = {
|
||||
client_id: 'client-id',
|
||||
extra_pnginfo: {
|
||||
workflow: {
|
||||
last_node_id: 1,
|
||||
last_link_id: 0,
|
||||
nodes: [],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
version: 0.4
|
||||
}
|
||||
}
|
||||
}
|
||||
const taskItem = new TaskItemImpl('Running', {
|
||||
priority: 1,
|
||||
prompt_id: 'test-id',
|
||||
extra_data: extraData
|
||||
})
|
||||
|
||||
expect(taskItem.extraData).toEqual(extraData)
|
||||
})
|
||||
|
||||
it('should correctly access workflow from extraPngInfo', () => {
|
||||
const workflow = {
|
||||
last_node_id: 1,
|
||||
last_link_id: 0,
|
||||
nodes: [],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
version: 0.4
|
||||
}
|
||||
const taskItem = new TaskItemImpl('History', {
|
||||
priority: 0,
|
||||
prompt_id: 'test-id',
|
||||
extra_data: {
|
||||
client_id: 'client-id',
|
||||
extra_pnginfo: { workflow }
|
||||
}
|
||||
})
|
||||
|
||||
expect(taskItem.workflow).toEqual(workflow)
|
||||
})
|
||||
|
||||
it('should return undefined workflow when extraPngInfo is missing', () => {
|
||||
const taskItem = new TaskItemImpl('History', {
|
||||
priority: 0,
|
||||
prompt_id: 'test-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
})
|
||||
|
||||
expect(taskItem.workflow).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove animated property from outputs during construction', () => {
|
||||
const taskItem = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 0,
|
||||
prompt_id: 'prompt-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
|
||||
{ status_str: 'success', messages: [], completed: true },
|
||||
{
|
||||
'node-1': {
|
||||
@@ -110,11 +26,7 @@ describe('TaskItemImpl', () => {
|
||||
it('should handle outputs without animated property', () => {
|
||||
const taskItem = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 0,
|
||||
prompt_id: 'prompt-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
|
||||
{ status_str: 'success', messages: [], completed: true },
|
||||
{
|
||||
'node-1': {
|
||||
@@ -130,11 +42,7 @@ describe('TaskItemImpl', () => {
|
||||
it('should recognize webm video from core', () => {
|
||||
const taskItem = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 0,
|
||||
prompt_id: 'prompt-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
|
||||
{ status_str: 'success', messages: [], completed: true },
|
||||
{
|
||||
'node-1': {
|
||||
@@ -156,11 +64,7 @@ describe('TaskItemImpl', () => {
|
||||
it('should recognize webm video from VHS', () => {
|
||||
const taskItem = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 0,
|
||||
prompt_id: 'prompt-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
|
||||
{ status_str: 'success', messages: [], completed: true },
|
||||
{
|
||||
'node-1': {
|
||||
@@ -189,11 +93,7 @@ describe('TaskItemImpl', () => {
|
||||
it('should recognize mp4 video from core', () => {
|
||||
const taskItem = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 0,
|
||||
prompt_id: 'prompt-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
|
||||
{ status_str: 'success', messages: [], completed: true },
|
||||
{
|
||||
'node-1': {
|
||||
@@ -228,11 +128,7 @@ describe('TaskItemImpl', () => {
|
||||
it(`should recognize ${extension} audio`, () => {
|
||||
const taskItem = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 0,
|
||||
prompt_id: 'prompt-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
|
||||
{ status_str: 'success', messages: [], completed: true },
|
||||
{
|
||||
'node-1': {
|
||||
@@ -257,193 +153,4 @@ describe('TaskItemImpl', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('execution timestamp properties', () => {
|
||||
it('should extract execution start timestamp from messages', () => {
|
||||
const taskItem = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 0,
|
||||
prompt_id: 'test-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
{
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
[
|
||||
'execution_start',
|
||||
{ prompt_id: 'test-id', timestamp: 1234567890 }
|
||||
],
|
||||
[
|
||||
'execution_success',
|
||||
{ prompt_id: 'test-id', timestamp: 1234567900 }
|
||||
]
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
expect(taskItem.executionStartTimestamp).toBe(1234567890)
|
||||
})
|
||||
|
||||
it('should return undefined when no execution_start message exists', () => {
|
||||
const taskItem = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 0,
|
||||
prompt_id: 'test-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
{
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
[
|
||||
'execution_success',
|
||||
{ prompt_id: 'test-id', timestamp: 1234567900 }
|
||||
]
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
expect(taskItem.executionStartTimestamp).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined when status has no messages', () => {
|
||||
const taskItem = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 0,
|
||||
prompt_id: 'test-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
{
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: []
|
||||
}
|
||||
)
|
||||
|
||||
expect(taskItem.executionStartTimestamp).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined when status is undefined', () => {
|
||||
const taskItem = new TaskItemImpl('History', {
|
||||
priority: 0,
|
||||
prompt_id: 'test-id',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
})
|
||||
|
||||
expect(taskItem.executionStartTimestamp).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('sorting by execution start time', () => {
|
||||
it('should sort history tasks by execution start timestamp descending', () => {
|
||||
const task1 = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 1,
|
||||
prompt_id: 'old-task',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
{
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'old-task', timestamp: 1000 }]
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
const task2 = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 2,
|
||||
prompt_id: 'new-task',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
{
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'new-task', timestamp: 3000 }]
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
const task3 = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 3,
|
||||
prompt_id: 'middle-task',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
{
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'middle-task', timestamp: 2000 }]
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
const tasks = [task1, task2, task3]
|
||||
|
||||
// Sort using the same logic as queueStore
|
||||
tasks.sort((a, b) => {
|
||||
const aTime = a.executionStartTimestamp ?? 0
|
||||
const bTime = b.executionStartTimestamp ?? 0
|
||||
return bTime - aTime
|
||||
})
|
||||
|
||||
expect(tasks[0].promptId).toBe('new-task')
|
||||
expect(tasks[1].promptId).toBe('middle-task')
|
||||
expect(tasks[2].promptId).toBe('old-task')
|
||||
})
|
||||
|
||||
it('should place tasks without execution start timestamp at end', () => {
|
||||
const taskWithTime = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 1,
|
||||
prompt_id: 'with-time',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
{
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'with-time', timestamp: 2000 }]
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
const taskWithoutTime = new TaskItemImpl(
|
||||
'History',
|
||||
{
|
||||
priority: 2,
|
||||
prompt_id: 'without-time',
|
||||
extra_data: { client_id: 'client-id' }
|
||||
},
|
||||
{
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: []
|
||||
}
|
||||
)
|
||||
|
||||
const tasks = [taskWithoutTime, taskWithTime]
|
||||
|
||||
// Sort using the same logic as queueStore
|
||||
tasks.sort((a, b) => {
|
||||
const aTime = a.executionStartTimestamp ?? 0
|
||||
const bTime = b.executionStartTimestamp ?? 0
|
||||
return bTime - aTime
|
||||
})
|
||||
|
||||
expect(tasks[0].promptId).toBe('with-time')
|
||||
expect(tasks[1].promptId).toBe('without-time')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,21 +17,10 @@ const SHOULD_MINIFY = process.env.ENABLE_MINIFY === 'true'
|
||||
// vite dev server will listen on all addresses, including LAN and public addresses
|
||||
const VITE_REMOTE_DEV = process.env.VITE_REMOTE_DEV === 'true'
|
||||
const DISABLE_TEMPLATES_PROXY = process.env.DISABLE_TEMPLATES_PROXY === 'true'
|
||||
const DISABLE_VUE_PLUGINS = false // Always enable Vue DevTools for development
|
||||
const DISABLE_VUE_PLUGINS = process.env.DISABLE_VUE_PLUGINS === 'true'
|
||||
|
||||
// Hardcoded to staging cloud for testing frontend changes against cloud backend
|
||||
const DEV_SERVER_COMFYUI_URL = 'https://stagingcloud.comfy.org'
|
||||
// To use local backend, change to: 'http://127.0.0.1:8188'
|
||||
|
||||
// Optional: Add API key to .env as STAGING_API_KEY if needed for authentication
|
||||
const addAuthHeaders = (proxy: any) => {
|
||||
proxy.on('proxyReq', (proxyReq: any, _req: any, _res: any) => {
|
||||
const apiKey = process.env.STAGING_API_KEY
|
||||
if (apiKey) {
|
||||
proxyReq.setHeader('X-API-KEY', apiKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
const DEV_SERVER_COMFYUI_URL =
|
||||
process.env.DEV_SERVER_COMFYUI_URL || 'http://127.0.0.1:8188'
|
||||
|
||||
export default defineConfig({
|
||||
base: '',
|
||||
@@ -39,31 +28,16 @@ export default defineConfig({
|
||||
host: VITE_REMOTE_DEV ? '0.0.0.0' : undefined,
|
||||
proxy: {
|
||||
'/internal': {
|
||||
target: DEV_SERVER_COMFYUI_URL,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
configure: addAuthHeaders
|
||||
target: DEV_SERVER_COMFYUI_URL
|
||||
},
|
||||
|
||||
'/api': {
|
||||
target: DEV_SERVER_COMFYUI_URL,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
configure: (proxy, _options) => {
|
||||
addAuthHeaders(proxy)
|
||||
},
|
||||
// Return empty array for extensions API as these modules
|
||||
// are not on vite's dev server.
|
||||
bypass: (req, res, _options) => {
|
||||
if (req.url === '/api/extensions') {
|
||||
res.end(JSON.stringify([]))
|
||||
return false // Return false to indicate request is handled
|
||||
}
|
||||
// Bypass multi-user auth check from staging
|
||||
if (req.url === '/api/users') {
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.end(JSON.stringify({})) // Return empty object to simulate single-user mode
|
||||
return false // Return false to indicate request is handled
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -71,39 +45,29 @@ export default defineConfig({
|
||||
|
||||
'/ws': {
|
||||
target: DEV_SERVER_COMFYUI_URL,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
configure: addAuthHeaders
|
||||
ws: true
|
||||
},
|
||||
|
||||
'/workflow_templates': {
|
||||
target: DEV_SERVER_COMFYUI_URL,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
configure: addAuthHeaders
|
||||
target: DEV_SERVER_COMFYUI_URL
|
||||
},
|
||||
|
||||
// Proxy extension assets (images/videos) under /extensions to the ComfyUI backend
|
||||
'/extensions': {
|
||||
target: DEV_SERVER_COMFYUI_URL,
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
changeOrigin: true
|
||||
},
|
||||
|
||||
// Proxy docs markdown from backend
|
||||
'/docs': {
|
||||
target: DEV_SERVER_COMFYUI_URL,
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
changeOrigin: true
|
||||
},
|
||||
|
||||
...(!DISABLE_TEMPLATES_PROXY
|
||||
? {
|
||||
'/templates': {
|
||||
target: DEV_SERVER_COMFYUI_URL,
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
target: DEV_SERVER_COMFYUI_URL
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
|
||||
Reference in New Issue
Block a user