Compare commits
2 Commits
docs/folde
...
update-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
702458acbd | ||
|
|
fd01f13fbf |
@@ -25,7 +25,3 @@ ENABLE_MINIFY=true
|
||||
# templates are served via the normal method from the server's python site
|
||||
# packages.
|
||||
DISABLE_TEMPLATES_PROXY=false
|
||||
|
||||
# If playwright tests are being run via vite dev server, Vue plugins will
|
||||
# invalidate screenshots. When `true`, vite plugins will not be loaded.
|
||||
DISABLE_VUE_PLUGINS=false
|
||||
|
||||
72
.github/workflows/dev-release.yaml
vendored
@@ -1,72 +0,0 @@
|
||||
name: Create Dev PyPI Package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
devVersion:
|
||||
description: 'Dev version'
|
||||
required: true
|
||||
type: number
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.current_version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
- name: Build project
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
npm run zipdist
|
||||
- name: Upload dist artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-files
|
||||
path: |
|
||||
dist/
|
||||
dist.zip
|
||||
|
||||
publish_pypi:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Download dist artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist-files
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install build dependencies
|
||||
run: python -m pip install build
|
||||
- name: Setup pypi package
|
||||
run: |
|
||||
mkdir -p comfyui_frontend_package/comfyui_frontend_package/static/
|
||||
cp -r dist/* comfyui_frontend_package/comfyui_frontend_package/static/
|
||||
- name: Build pypi package
|
||||
run: python -m build
|
||||
working-directory: comfyui_frontend_package
|
||||
env:
|
||||
COMFYUI_FRONTEND_VERSION: ${{ format('{0}.dev{1}', needs.build.outputs.version, inputs.devVersion) }}
|
||||
- name: Publish pypi package
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
packages-dir: comfyui_frontend_package/dist
|
||||
2
.github/workflows/test-ui.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
repository: 'Comfy-Org/ComfyUI_devtools'
|
||||
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
|
||||
ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684'
|
||||
ref: '9d2421fd3208a310e4d0f71fca2ea0c985759c33'
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
135
README.md
@@ -526,38 +526,6 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (v16 or later) and npm must be installed
|
||||
- Git for version control
|
||||
- A running ComfyUI backend instance
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/Comfy-Org/ComfyUI_frontend.git
|
||||
cd ComfyUI_frontend
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Configure environment (optional):
|
||||
Create a `.env` file in the project root based on the provided [.env.example](.env.example) file.
|
||||
|
||||
**Note about ports**: By default, the dev server expects the ComfyUI backend at `localhost:8188`. If your ComfyUI instance runs on a different port, update this in your `.env` file.
|
||||
|
||||
### Dev Server Configuration
|
||||
|
||||
To launch ComfyUI and have it connect to your development server:
|
||||
|
||||
```bash
|
||||
python main.py --port 8188
|
||||
```
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- [Vue 3](https://vuejs.org/) with [TypeScript](https://www.typescriptlang.org/)
|
||||
@@ -579,6 +547,7 @@ core extensions will be loaded.
|
||||
|
||||
- Start local ComfyUI backend at `localhost:8188`
|
||||
- Run `npm run dev` to start the dev server
|
||||
- Run `npm run dev:electron` to start the dev server with electron API mocked
|
||||
|
||||
#### Access dev server on touch devices
|
||||
|
||||
@@ -606,7 +575,7 @@ navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to
|
||||
|
||||
This project includes `.vscode/launch.json.default` and `.vscode/settings.json.default` files with recommended launch and workspace settings for editors that use the `.vscode` directory (e.g., VS Code, Cursor, etc.).
|
||||
|
||||
We've also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
|
||||
We’ve also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
|
||||
|
||||
### Unit Test
|
||||
|
||||
@@ -637,103 +606,3 @@ This will replace the litegraph package in this repo with the local litegraph re
|
||||
### i18n
|
||||
|
||||
See [locales/README.md](src/locales/README.md) for details.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
> **Note**: For comprehensive troubleshooting and how-to guides, please refer to our [official documentation](https://docs.comfy.org/). This section covers only the most common issues related to frontend development.
|
||||
|
||||
> **Desktop Users**: For issues specific to the desktop application, please refer to the [ComfyUI desktop repository](https://github.com/Comfy-Org/desktop).
|
||||
|
||||
### Debugging Custom Node (Extension) Issues
|
||||
|
||||
If you're experiencing crashes, errors, or unexpected behavior with ComfyUI, it's often caused by custom nodes (extensions). Follow these steps to identify and resolve the issues:
|
||||
|
||||
#### Step 1: Verify if custom nodes are causing the problem
|
||||
|
||||
Run ComfyUI with the `--disable-all-custom-nodes` flag:
|
||||
|
||||
```bash
|
||||
python main.py --disable-all-custom-nodes
|
||||
```
|
||||
|
||||
If the issue disappears, a custom node is the culprit. Proceed to the next step.
|
||||
|
||||
#### Step 2: Identify the problematic custom node using binary search
|
||||
|
||||
Rather than disabling nodes one by one, use this more efficient approach:
|
||||
|
||||
1. Temporarily move half of your custom nodes out of the `custom_nodes` directory
|
||||
```bash
|
||||
# Create a temporary directory
|
||||
# Linux/Mac
|
||||
mkdir ~/custom_nodes_disabled
|
||||
|
||||
# Windows
|
||||
mkdir %USERPROFILE%\custom_nodes_disabled
|
||||
|
||||
# Move half of your custom nodes (assuming you have node1 through node8)
|
||||
# Linux/Mac
|
||||
mv custom_nodes/node1 custom_nodes/node2 custom_nodes/node3 custom_nodes/node4 ~/custom_nodes_disabled/
|
||||
|
||||
# Windows
|
||||
move custom_nodes\node1 custom_nodes\node2 custom_nodes\node3 custom_nodes\node4 %USERPROFILE%\custom_nodes_disabled\
|
||||
```
|
||||
|
||||
2. Run ComfyUI again
|
||||
- If the issue persists: The problem is in nodes 5-8 (the remaining half)
|
||||
- If the issue disappears: The problem is in nodes 1-4 (the moved half)
|
||||
|
||||
3. Let's assume the issue disappeared, so the problem is in nodes 1-4. Move half of these for the next test:
|
||||
```bash
|
||||
# Move nodes 3-4 back to custom_nodes
|
||||
# Linux/Mac
|
||||
mv ~/custom_nodes_disabled/node3 ~/custom_nodes_disabled/node4 custom_nodes/
|
||||
|
||||
# Windows
|
||||
move %USERPROFILE%\custom_nodes_disabled\node3 %USERPROFILE%\custom_nodes_disabled\node4 custom_nodes\
|
||||
```
|
||||
|
||||
4. Run ComfyUI again
|
||||
- If the issue reappears: The problem is in nodes 3-4
|
||||
- If issue still gone: The problem is in nodes 1-2
|
||||
|
||||
5. Let's assume the issue reappeared, so the problem is in nodes 3-4. Test each one:
|
||||
```bash
|
||||
# Move node 3 back to disabled
|
||||
# Linux/Mac
|
||||
mv custom_nodes/node3 ~/custom_nodes_disabled/
|
||||
|
||||
# Windows
|
||||
move custom_nodes\node3 %USERPROFILE%\custom_nodes_disabled\
|
||||
```
|
||||
|
||||
6. Run ComfyUI again
|
||||
- If the issue disappears: node3 is the problem
|
||||
- If issue persists: node4 is the problem
|
||||
|
||||
7. Repeat until you identify the specific problematic node
|
||||
|
||||
#### Step 3: Update or replace the problematic node
|
||||
|
||||
Once identified:
|
||||
1. Check for updates to the problematic custom node
|
||||
2. Consider alternatives with similar functionality
|
||||
3. Report the issue to the custom node developer with specific details
|
||||
|
||||
### Common Issues and Solutions
|
||||
|
||||
- **"Module not found" errors**: Usually indicates missing Python dependencies. Check the custom node's `requirements.txt` file for required packages and install them:
|
||||
```bash
|
||||
pip install -r custom_nodes/problematic_node/requirements.txt
|
||||
```
|
||||
|
||||
- **Frontend or Templates Package Not Updated**: After updating ComfyUI via Git, ensure you update the frontend dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
- **Can't Find Custom Node**: Make sure to disable node validation in ComfyUI settings.
|
||||
|
||||
- **Error Toast About Workflow Failing Validation**: Report the issue to the ComfyUI team. As a temporary workaround, disable workflow validation in settings.
|
||||
|
||||
- **Login Issues When Not on Localhost**: Normal login is only available when accessing from localhost. If you're running ComfyUI via LAN, another domain, or headless, you can use our API key feature to authenticate. The API key lets you log in normally through the UI. Generate an API key at [platform.comfy.org/login](https://platform.comfy.org/login) and use it in the API Key field in the login dialog or with the `--api-key` command line argument. Refer to our [API Key Integration Guide](https://docs.comfy.org/essentials/comfyui-server/api-key-integration#integration-of-api-key-to-use-comfyui-api-nodes) for complete setup instructions.
|
||||
@@ -33,11 +33,5 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -130,11 +130,6 @@
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -42,12 +42,12 @@
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"scale": 2.1600300525920346,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
63.071794466403446,
|
||||
75.18055335968394
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -499,11 +499,6 @@
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
{
|
||||
"id": "1a95532f-c8aa-4c9d-a7f6-f928ba2d4862",
|
||||
"revision": 0,
|
||||
"last_node_id": 4,
|
||||
"last_link_id": 3,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PreviewAny",
|
||||
"pos": [946.2566528320312, 598.4373168945312],
|
||||
"size": [140, 76],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "PreviewAny",
|
||||
"pos": [951.0236206054688, 421.3861083984375],
|
||||
"size": [140, 76],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "PrimitiveString",
|
||||
"pos": [575.1760864257812, 504.5214538574219],
|
||||
"size": [270, 58],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [2, 3]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveString"
|
||||
},
|
||||
"widgets_values": ["foo"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[2, 3, 0, 1, 0, "*"],
|
||||
[3, 3, 0, 4, 0, "*"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.19.1",
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -160,4 +160,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -43,10 +43,6 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"groupNodes": {
|
||||
"group_node": {
|
||||
"nodes": [
|
||||
@@ -405,4 +401,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -110,10 +110,6 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"groupNodes": {
|
||||
"hello": {
|
||||
"nodes": [
|
||||
@@ -253,4 +249,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -44,11 +44,6 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -61,11 +61,6 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"id": "5635564e-189f-49e4-9b25-6b7634bcd595",
|
||||
"revision": 0,
|
||||
"last_node_id": 78,
|
||||
"last_link_id": 53,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 78,
|
||||
"type": "DevToolsNodeWithV2ComboInput",
|
||||
"pos": [1320, 904],
|
||||
"size": [270.3199157714844, 58],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "COMBO",
|
||||
"type": "COMBO",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsNodeWithV2ComboInput"
|
||||
},
|
||||
"widgets_values": ["A"]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.19.7"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -50,11 +50,6 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,7 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"groupNodes": {},
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
"groupNodes": {}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -54,11 +54,6 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -92,14 +92,10 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": false,
|
||||
"VHS_KeepIntermediate": false
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -84,10 +84,6 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -106,7 +106,10 @@
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [0, 0]
|
||||
"offset": {
|
||||
"0": 0,
|
||||
"1": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
|
||||
@@ -368,10 +368,10 @@
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
149.9747408641311,
|
||||
383.8593224280729
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,5 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -8,4 +8,4 @@
|
||||
"title": "Load Animated Image"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,6 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -41,11 +41,6 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -54,11 +54,7 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.17.0",
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
"frontendVersion": "1.17.0"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -133,9 +133,6 @@ export class ComfyPage {
|
||||
// Inputs
|
||||
public readonly workflowUploadInput: Locator
|
||||
|
||||
// Toasts
|
||||
public readonly visibleToasts: Locator
|
||||
|
||||
// Components
|
||||
public readonly searchBox: ComfyNodeSearchBox
|
||||
public readonly menu: ComfyMenu
|
||||
@@ -162,8 +159,6 @@ export class ComfyPage {
|
||||
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
|
||||
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
this.visibleToasts = page.locator('.p-toast-message:visible')
|
||||
|
||||
this.searchBox = new ComfyNodeSearchBox(page)
|
||||
this.menu = new ComfyMenu(page)
|
||||
this.actionbar = new ComfyActionbar(page)
|
||||
@@ -275,6 +270,7 @@ export class ComfyPage {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
localStorage.setItem('Comfy.userId', id)
|
||||
localStorage.setItem('api-nodes-news-seen', 'true')
|
||||
}, this.id)
|
||||
}
|
||||
await this.goto()
|
||||
@@ -401,30 +397,6 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async deleteWorkflow(
|
||||
workflowName: string,
|
||||
whenMissing: 'ignoreMissing' | 'throwIfMissing' = 'ignoreMissing'
|
||||
) {
|
||||
// Open workflows tab
|
||||
const { workflowsTab } = this.menu
|
||||
await workflowsTab.open()
|
||||
|
||||
// Action to take if workflow missing
|
||||
if (whenMissing === 'ignoreMissing') {
|
||||
const workflows = await workflowsTab.getTopLevelSavedWorkflowNames()
|
||||
if (!workflows.includes(workflowName)) return
|
||||
}
|
||||
|
||||
// Delete workflow
|
||||
await workflowsTab.getPersistedItem(workflowName).click({ button: 'right' })
|
||||
await this.clickContextMenuItem('Delete')
|
||||
await this.confirmDialog.delete.click()
|
||||
|
||||
// Clear toast & close tab
|
||||
await this.closeToasts(1)
|
||||
await workflowsTab.close()
|
||||
}
|
||||
|
||||
async resetView() {
|
||||
if (await this.resetViewButton.isVisible()) {
|
||||
await this.resetViewButton.click()
|
||||
@@ -441,20 +413,7 @@ export class ComfyPage {
|
||||
}
|
||||
|
||||
async getVisibleToastCount() {
|
||||
return await this.visibleToasts.count()
|
||||
}
|
||||
|
||||
async closeToasts(requireCount = 0) {
|
||||
if (requireCount) await expect(this.visibleToasts).toHaveCount(requireCount)
|
||||
|
||||
// Clear all toasts
|
||||
const toastCloseButtons = await this.page
|
||||
.locator('.p-toast-close-button')
|
||||
.all()
|
||||
for (const button of toastCloseButtons) {
|
||||
await button.click()
|
||||
}
|
||||
await expect(this.visibleToasts).toHaveCount(0)
|
||||
return await this.page.locator('.p-toast-message:visible').count()
|
||||
}
|
||||
|
||||
async clickTextEncodeNode1() {
|
||||
|
||||
@@ -18,27 +18,3 @@ test.describe('Execution', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Execute to selected output nodes', () => {
|
||||
test('Execute to selected output nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('execution/partial_execution')
|
||||
const input = await comfyPage.getNodeRefById(3)
|
||||
const output1 = await comfyPage.getNodeRefById(1)
|
||||
const output2 = await comfyPage.getNodeRefById(4)
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
|
||||
await output1.click('title')
|
||||
|
||||
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
|
||||
// @note: Wait for the execution to finish. We might want to move to a more
|
||||
// reliable way to wait for the execution to finish. Workflow in this test
|
||||
// is simple enough that this is fine for now.
|
||||
await comfyPage.page.waitForTimeout(200)
|
||||
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { type ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Item Interaction', () => {
|
||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||
@@ -666,12 +666,6 @@ test.describe('Load workflow', () => {
|
||||
expect(activeWorkflowName).toEqual(workflowPathB)
|
||||
})
|
||||
})
|
||||
|
||||
test('Auto fit view after loading workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false)
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Load duplicate workflow', () => {
|
||||
@@ -689,42 +683,3 @@ test.describe('Load duplicate workflow', () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Viewport settings', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar')
|
||||
|
||||
await comfyPage.setupWorkflowsDirectory({})
|
||||
})
|
||||
|
||||
test('Keeps viewport settings when changing tabs', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
// Screenshot the canvas element
|
||||
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-a.png')
|
||||
|
||||
// Save workflow as a new file, then zoom out before screen shot
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('Workflow B')
|
||||
await comfyMouse.move(comfyPage.emptySpace)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await comfyMouse.wheel(0, 60)
|
||||
}
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-b.png')
|
||||
|
||||
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
|
||||
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
|
||||
|
||||
// Go back to Workflow A
|
||||
await tabA.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-a.png')
|
||||
|
||||
// And back to Workflow B
|
||||
await tabB.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-b.png')
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 48 KiB |
@@ -53,26 +53,6 @@ test.describe('Combo text widget', () => {
|
||||
const refreshedComboValues = await getComboValues()
|
||||
expect(refreshedComboValues).not.toEqual(initialComboValues)
|
||||
})
|
||||
|
||||
test('Should refresh combo values of nodes with v2 combo input spec', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('node_with_v2_combo_input')
|
||||
// click canvas to focus
|
||||
await comfyPage.page.mouse.click(400, 300)
|
||||
// press R to trigger refresh
|
||||
await comfyPage.page.keyboard.press('r')
|
||||
// wait for nodes' widgets to be updated
|
||||
await comfyPage.page.mouse.click(400, 300)
|
||||
await comfyPage.nextFrame()
|
||||
// get the combo widget's values
|
||||
const comboValues = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].graph.nodes
|
||||
.find((node) => node.title === 'Node With V2 Combo Input')
|
||||
.widgets.find((widget) => widget.name === 'combo_input').options.values
|
||||
})
|
||||
expect(comboValues).toEqual(['A', 'B'])
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Boolean widget', () => {
|
||||
|
||||
1717
package-lock.json
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.20.2",
|
||||
"version": "1.19.4",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -63,8 +63,6 @@
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.19",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vite-plugin-vue-devtools": "^7.7.6",
|
||||
"vitest": "^2.0.0",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"zip-dir": "^2.0.0",
|
||||
@@ -74,7 +72,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.15.11",
|
||||
"@comfyorg/litegraph": "^0.15.3",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 647 B |
|
Before Width: | Height: | Size: 674 B |
|
Before Width: | Height: | Size: 674 B |
|
Before Width: | Height: | Size: 674 B |
|
Before Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 700 B |
|
Before Width: | Height: | Size: 702 B |
|
Before Width: | Height: | Size: 705 B |
|
Before Width: | Height: | Size: 708 B |
|
Before Width: | Height: | Size: 705 B |
@@ -1,7 +1,8 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, reactive } from 'vue'
|
||||
|
||||
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
|
||||
import BrowserTabTitle from '@/components/BrowserTabTitle.vue'
|
||||
|
||||
// Mock the execution store
|
||||
const executionStore = reactive({
|
||||
@@ -30,8 +31,11 @@ vi.mock('@/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => workflowStore
|
||||
}))
|
||||
|
||||
describe('useBrowserTabTitle', () => {
|
||||
describe('BrowserTabTitle.vue', () => {
|
||||
let wrapper: ReturnType<typeof mount> | null
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = null
|
||||
// reset execution store
|
||||
executionStore.isIdle = true
|
||||
executionStore.executionProgress = 0
|
||||
@@ -46,8 +50,12 @@ describe('useBrowserTabTitle', () => {
|
||||
document.title = ''
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper?.unmount()
|
||||
})
|
||||
|
||||
it('sets default title when idle and no workflow', () => {
|
||||
useBrowserTabTitle()
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
expect(document.title).toBe('ComfyUI')
|
||||
})
|
||||
|
||||
@@ -58,7 +66,7 @@ describe('useBrowserTabTitle', () => {
|
||||
isModified: false,
|
||||
isPersisted: true
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('myFlow - ComfyUI')
|
||||
})
|
||||
@@ -70,21 +78,19 @@ describe('useBrowserTabTitle', () => {
|
||||
isModified: true,
|
||||
isPersisted: true
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('*myFlow - ComfyUI')
|
||||
})
|
||||
|
||||
// Fails when run together with other tests. Suspect to be caused by leaked
|
||||
// state from previous tests.
|
||||
it.skip('disables workflow title when menu disabled', async () => {
|
||||
it('disables workflow title when menu disabled', async () => {
|
||||
;(settingStore.get as any).mockReturnValue('Disabled')
|
||||
workflowStore.activeWorkflow = {
|
||||
filename: 'myFlow',
|
||||
isModified: false,
|
||||
isPersisted: true
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('ComfyUI')
|
||||
})
|
||||
@@ -92,7 +98,7 @@ describe('useBrowserTabTitle', () => {
|
||||
it('shows execution progress when not idle without workflow', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.3
|
||||
useBrowserTabTitle()
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[30%]ComfyUI')
|
||||
})
|
||||
@@ -102,7 +108,7 @@ describe('useBrowserTabTitle', () => {
|
||||
executionStore.executionProgress = 0.4
|
||||
executionStore.executingNodeProgress = 0.5
|
||||
executionStore.executingNode = { type: 'Foo' }
|
||||
useBrowserTabTitle()
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[40%][50%] Foo')
|
||||
})
|
||||
58
src/components/BrowserTabTitle.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- This component does not render anything visible. -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTitle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
const DEFAULT_TITLE = 'ComfyUI'
|
||||
const TITLE_SUFFIX = ' - ComfyUI'
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const executionText = computed(() =>
|
||||
executionStore.isIdle
|
||||
? ''
|
||||
: `[${Math.round(executionStore.executionProgress * 100)}%]`
|
||||
)
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const newMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const isUnsavedText = computed(() =>
|
||||
workflowStore.activeWorkflow?.isModified ||
|
||||
!workflowStore.activeWorkflow?.isPersisted
|
||||
? ' *'
|
||||
: ''
|
||||
)
|
||||
const workflowNameText = computed(() => {
|
||||
const workflowName = workflowStore.activeWorkflow?.filename
|
||||
return workflowName
|
||||
? isUnsavedText.value + workflowName + TITLE_SUFFIX
|
||||
: DEFAULT_TITLE
|
||||
})
|
||||
|
||||
const nodeExecutionTitle = computed(() =>
|
||||
executionStore.executingNode && executionStore.executingNodeProgress
|
||||
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
|
||||
: ''
|
||||
)
|
||||
|
||||
const workflowTitle = computed(
|
||||
() =>
|
||||
executionText.value +
|
||||
(newMenuEnabled.value ? workflowNameText.value : DEFAULT_TITLE)
|
||||
)
|
||||
|
||||
const title = computed(() => nodeExecutionTitle.value || workflowTitle.value)
|
||||
useTitle(title)
|
||||
</script>
|
||||
@@ -63,6 +63,15 @@ watchDebounced(
|
||||
|
||||
// Set initial position to bottom center
|
||||
const setInitialPosition = () => {
|
||||
if (x.value !== 0 || y.value !== 0) {
|
||||
return
|
||||
}
|
||||
if (storedPosition.value.x !== 0 || storedPosition.value.y !== 0) {
|
||||
x.value = storedPosition.value.x
|
||||
y.value = storedPosition.value.y
|
||||
captureLastDragState()
|
||||
return
|
||||
}
|
||||
if (panelRef.value) {
|
||||
const screenWidth = window.innerWidth
|
||||
const screenHeight = window.innerHeight
|
||||
@@ -73,25 +82,9 @@ const setInitialPosition = () => {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if stored position exists and is within bounds
|
||||
if (storedPosition.value.x !== 0 || storedPosition.value.y !== 0) {
|
||||
// Ensure stored position is within screen bounds
|
||||
x.value = clamp(storedPosition.value.x, 0, screenWidth - menuWidth)
|
||||
y.value = clamp(storedPosition.value.y, 0, screenHeight - menuHeight)
|
||||
captureLastDragState()
|
||||
return
|
||||
}
|
||||
|
||||
// If no stored position or current position, set to bottom center
|
||||
if (x.value === 0 && y.value === 0) {
|
||||
x.value = clamp((screenWidth - menuWidth) / 2, 0, screenWidth - menuWidth)
|
||||
y.value = clamp(
|
||||
screenHeight - menuHeight - 10,
|
||||
0,
|
||||
screenHeight - menuHeight
|
||||
)
|
||||
captureLastDragState()
|
||||
}
|
||||
x.value = (screenWidth - menuWidth) / 2
|
||||
y.value = screenHeight - menuHeight - 10 // 10px margin from bottom
|
||||
captureLastDragState()
|
||||
}
|
||||
}
|
||||
onMounted(setInitialPosition)
|
||||
|
||||
@@ -32,8 +32,6 @@ describe('TreeExplorerTreeNode', () => {
|
||||
handleRename: () => {}
|
||||
} as RenderedTreeExplorerNode
|
||||
|
||||
const mockHandleEditLabel = vi.fn()
|
||||
|
||||
beforeAll(() => {
|
||||
// Create a Vue app instance for PrimeVuePrimeVue
|
||||
const app = createApp({})
|
||||
@@ -50,10 +48,7 @@ describe('TreeExplorerTreeNode', () => {
|
||||
props: { node: mockNode },
|
||||
global: {
|
||||
components: { EditableText, Badge },
|
||||
plugins: [createTestingPinia(), i18n],
|
||||
provide: {
|
||||
[InjectKeyHandleEditLabelFunction]: mockHandleEditLabel
|
||||
}
|
||||
plugins: [createTestingPinia(), i18n]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -77,10 +72,7 @@ describe('TreeExplorerTreeNode', () => {
|
||||
},
|
||||
global: {
|
||||
components: { EditableText, Badge, InputText },
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue],
|
||||
provide: {
|
||||
[InjectKeyHandleEditLabelFunction]: mockHandleEditLabel
|
||||
}
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -70,12 +70,11 @@ const state = computed<GridState>(() => {
|
||||
const fromCol = fromRow * cols.value
|
||||
const toCol = toRow * cols.value
|
||||
const remainingCol = items.length - toCol
|
||||
const hasMoreToRender = remainingCol >= 0
|
||||
|
||||
return {
|
||||
start: clamp(fromCol, 0, items?.length),
|
||||
end: clamp(toCol, fromCol, items?.length),
|
||||
isNearEnd: hasMoreToRender && remainingCol <= cols.value * bufferRows
|
||||
isNearEnd: remainingCol <= cols.value * bufferRows
|
||||
}
|
||||
})
|
||||
const renderedItems = computed(() =>
|
||||
|
||||
74
src/components/dialog/content/ApiNodesNewsContent.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-12 p-2 w-96">
|
||||
<img src="@/assets/images/api-nodes-news.webp" alt="API Nodes News" />
|
||||
<div class="flex flex-col gap-2 justify-center items-center">
|
||||
<div class="text-xl">
|
||||
{{ $t('apiNodesNews.introducing') }}
|
||||
<span class="text-amber-500">API NODES</span>
|
||||
</div>
|
||||
<div class="text-muted">{{ $t('apiNodesNews.subtitle') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
class="grid grid-cols-[auto_1fr] gap-2 items-center"
|
||||
>
|
||||
<Tag class="w-8 h-8" :value="index + 1" rounded />
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>{{ step.title }}</div>
|
||||
<div v-if="step.subtitle" class="text-muted">
|
||||
{{ step.subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-between">
|
||||
<Button label="Learn More" text @click="handleLearnMore" />
|
||||
<Button label="Close" @click="onClose" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Tag from 'primevue/tag'
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const steps: {
|
||||
title: string
|
||||
subtitle?: string
|
||||
}[] = [
|
||||
{
|
||||
title: t('apiNodesNews.steps.step1.title'),
|
||||
subtitle: t('apiNodesNews.steps.step1.subtitle')
|
||||
},
|
||||
{
|
||||
title: t('apiNodesNews.steps.step2.title'),
|
||||
subtitle: t('apiNodesNews.steps.step2.subtitle')
|
||||
},
|
||||
{
|
||||
title: t('apiNodesNews.steps.step3.title')
|
||||
},
|
||||
{
|
||||
title: t('apiNodesNews.steps.step4.title')
|
||||
}
|
||||
]
|
||||
|
||||
const { onClose } = defineProps<{
|
||||
onClose: () => void
|
||||
}>()
|
||||
|
||||
const handleLearnMore = () => {
|
||||
window.open('https://blog.comfy.org/p/comfyui-native-api-nodes', '_blank')
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
localStorage.setItem('api-nodes-news-seen', 'true')
|
||||
})
|
||||
</script>
|
||||
@@ -67,9 +67,9 @@ import Tabs from 'primevue/tabs'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSettingSearch } from '@/composables/setting/useSettingSearch'
|
||||
import { useSettingUI } from '@/composables/setting/useSettingUI'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { SettingTreeNode } from '@/stores/settingStore'
|
||||
import { ISettingGroup, SettingParams } from '@/types/settingTypes'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
@@ -107,7 +107,7 @@ const {
|
||||
getSearchResults
|
||||
} = useSettingSearch()
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
|
||||
// Sort groups for a category
|
||||
const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
|
||||
@@ -140,7 +140,7 @@ watch(activeCategory, (_, oldValue) => {
|
||||
activeCategory.value = oldValue
|
||||
}
|
||||
if (activeCategory.value?.key === 'credits') {
|
||||
void authActions.fetchBalance()
|
||||
void authService.fetchBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,141 +1,95 @@
|
||||
<template>
|
||||
<div class="w-96 p-2 overflow-x-hidden">
|
||||
<ApiKeyForm
|
||||
v-if="showApiKeyForm"
|
||||
@back="showApiKeyForm = false"
|
||||
@success="onSuccess"
|
||||
/>
|
||||
<template v-else>
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-4 mb-8">
|
||||
<h1 class="text-2xl font-medium leading-normal my-0">
|
||||
{{ isSignIn ? t('auth.login.title') : t('auth.signup.title') }}
|
||||
</h1>
|
||||
<p class="text-base my-0">
|
||||
<span class="text-muted">{{
|
||||
isSignIn
|
||||
? t('auth.login.newUser')
|
||||
: t('auth.signup.alreadyHaveAccount')
|
||||
}}</span>
|
||||
<span
|
||||
class="ml-1 cursor-pointer text-blue-500"
|
||||
@click="toggleState"
|
||||
>{{
|
||||
isSignIn ? t('auth.login.signUp') : t('auth.signup.signIn')
|
||||
}}</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Message v-if="!isSecureContext" severity="warn" class="mb-4">
|
||||
{{ t('auth.login.insecureContextWarning') }}
|
||||
</Message>
|
||||
|
||||
<!-- Form -->
|
||||
<SignInForm v-if="isSignIn" @submit="signInWithEmail" />
|
||||
<template v-else>
|
||||
<Message v-if="userIsInChina" severity="warn" class="mb-4">
|
||||
{{ t('auth.signup.regionRestrictionChina') }}
|
||||
</Message>
|
||||
<SignUpForm v-else @submit="signUpWithEmail" />
|
||||
</template>
|
||||
|
||||
<!-- Divider -->
|
||||
<Divider align="center" layout="horizontal" class="my-8">
|
||||
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
|
||||
</Divider>
|
||||
|
||||
<!-- Social Login Buttons -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGoogle"
|
||||
>
|
||||
<i class="pi pi-google mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGoogle')
|
||||
: t('auth.signup.signUpWithGoogle')
|
||||
}}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGithub"
|
||||
>
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGithub')
|
||||
: t('auth.signup.signUpWithGithub')
|
||||
}}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="showApiKeyForm = true"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/comfy-logo-mono.svg"
|
||||
class="w-5 h-5 mr-2"
|
||||
alt="Comfy"
|
||||
/>
|
||||
{{ t('auth.login.useApiKey') }}
|
||||
</Button>
|
||||
<small class="text-muted text-center">
|
||||
{{ t('auth.apiKey.helpText') }}
|
||||
<a
|
||||
:href="`${COMFY_PLATFORM_BASE_URL}/login`"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.apiKey.generateKey') }}
|
||||
</a>
|
||||
</small>
|
||||
<Message
|
||||
v-if="authActions.accessError.value"
|
||||
severity="info"
|
||||
icon="pi pi-info-circle"
|
||||
variant="outlined"
|
||||
closable
|
||||
>
|
||||
{{ t('toastMessages.useApiKeyTip') }}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<!-- Terms & Contact -->
|
||||
<p class="text-xs text-muted mt-8">
|
||||
{{ t('auth.login.termsText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/terms-of-service"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.login.termsLink') }}
|
||||
</a>
|
||||
{{ t('auth.login.andText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/privacy"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.login.privacyLink') }} </a
|
||||
>.
|
||||
{{ t('auth.login.questionsContactPrefix') }}
|
||||
<a href="mailto:hello@comfy.org" class="text-blue-500 cursor-pointer">
|
||||
hello@comfy.org</a
|
||||
>.
|
||||
<div class="w-96 p-2">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-4 mb-8">
|
||||
<h1 class="text-2xl font-medium leading-normal my-0">
|
||||
{{ isSignIn ? t('auth.login.title') : t('auth.signup.title') }}
|
||||
</h1>
|
||||
<p class="text-base my-0">
|
||||
<span class="text-muted">{{
|
||||
isSignIn
|
||||
? t('auth.login.newUser')
|
||||
: t('auth.signup.alreadyHaveAccount')
|
||||
}}</span>
|
||||
<span class="ml-1 cursor-pointer text-blue-500" @click="toggleState">{{
|
||||
isSignIn ? t('auth.login.signUp') : t('auth.signup.signIn')
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Message v-if="!isSecureContext" severity="warn" class="mb-4">
|
||||
{{ t('auth.login.insecureContextWarning') }}
|
||||
</Message>
|
||||
|
||||
<!-- Form -->
|
||||
<SignInForm v-if="isSignIn" @submit="signInWithEmail" />
|
||||
<template v-else>
|
||||
<Message v-if="userIsInChina" severity="warn" class="mb-4">
|
||||
{{ t('auth.signup.regionRestrictionChina') }}
|
||||
</Message>
|
||||
<SignUpForm v-else @submit="signUpWithEmail" />
|
||||
</template>
|
||||
|
||||
<!-- Divider -->
|
||||
<Divider align="center" layout="horizontal" class="my-8">
|
||||
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
|
||||
</Divider>
|
||||
|
||||
<!-- Social Login Buttons -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGoogle"
|
||||
>
|
||||
<i class="pi pi-google mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGoogle')
|
||||
: t('auth.signup.signUpWithGoogle')
|
||||
}}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGithub"
|
||||
>
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGithub')
|
||||
: t('auth.signup.signUpWithGithub')
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Terms & Contact -->
|
||||
<p class="text-xs text-muted mt-8">
|
||||
{{ t('auth.login.termsText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/terms-of-service"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.login.termsLink') }}
|
||||
</a>
|
||||
{{ t('auth.login.andText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/privacy"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.login.privacyLink') }} </a
|
||||
>.
|
||||
{{ t('auth.login.questionsContactPrefix') }}
|
||||
<a href="mailto:hello@comfy.org" class="text-blue-500 cursor-pointer">
|
||||
hello@comfy.org</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -143,15 +97,13 @@
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import Message from 'primevue/message'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
|
||||
import { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
|
||||
import ApiKeyForm from './signin/ApiKeyForm.vue'
|
||||
import SignInForm from './signin/SignInForm.vue'
|
||||
import SignUpForm from './signin/SignUpForm.vue'
|
||||
|
||||
@@ -160,36 +112,33 @@ const { onSuccess } = defineProps<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
const isSecureContext = window.isSecureContext
|
||||
const isSignIn = ref(true)
|
||||
const showApiKeyForm = ref(false)
|
||||
|
||||
const toggleState = () => {
|
||||
isSignIn.value = !isSignIn.value
|
||||
showApiKeyForm.value = false
|
||||
}
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
if (await authActions.signInWithGoogle()) {
|
||||
if (await authService.signInWithGoogle()) {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
const signInWithGithub = async () => {
|
||||
if (await authActions.signInWithGithub()) {
|
||||
if (await authService.signInWithGithub()) {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
const signInWithEmail = async (values: SignInData) => {
|
||||
if (await authActions.signInWithEmail(values.email, values.password)) {
|
||||
if (await authService.signInWithEmail(values.email, values.password)) {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
const signUpWithEmail = async (values: SignUpData) => {
|
||||
if (await authActions.signUpWithEmail(values.email, values.password)) {
|
||||
if (await authService.signUpWithEmail(values.email, values.password)) {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
@@ -198,8 +147,4 @@ const userIsInChina = ref(false)
|
||||
onMounted(async () => {
|
||||
userIsInChina.value = await isInChina()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
authActions.accessError.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
|
||||
import CreditTopUpOption from './credit/CreditTopUpOption.vue'
|
||||
|
||||
@@ -65,9 +65,9 @@ const {
|
||||
preselectedAmountOption?: number
|
||||
}>()
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
|
||||
const handleSeeDetails = async () => {
|
||||
await authActions.accessBillingPortal()
|
||||
await authService.accessBillingPortal()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,10 +23,10 @@ import Button from 'primevue/button'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import PasswordFields from '@/components/dialog/content/signin/PasswordFields.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { updatePasswordSchema } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
const loading = ref(false)
|
||||
|
||||
const { onSuccess } = defineProps<{
|
||||
@@ -37,7 +37,7 @@ const onSubmit = async (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
loading.value = true
|
||||
try {
|
||||
await authActions.updatePassword(event.values.password)
|
||||
await authService.updatePassword(event.values.password)
|
||||
onSuccess()
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -41,9 +41,9 @@ import ProgressSpinner from 'primevue/progressspinner'
|
||||
import Tag from 'primevue/tag'
|
||||
import { onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
|
||||
const {
|
||||
amount,
|
||||
@@ -61,7 +61,7 @@ const loading = ref(false)
|
||||
|
||||
const handleBuyNow = async () => {
|
||||
loading.value = true
|
||||
await authActions.purchaseCredits(editable ? customAmount.value : amount)
|
||||
await authService.purchaseCredits(editable ? customAmount.value : amount)
|
||||
loading.value = false
|
||||
didClickBuyNow.value = true
|
||||
}
|
||||
@@ -69,7 +69,7 @@ const handleBuyNow = async () => {
|
||||
onBeforeUnmount(() => {
|
||||
if (didClickBuyNow.value) {
|
||||
// If clicked buy now, then returned back to the dialog and closed, fetch the balance
|
||||
void authActions.fetchBalance()
|
||||
void authService.fetchBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
/>
|
||||
<div v-else class="h-full" @click="handleGridContainerClick">
|
||||
<VirtualGrid
|
||||
id="results-grid"
|
||||
:items="resultsWithKeys"
|
||||
:buffer-rows="3"
|
||||
:grid-style="GRID_STYLE"
|
||||
@@ -93,7 +92,7 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { merge } from 'lodash'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
@@ -200,10 +199,6 @@ const {
|
||||
const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
|
||||
packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id))
|
||||
|
||||
whenever(selectedTab, () => {
|
||||
pageNumber.value = 0
|
||||
})
|
||||
|
||||
const isUpdateAvailableTab = computed(
|
||||
() => selectedTab.value?.id === ManagerTab.UpdateAvailable
|
||||
)
|
||||
@@ -424,17 +419,6 @@ whenever(selectedNodePack, async () => {
|
||||
}
|
||||
})
|
||||
|
||||
let gridContainer: HTMLElement | null = null
|
||||
onMounted(() => {
|
||||
gridContainer = document.getElementById('results-grid')
|
||||
})
|
||||
watch(searchQuery, () => {
|
||||
gridContainer ??= document.getElementById('results-grid')
|
||||
if (gridContainer) {
|
||||
gridContainer.scrollTop = 0
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
getPackById.cancel()
|
||||
})
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
style: 'display: none'
|
||||
}
|
||||
}"
|
||||
:show-empty-message="false"
|
||||
@complete="stubTrue"
|
||||
@option-select="onOptionSelect"
|
||||
/>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
text
|
||||
size="small"
|
||||
severity="secondary"
|
||||
@click="() => authActions.fetchBalance()"
|
||||
@click="() => authService.fetchBalance()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,8 +112,8 @@ import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
@@ -127,7 +127,7 @@ interface CreditHistoryItemData {
|
||||
const { t } = useI18n()
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
const loading = computed(() => authStore.loading)
|
||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||
|
||||
@@ -142,7 +142,7 @@ const handlePurchaseCreditsClick = () => {
|
||||
}
|
||||
|
||||
const handleCreditsHistoryClick = async () => {
|
||||
await authActions.accessBillingPortal()
|
||||
await authService.accessBillingPortal()
|
||||
}
|
||||
|
||||
const handleMessageSupport = () => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tag from 'primevue/tag'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
@@ -21,16 +19,7 @@ describe('SettingItem', () => {
|
||||
const mountComponent = (props: any, options = {}): any => {
|
||||
return mount(SettingItem, {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n, createPinia()],
|
||||
components: {
|
||||
Tag
|
||||
},
|
||||
directives: {
|
||||
tooltip: Tooltip
|
||||
},
|
||||
stubs: {
|
||||
'i-material-symbols:experiment-outline': true
|
||||
}
|
||||
plugins: [PrimeVue, i18n, createPinia()]
|
||||
},
|
||||
props,
|
||||
...options
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
<h2 class="text-2xl font-bold mb-2">{{ $t('userSettings.title') }}</h2>
|
||||
<Divider class="mb-3" />
|
||||
|
||||
<!-- Normal User Panel -->
|
||||
<div v-if="isLoggedIn" class="flex flex-col gap-2">
|
||||
<div v-if="user" class="flex flex-col gap-2">
|
||||
<UserAvatar
|
||||
v-if="userPhotoUrl"
|
||||
:photo-url="userPhotoUrl"
|
||||
v-if="user.photoURL"
|
||||
:photo-url="user.photoURL"
|
||||
shape="circle"
|
||||
size="large"
|
||||
/>
|
||||
@@ -18,7 +17,7 @@
|
||||
{{ $t('userSettings.name') }}
|
||||
</h3>
|
||||
<div class="text-muted">
|
||||
{{ userDisplayName || $t('userSettings.notSet') }}
|
||||
{{ user.displayName || $t('userSettings.notSet') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,9 +25,9 @@
|
||||
<h3 class="font-medium">
|
||||
{{ $t('userSettings.email') }}
|
||||
</h3>
|
||||
<span class="text-muted">
|
||||
{{ userEmail }}
|
||||
</span>
|
||||
<a :href="'mailto:' + user.email" class="hover:underline">
|
||||
{{ user.email }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
@@ -91,22 +90,51 @@ import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const dialogService = useDialogService()
|
||||
const {
|
||||
loading,
|
||||
isLoggedIn,
|
||||
isEmailProvider,
|
||||
userDisplayName,
|
||||
userEmail,
|
||||
userPhotoUrl,
|
||||
providerName,
|
||||
providerIcon,
|
||||
handleSignOut,
|
||||
handleSignIn
|
||||
} = useCurrentUser()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const commandStore = useCommandStore()
|
||||
const user = computed(() => authStore.currentUser)
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const providerName = computed(() => {
|
||||
const providerId = user.value?.providerData[0]?.providerId
|
||||
if (providerId?.includes('google')) {
|
||||
return 'Google'
|
||||
}
|
||||
if (providerId?.includes('github')) {
|
||||
return 'GitHub'
|
||||
}
|
||||
return providerId
|
||||
})
|
||||
|
||||
const providerIcon = computed(() => {
|
||||
const providerId = user.value?.providerData[0]?.providerId
|
||||
if (providerId?.includes('google')) {
|
||||
return 'pi pi-google'
|
||||
}
|
||||
if (providerId?.includes('github')) {
|
||||
return 'pi pi-github'
|
||||
}
|
||||
return 'pi pi-user'
|
||||
})
|
||||
|
||||
const isEmailProvider = computed(() => {
|
||||
const providerId = user.value?.providerData[0]?.providerId
|
||||
return providerId === 'password'
|
||||
})
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await commandStore.execute('Comfy.User.SignOut')
|
||||
}
|
||||
|
||||
const handleSignIn = async () => {
|
||||
await commandStore.execute('Comfy.User.OpenSignInDialog')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import { Form } from '@primevue/forms'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
|
||||
|
||||
import ApiKeyForm from './ApiKeyForm.vue'
|
||||
|
||||
const mockStoreApiKey = vi.fn()
|
||||
const mockLoading = vi.fn(() => false)
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
loading: mockLoading()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/apiKeyAuthStore', () => ({
|
||||
useApiKeyAuthStore: vi.fn(() => ({
|
||||
storeApiKey: mockStoreApiKey
|
||||
}))
|
||||
}))
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
auth: {
|
||||
apiKey: {
|
||||
title: 'API Key',
|
||||
label: 'API Key',
|
||||
placeholder: 'Enter your API Key',
|
||||
error: 'Invalid API Key',
|
||||
helpText: 'Need an API key?',
|
||||
generateKey: 'Get one here',
|
||||
whitelistInfo: 'About non-whitelisted sites',
|
||||
description: 'Use your Comfy API key to enable API Nodes'
|
||||
}
|
||||
},
|
||||
g: {
|
||||
back: 'Back',
|
||||
save: 'Save',
|
||||
learnMore: 'Learn more'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('ApiKeyForm', () => {
|
||||
beforeEach(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
vi.clearAllMocks()
|
||||
mockStoreApiKey.mockReset()
|
||||
mockLoading.mockReset()
|
||||
})
|
||||
|
||||
const mountComponent = (props: any = {}) => {
|
||||
return mount(ApiKeyForm, {
|
||||
global: {
|
||||
plugins: [PrimeVue, createPinia(), i18n],
|
||||
components: { Button, Form, InputText, Message }
|
||||
},
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
it('renders correctly with all required elements', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.find('h1').text()).toBe('API Key')
|
||||
expect(wrapper.find('label').text()).toBe('API Key')
|
||||
expect(wrapper.findComponent(InputText).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(Button).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('emits back event when back button is clicked', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
await wrapper.findComponent(Button).trigger('click')
|
||||
expect(wrapper.emitted('back')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows loading state when submitting', async () => {
|
||||
mockLoading.mockReturnValue(true)
|
||||
const wrapper = mountComponent()
|
||||
const input = wrapper.findComponent(InputText)
|
||||
|
||||
await input.setValue(
|
||||
'comfyui-123456789012345678901234567890123456789012345678901234567890123456789012'
|
||||
)
|
||||
await wrapper.find('form').trigger('submit')
|
||||
|
||||
const submitButton = wrapper
|
||||
.findAllComponents(Button)
|
||||
.find((btn) => btn.text() === 'Save')
|
||||
expect(submitButton?.props('loading')).toBe(true)
|
||||
})
|
||||
|
||||
it('displays help text and links correctly', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const helpText = wrapper.find('small')
|
||||
expect(helpText.text()).toContain('Need an API key?')
|
||||
expect(helpText.find('a').attributes('href')).toBe(
|
||||
`${COMFY_PLATFORM_BASE_URL}/login`
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,112 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-4 mb-8">
|
||||
<h1 class="text-2xl font-medium leading-normal my-0">
|
||||
{{ t('auth.apiKey.title') }}
|
||||
</h1>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-base my-0 text-muted">
|
||||
{{ t('auth.apiKey.description') }}
|
||||
</p>
|
||||
<a
|
||||
href="https://docs.comfy.org/interface/user#logging-in-with-an-api-key"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('g.learnMore') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
v-slot="$form"
|
||||
class="flex flex-col gap-6"
|
||||
:resolver="zodResolver(apiKeySchema)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<Message v-if="$form.apiKey?.invalid" severity="error" class="mb-4">
|
||||
{{ $form.apiKey.error.message }}
|
||||
</Message>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium mb-2"
|
||||
for="comfy-org-api-key"
|
||||
>
|
||||
{{ t('auth.apiKey.label') }}
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<InputText
|
||||
pt:root:id="comfy-org-api-key"
|
||||
pt:root:autocomplete="off"
|
||||
class="h-10"
|
||||
name="apiKey"
|
||||
type="password"
|
||||
:placeholder="t('auth.apiKey.placeholder')"
|
||||
:invalid="$form.apiKey?.invalid"
|
||||
/>
|
||||
<small class="text-muted">
|
||||
{{ t('auth.apiKey.helpText') }}
|
||||
<a
|
||||
:href="`${COMFY_PLATFORM_BASE_URL}/login`"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.apiKey.generateKey') }}
|
||||
</a>
|
||||
<span class="mx-1">•</span>
|
||||
<a
|
||||
href="https://docs.comfy.org/tutorials/api-nodes/overview#log-in-with-api-key-on-non-whitelisted-websites"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.apiKey.whitelistInfo') }}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<Button type="button" link @click="$emit('back')">
|
||||
{{ t('g.back') }}
|
||||
</Button>
|
||||
<Button type="submit" :loading="loading" :disabled="loading">
|
||||
{{ t('g.save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
|
||||
import { apiKeySchema } from '@/schemas/signInSchema'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const apiKeyStore = useApiKeyAuthStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'back'): void
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const onSubmit = async (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
await apiKeyStore.storeApiKey(event.values.apiKey)
|
||||
emit('success')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -80,12 +80,12 @@ import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { type SignInData, signInSchema } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const firebaseAuthActions = useFirebaseAuthActions()
|
||||
const firebaseAuthService = useFirebaseAuthService()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -102,6 +102,6 @@ const onSubmit = (event: FormSubmitEvent) => {
|
||||
|
||||
const handleForgotPassword = async (email: string) => {
|
||||
if (!email) return
|
||||
await firebaseAuthActions.sendPasswordReset(email)
|
||||
await firebaseAuthService.sendPasswordReset(email)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -12,17 +12,16 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import DomWidget from '@/components/graph/widgets/DomWidget.vue'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { type DomWidgetState, useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
const widgetStates = computed(() =>
|
||||
Array.from(domWidgetStore.widgetStates.values())
|
||||
const widgetStates = computed(
|
||||
() => Array.from(domWidgetStore.widgetStates.values()) as DomWidgetState[]
|
||||
)
|
||||
|
||||
const updateWidgets = () => {
|
||||
@@ -55,13 +54,18 @@ const updateWidgets = () => {
|
||||
}
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
whenever(
|
||||
watch(
|
||||
() => canvasStore.canvas,
|
||||
(canvas) =>
|
||||
(canvas.onDrawForeground = useChainCallback(
|
||||
canvas.onDrawForeground,
|
||||
updateWidgets
|
||||
)),
|
||||
(lgCanvas) => {
|
||||
if (!lgCanvas) return
|
||||
|
||||
lgCanvas.onDrawForeground = useChainCallback(
|
||||
lgCanvas.onDrawForeground,
|
||||
() => {
|
||||
updateWidgets()
|
||||
}
|
||||
)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
class="w-full h-full touch-none"
|
||||
/>
|
||||
|
||||
<NodeBadge />
|
||||
<NodeTooltip v-if="tooltipEnabled" />
|
||||
<NodeSearchboxPopover />
|
||||
|
||||
@@ -52,6 +53,7 @@ import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import DomWidgets from '@/components/graph/DomWidgets.vue'
|
||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
import NodeBadge from '@/components/graph/NodeBadge.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
|
||||
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
||||
@@ -60,7 +62,6 @@ import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vu
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useNodeBadge } from '@/composables/node/useNodeBadge'
|
||||
import { useCanvasDrop } from '@/composables/useCanvasDrop'
|
||||
import { useContextMenuTranslation } from '@/composables/useContextMenuTranslation'
|
||||
import { useCopy } from '@/composables/useCopy'
|
||||
@@ -70,7 +71,7 @@ import { usePaste } from '@/composables/usePaste'
|
||||
import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import { i18n } from '@/i18n'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { UnauthorizedError, api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
@@ -224,13 +225,39 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// Save the drag & scale info in the serialized workflow if the setting is enabled
|
||||
watch(
|
||||
[
|
||||
() => canvasStore.canvas,
|
||||
() => settingStore.get('Comfy.EnableWorkflowViewRestore')
|
||||
],
|
||||
([canvas, enableWorkflowViewRestore]) => {
|
||||
const extra = canvas?.graph?.extra
|
||||
if (!extra) return
|
||||
|
||||
if (enableWorkflowViewRestore) {
|
||||
extra.ds = {
|
||||
get scale() {
|
||||
return canvas.ds.scale
|
||||
},
|
||||
get offset() {
|
||||
const [x, y] = canvas.ds.offset
|
||||
return [x, y]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
delete extra.ds
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
useEventListener(
|
||||
canvasRef,
|
||||
'litegraph:no-items-selected',
|
||||
() => {
|
||||
toastStore.add({
|
||||
severity: 'warn',
|
||||
summary: t('toastMessages.nothingSelected'),
|
||||
summary: 'No items selected',
|
||||
life: 2000
|
||||
})
|
||||
},
|
||||
@@ -253,7 +280,6 @@ const workflowPersistence = useWorkflowPersistence()
|
||||
// @ts-expect-error fixme ts strict error
|
||||
useCanvasDrop(canvasRef)
|
||||
useLitegraphSettings()
|
||||
useNodeBadge()
|
||||
|
||||
onMounted(async () => {
|
||||
useGlobalLitegraph()
|
||||
@@ -267,7 +293,7 @@ onMounted(async () => {
|
||||
workspaceStore.spinner = true
|
||||
// ChangeTracker needs to be initialized before setup, as it will overwrite
|
||||
// some listeners of litegraph canvas.
|
||||
ChangeTracker.init()
|
||||
ChangeTracker.init(comfyApp)
|
||||
await loadCustomNodesI18n()
|
||||
try {
|
||||
await settingStore.loadSettingValues()
|
||||
@@ -292,8 +318,10 @@ onMounted(async () => {
|
||||
canvasStore.canvas.render_canvas_border = false
|
||||
workspaceStore.spinner = false
|
||||
|
||||
window.app = comfyApp
|
||||
window.graph = comfyApp.graph
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['app'] = comfyApp
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['graph'] = comfyApp.graph
|
||||
|
||||
comfyAppReady.value = true
|
||||
|
||||
|
||||
114
src/components/graph/NodeBadge.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- This component does not render anything visible. -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BadgePosition,
|
||||
LGraphBadge,
|
||||
type LGraphNode
|
||||
} from '@comfyorg/litegraph'
|
||||
import _ from 'lodash'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const nodeSourceBadgeMode = computed(
|
||||
() => settingStore.get('Comfy.NodeBadge.NodeSourceBadgeMode') as NodeBadgeMode
|
||||
)
|
||||
const nodeIdBadgeMode = computed(
|
||||
() => settingStore.get('Comfy.NodeBadge.NodeIdBadgeMode') as NodeBadgeMode
|
||||
)
|
||||
const nodeLifeCycleBadgeMode = computed(
|
||||
() =>
|
||||
settingStore.get('Comfy.NodeBadge.NodeLifeCycleBadgeMode') as NodeBadgeMode
|
||||
)
|
||||
|
||||
watch([nodeSourceBadgeMode, nodeIdBadgeMode, nodeLifeCycleBadgeMode], () => {
|
||||
app.graph?.setDirtyCanvas(true, true)
|
||||
})
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
function badgeTextVisible(
|
||||
nodeDef: ComfyNodeDefImpl | null,
|
||||
badgeMode: NodeBadgeMode
|
||||
): boolean {
|
||||
return !(
|
||||
badgeMode === NodeBadgeMode.None ||
|
||||
(nodeDef?.isCoreNode && badgeMode === NodeBadgeMode.HideBuiltIn)
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
app.registerExtension({
|
||||
name: 'Comfy.NodeBadge',
|
||||
nodeCreated(node: LGraphNode) {
|
||||
node.badgePosition = BadgePosition.TopRight
|
||||
|
||||
const badge = computed(() => {
|
||||
const nodeDef = nodeDefStore.fromLGraphNode(node)
|
||||
return new LGraphBadge({
|
||||
text: _.truncate(
|
||||
[
|
||||
badgeTextVisible(nodeDef, nodeIdBadgeMode.value)
|
||||
? `#${node.id}`
|
||||
: '',
|
||||
badgeTextVisible(nodeDef, nodeLifeCycleBadgeMode.value)
|
||||
? nodeDef?.nodeLifeCycleBadgeText ?? ''
|
||||
: '',
|
||||
badgeTextVisible(nodeDef, nodeSourceBadgeMode.value)
|
||||
? nodeDef?.nodeSource?.badgeText ?? ''
|
||||
: ''
|
||||
]
|
||||
.filter((s) => s.length > 0)
|
||||
.join(' '),
|
||||
{
|
||||
length: 31
|
||||
}
|
||||
),
|
||||
fgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_FG_COLOR,
|
||||
bgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_BG_COLOR
|
||||
})
|
||||
})
|
||||
|
||||
node.badges.push(() => badge.value)
|
||||
|
||||
if (node.constructor.nodeData?.api_node) {
|
||||
const creditsBadge = computed(() => {
|
||||
return new LGraphBadge({
|
||||
text: '',
|
||||
iconOptions: {
|
||||
unicode: '\ue96b',
|
||||
fontFamily: 'PrimeIcons',
|
||||
color: '#FABC25',
|
||||
bgColor: '#353535',
|
||||
fontSize: 8
|
||||
},
|
||||
fgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_FG_COLOR,
|
||||
bgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_BG_COLOR
|
||||
})
|
||||
})
|
||||
|
||||
node.badges.push(() => creditsBadge.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -14,10 +14,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { createBounds } from '@comfyorg/litegraph'
|
||||
import type { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
@@ -26,8 +28,8 @@ const { style, updatePosition } = useAbsolutePosition()
|
||||
const visible = ref(false)
|
||||
const showBorder = ref(false)
|
||||
|
||||
const positionSelectionOverlay = () => {
|
||||
const { selectedItems } = canvasStore.getCanvas()
|
||||
const positionSelectionOverlay = (canvas: LGraphCanvas) => {
|
||||
const selectedItems = canvas.selectedItems
|
||||
showBorder.value = selectedItems.size > 1
|
||||
|
||||
if (!selectedItems.size) {
|
||||
@@ -46,18 +48,26 @@ const positionSelectionOverlay = () => {
|
||||
}
|
||||
|
||||
// Register listener on canvas creation.
|
||||
whenever(
|
||||
() => canvasStore.getCanvas().state.selectionChanged,
|
||||
() => {
|
||||
requestAnimationFrame(() => {
|
||||
positionSelectionOverlay()
|
||||
canvasStore.getCanvas().state.selectionChanged = false
|
||||
})
|
||||
watch(
|
||||
() => canvasStore.canvas as LGraphCanvas | null,
|
||||
(canvas: LGraphCanvas | null) => {
|
||||
if (!canvas) return
|
||||
|
||||
canvas.onSelectionChange = useChainCallback(
|
||||
canvas.onSelectionChange,
|
||||
// Wait for next frame as sometimes the selected items haven't been
|
||||
// rendered yet, so the boundingRect is not available on them.
|
||||
() => requestAnimationFrame(() => positionSelectionOverlay(canvas))
|
||||
)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
canvasStore.getCanvas().ds.onChanged = positionSelectionOverlay
|
||||
whenever(
|
||||
() => canvasStore.getCanvas().ds.state,
|
||||
() => positionSelectionOverlay(canvasStore.getCanvas()),
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => canvasStore.canvas?.state?.draggingItems,
|
||||
@@ -67,10 +77,10 @@ watch(
|
||||
// the correct position.
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/2656
|
||||
if (draggingItems === false) {
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
visible.value = true
|
||||
positionSelectionOverlay()
|
||||
})
|
||||
positionSelectionOverlay(canvasStore.canvas as LGraphCanvas)
|
||||
}, 100)
|
||||
} else {
|
||||
// Selection change update to visible state is delayed by a frame. Here
|
||||
// we also delay a frame so that the order of events is correct when
|
||||
|
||||
@@ -6,41 +6,94 @@
|
||||
content: 'p-0 flex flex-row'
|
||||
}"
|
||||
>
|
||||
<ExecuteButton />
|
||||
<ColorPickerButton />
|
||||
<BypassButton />
|
||||
<PinButton />
|
||||
<DeleteButton />
|
||||
<RefreshButton />
|
||||
<ExtensionCommandButton
|
||||
<ColorPickerButton v-show="nodeSelected || groupSelected" />
|
||||
<Button
|
||||
v-show="nodeSelected"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Bypass.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
data-testid="bypass-button"
|
||||
@click="
|
||||
() => commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<i-game-icons:detour />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
v-show="nodeSelected || groupSelected"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Pin.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
icon="pi pi-thumbtack"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="danger"
|
||||
text
|
||||
icon="pi pi-trash"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
|
||||
/>
|
||||
<Button
|
||||
v-show="isRefreshable"
|
||||
severity="info"
|
||||
text
|
||||
icon="pi pi-refresh"
|
||||
@click="refreshSelected"
|
||||
/>
|
||||
<Button
|
||||
v-for="command in extensionToolboxCommands"
|
||||
:key="command.id"
|
||||
:command="command"
|
||||
v-tooltip.top="{
|
||||
value:
|
||||
st(`commands.${normalizeI18nKey(command.id)}.label`, '') || undefined,
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
:icon="typeof command.icon === 'function' ? command.icon() : command.icon"
|
||||
@click="() => commandStore.execute(command.id)"
|
||||
/>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Panel from 'primevue/panel'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
||||
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
|
||||
import { useRefreshableSelection } from '@/composables/useRefreshableSelection'
|
||||
import { st, t } from '@/i18n'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
||||
import { ComfyCommand, useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
import BypassButton from './selectionToolbox/BypassButton.vue'
|
||||
import DeleteButton from './selectionToolbox/DeleteButton.vue'
|
||||
import ExtensionCommandButton from './selectionToolbox/ExtensionCommandButton.vue'
|
||||
import PinButton from './selectionToolbox/PinButton.vue'
|
||||
import RefreshButton from './selectionToolbox/RefreshButton.vue'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const extensionService = useExtensionService()
|
||||
const { isRefreshable, refreshSelected } = useRefreshableSelection()
|
||||
const nodeSelected = computed(() =>
|
||||
canvasStore.selectedItems.some(isLGraphNode)
|
||||
)
|
||||
const groupSelected = computed(() =>
|
||||
canvasStore.selectedItems.some(isLGraphGroup)
|
||||
)
|
||||
|
||||
const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
||||
const extensionToolboxCommands = computed<ComfyCommand[]>(() => {
|
||||
const commandIds = new Set<string>(
|
||||
canvasStore.selectedItems
|
||||
.map(
|
||||
@@ -53,7 +106,7 @@ const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
||||
)
|
||||
return Array.from(commandIds)
|
||||
.map((commandId) => commandStore.getCommand(commandId))
|
||||
.filter((command): command is ComfyCommandImpl => command !== undefined)
|
||||
.filter((command) => command !== undefined)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="canvasStore.nodeSelected"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Bypass.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
data-testid="bypass-button"
|
||||
@click="
|
||||
() => commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<i-game-icons:detour />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
</script>
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<Button
|
||||
v-show="canvasStore.nodeSelected || canvasStore.groupSelected"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="() => (showColorPicker = !showColorPicker)"
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="danger"
|
||||
text
|
||||
icon="pi pi-trash"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
</script>
|
||||
@@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="canvasStore.nodeSelected"
|
||||
v-tooltip.top="{
|
||||
value: isDisabled
|
||||
? t('selectionToolbox.executeButton.disabledTooltip')
|
||||
: t('selectionToolbox.executeButton.tooltip'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
:severity="isDisabled ? 'secondary' : 'success'"
|
||||
text
|
||||
:disabled="isDisabled"
|
||||
@mouseenter="() => handleMouseEnter()"
|
||||
@mouseleave="() => handleMouseLeave()"
|
||||
@click="handleClick"
|
||||
>
|
||||
<i-lucide:play />
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const buttonHovered = ref(false)
|
||||
const selectedOutputNodes = computed(
|
||||
() =>
|
||||
canvasStore.selectedItems.filter(
|
||||
(item) => isLGraphNode(item) && item.constructor.nodeData?.output_node
|
||||
) as LGraphNode[]
|
||||
)
|
||||
|
||||
const isDisabled = computed(() => selectedOutputNodes.value.length === 0)
|
||||
|
||||
function outputNodeStokeStyle(this: LGraphNode) {
|
||||
if (
|
||||
this.selected &&
|
||||
this.constructor.nodeData?.output_node &&
|
||||
buttonHovered.value
|
||||
) {
|
||||
return { color: 'orange', lineWidth: 2, padding: 10 }
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
buttonHovered.value = true
|
||||
for (const node of selectedOutputNodes.value) {
|
||||
node.strokeStyles['outputNode'] = outputNodeStokeStyle
|
||||
}
|
||||
canvas.setDirty(true)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
buttonHovered.value = false
|
||||
canvas.setDirty(true)
|
||||
}
|
||||
|
||||
const handleClick = async () => {
|
||||
await commandStore.execute('Comfy.QueueSelectedOutputNodes')
|
||||
}
|
||||
</script>
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value:
|
||||
st(`commands.${normalizeI18nKey(command.id)}.label`, '') || undefined,
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
:icon="typeof command.icon === 'function' ? command.icon() : command.icon"
|
||||
@click="() => commandStore.execute(command.id)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import { ComfyCommand, useCommandStore } from '@/stores/commandStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
defineProps<{
|
||||
command: ComfyCommand
|
||||
}>()
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
</script>
|
||||
@@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="canvasStore.nodeSelected || canvasStore.groupSelected"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Pin.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
icon="pi pi-thumbtack"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
</script>
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="isRefreshable"
|
||||
severity="info"
|
||||
text
|
||||
icon="pi pi-refresh"
|
||||
@click="refreshSelected"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useRefreshableSelection } from '@/composables/useRefreshableSelection'
|
||||
|
||||
const { isRefreshable, refreshSelected } = useRefreshableSelection()
|
||||
</script>
|
||||
@@ -1,135 +0,0 @@
|
||||
<template>
|
||||
<ScrollPanel
|
||||
ref="scrollPanelRef"
|
||||
class="w-full min-h-[400px] rounded-lg px-2 py-2 text-xs"
|
||||
:pt="{ content: { id: 'chat-scroll-content' } }"
|
||||
>
|
||||
<div v-for="(item, i) in parsedHistory" :key="i" class="mb-4">
|
||||
<!-- Prompt (user, right) -->
|
||||
<span
|
||||
:class="{
|
||||
'opacity-40 pointer-events-none': editIndex !== null && i > editIndex
|
||||
}"
|
||||
>
|
||||
<div class="flex justify-end mb-1">
|
||||
<div
|
||||
class="bg-gray-300 dark-theme:bg-gray-800 rounded-xl px-4 py-1 max-w-[80%] text-right"
|
||||
>
|
||||
<div class="break-words text-[12px]">{{ item.prompt }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end mb-2 mr-1">
|
||||
<CopyButton :text="item.prompt" />
|
||||
<Button
|
||||
v-tooltip="
|
||||
editIndex === i ? $t('chatHistory.cancelEditTooltip') : null
|
||||
"
|
||||
text
|
||||
rounded
|
||||
class="!p-1 !h-4 !w-4 text-gray-400 hover:text-gray-600 dark-theme:hover:text-gray-200 transition"
|
||||
pt:icon:class="!text-xs"
|
||||
:icon="editIndex === i ? 'pi pi-times' : 'pi pi-pencil'"
|
||||
:aria-label="
|
||||
editIndex === i ? $t('chatHistory.cancelEdit') : $t('g.edit')
|
||||
"
|
||||
@click="editIndex === i ? handleCancelEdit() : handleEdit(i)"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
<!-- Response (LLM, left) -->
|
||||
<ResponseBlurb
|
||||
:text="item.response"
|
||||
:class="{
|
||||
'opacity-25 pointer-events-none': editIndex !== null && i >= editIndex
|
||||
}"
|
||||
>
|
||||
<div v-html="nl2br(linkifyHtml(item.response))" />
|
||||
</ResponseBlurb>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import CopyButton from '@/components/graph/widgets/chatHistory/CopyButton.vue'
|
||||
import ResponseBlurb from '@/components/graph/widgets/chatHistory/ResponseBlurb.vue'
|
||||
import { ComponentWidget } from '@/scripts/domWidget'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const { widget, history = '[]' } = defineProps<{
|
||||
widget?: ComponentWidget<string>
|
||||
history: string
|
||||
}>()
|
||||
|
||||
const editIndex = ref<number | null>(null)
|
||||
const scrollPanelRef = ref<InstanceType<typeof ScrollPanel> | null>(null)
|
||||
|
||||
const parsedHistory = computed(() => JSON.parse(history || '[]'))
|
||||
|
||||
const findPromptInput = () =>
|
||||
widget?.node.widgets?.find((w) => w.name === 'prompt')
|
||||
let promptInput = findPromptInput()
|
||||
const previousPromptInput = ref<string | null>(null)
|
||||
|
||||
const getPreviousResponseId = (index: number) =>
|
||||
index > 0 ? parsedHistory.value[index - 1]?.response_id ?? '' : ''
|
||||
|
||||
const storePromptInput = () => {
|
||||
promptInput ??= widget?.node.widgets?.find((w) => w.name === 'prompt')
|
||||
if (!promptInput) return
|
||||
|
||||
previousPromptInput.value = String(promptInput.value)
|
||||
}
|
||||
|
||||
const setPromptInput = (text: string, previousResponseId?: string | null) => {
|
||||
promptInput ??= widget?.node.widgets?.find((w) => w.name === 'prompt')
|
||||
if (!promptInput) return
|
||||
|
||||
if (previousResponseId !== null) {
|
||||
promptInput.value = `<starting_point_id:${previousResponseId}>\n\n${text}`
|
||||
} else {
|
||||
promptInput.value = text
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = (index: number) => {
|
||||
if (!promptInput) return
|
||||
|
||||
editIndex.value = index
|
||||
const prevResponseId = index === 0 ? 'start' : getPreviousResponseId(index)
|
||||
const promptText = parsedHistory.value[index]?.prompt ?? ''
|
||||
|
||||
storePromptInput()
|
||||
setPromptInput(promptText, prevResponseId)
|
||||
}
|
||||
|
||||
const resetEditingState = () => {
|
||||
editIndex.value = null
|
||||
}
|
||||
const handleCancelEdit = () => {
|
||||
resetEditingState()
|
||||
if (promptInput) {
|
||||
promptInput.value = previousPromptInput.value ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
const scrollChatToBottom = () => {
|
||||
const content = document.getElementById('chat-scroll-content')
|
||||
if (content) {
|
||||
content.scrollTo({ top: content.scrollHeight, behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
|
||||
const onHistoryChanged = () => {
|
||||
resetEditingState()
|
||||
void nextTick(() => scrollChatToBottom())
|
||||
}
|
||||
|
||||
watch(() => parsedHistory.value, onHistoryChanged, {
|
||||
immediate: true,
|
||||
deep: true
|
||||
})
|
||||
</script>
|
||||
@@ -11,7 +11,6 @@
|
||||
v-if="isComponentWidget(widget)"
|
||||
:model-value="widget.value"
|
||||
:widget="widget"
|
||||
v-bind="widget.props"
|
||||
@update:model-value="emit('update:widgetValue', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative w-full text-xs min-h-[28px] max-h-[200px] rounded-lg px-4 py-2 overflow-y-auto"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 break-all flex items-center gap-2">
|
||||
<span v-html="formattedText"></span>
|
||||
<Skeleton v-if="isParentNodeExecuting" class="!flex-1 !h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NodeId } from '@comfyorg/litegraph'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
defineProps<{
|
||||
widget?: object
|
||||
}>()
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const isParentNodeExecuting = ref(true)
|
||||
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
|
||||
|
||||
let executingNodeId: NodeId | null = null
|
||||
onMounted(() => {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
})
|
||||
|
||||
// Watch for either a new node has starting execution or overall execution ending
|
||||
const stopWatching = watch(
|
||||
[() => executionStore.executingNode, () => executionStore.isIdle],
|
||||
() => {
|
||||
if (
|
||||
executionStore.isIdle ||
|
||||
(executionStore.executingNode &&
|
||||
executionStore.executingNode.id !== executingNodeId)
|
||||
) {
|
||||
isParentNodeExecuting.value = false
|
||||
stopWatching()
|
||||
}
|
||||
if (!executingNodeId) {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -1,36 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip="
|
||||
copied ? $t('chatHistory.copiedTooltip') : $t('chatHistory.copyTooltip')
|
||||
"
|
||||
text
|
||||
rounded
|
||||
class="!p-1 !h-4 !w-6 text-gray-400 hover:text-gray-600 dark-theme:hover:text-gray-200 transition"
|
||||
pt:icon:class="!text-xs"
|
||||
:icon="copied ? 'pi pi-check' : 'pi pi-copy'"
|
||||
:aria-label="
|
||||
copied ? $t('chatHistory.copiedTooltip') : $t('chatHistory.copyTooltip')
|
||||
"
|
||||
@click="handleCopy"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const { text } = defineProps<{
|
||||
text: string
|
||||
}>()
|
||||
|
||||
const copied = ref(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!text) return
|
||||
await navigator.clipboard.writeText(text)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 1024)
|
||||
}
|
||||
</script>
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<span>
|
||||
<div class="flex justify-start mb-1">
|
||||
<div class="rounded-xl px-4 py-1 max-w-[80%]">
|
||||
<div class="break-words text-[12px]">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-start ml-1">
|
||||
<CopyButton :text="text" />
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CopyButton from '@/components/graph/widgets/chatHistory/CopyButton.vue'
|
||||
|
||||
defineProps<{
|
||||
text: string
|
||||
}>()
|
||||
</script>
|
||||
@@ -52,9 +52,6 @@ const eventConfig = {
|
||||
showPreviewChange: (value: boolean) => emit('showPreviewChange', value),
|
||||
backgroundImageChange: (value: string) =>
|
||||
emit('backgroundImageChange', value),
|
||||
backgroundImageLoadingStart: () =>
|
||||
loadingOverlayRef.value?.startLoading(t('load3d.loadingBackgroundImage')),
|
||||
backgroundImageLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
|
||||
upDirectionChange: (value: string) => emit('upDirectionChange', value),
|
||||
edgeThresholdChange: (value: number) => emit('edgeThresholdChange', value),
|
||||
modelLoadingStart: () =>
|
||||
@@ -78,7 +75,7 @@ const eventConfig = {
|
||||
|
||||
watchEffect(async () => {
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
const rawLoad3d = toRaw(load3d.value)
|
||||
|
||||
rawLoad3d.setBackgroundColor(props.backgroundColor)
|
||||
rawLoad3d.toggleGrid(props.showGrid)
|
||||
@@ -87,25 +84,15 @@ watchEffect(async () => {
|
||||
rawLoad3d.toggleCamera(props.cameraType)
|
||||
rawLoad3d.togglePreview(props.showPreview)
|
||||
await rawLoad3d.setBackgroundImage(props.backgroundImage)
|
||||
rawLoad3d.setUpDirection(props.upDirection)
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.upDirection,
|
||||
(newValue) => {
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
|
||||
rawLoad3d.setUpDirection(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.materialMode,
|
||||
(newValue) => {
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
const rawLoad3d = toRaw(load3d.value)
|
||||
|
||||
rawLoad3d.setMaterialMode(newValue)
|
||||
}
|
||||
@@ -115,9 +102,10 @@ watch(
|
||||
watch(
|
||||
() => props.edgeThreshold,
|
||||
(newValue) => {
|
||||
if (load3d.value && newValue) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value)
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
rawLoad3d.setEdgeThreshold(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { IWidget, LGraphNode } from '@comfyorg/litegraph'
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
@@ -101,13 +101,13 @@ const emit = defineEmits<{
|
||||
const resizeNodeMatchOutput = () => {
|
||||
console.log('resizeNodeMatchOutput')
|
||||
|
||||
const outputWidth = node.widgets?.find((w) => w.name === 'width')
|
||||
const outputHeight = node.widgets?.find((w) => w.name === 'height')
|
||||
const outputWidth = node.widgets?.find((w: IWidget) => w.name === 'width')
|
||||
const outputHeight = node.widgets?.find((w: IWidget) => w.name === 'height')
|
||||
|
||||
if (outputWidth && outputHeight && outputHeight.value && outputWidth.value) {
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
|
||||
const scene = node.widgets?.find((w) => w.name === 'image')
|
||||
const scene = node.widgets?.find((w: IWidget) => w.name === 'image')
|
||||
|
||||
const sceneHeight = scene?.computedHeight
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
@@ -17,16 +16,9 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const { isExtensionInstalled, isExtensionEnabled } = useExtensionStore()
|
||||
|
||||
const vhsAdvancedPreviews = computed(() => {
|
||||
return (
|
||||
isExtensionInstalled('VideoHelperSuite.Core') &&
|
||||
isExtensionEnabled('VideoHelperSuite.Core') &&
|
||||
settingStore.get('VHS.AdvancedPreviews') &&
|
||||
settingStore.get('VHS.AdvancedPreviews') !== 'Never'
|
||||
)
|
||||
})
|
||||
const vhsAdvancedPreviews = computed(() =>
|
||||
settingStore.get('VHS.AdvancedPreviews')
|
||||
)
|
||||
|
||||
const url = computed(() =>
|
||||
vhsAdvancedPreviews.value
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<Button
|
||||
v-if="isLoggedIn"
|
||||
v-if="isAuthenticated"
|
||||
class="user-profile-button p-1"
|
||||
severity="secondary"
|
||||
text
|
||||
@@ -30,14 +30,15 @@ import Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
import CurrentUserPopover from './CurrentUserPopover.vue'
|
||||
|
||||
const { isLoggedIn, userPhotoUrl } = useCurrentUser()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
const popover = ref<InstanceType<typeof Popover> | null>(null)
|
||||
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
||||
const photoURL = computed<string | undefined>(
|
||||
() => userPhotoUrl.value ?? undefined
|
||||
() => authStore.currentUser?.photoURL ?? undefined
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -6,19 +6,19 @@
|
||||
<div class="flex flex-col items-center">
|
||||
<UserAvatar
|
||||
class="mb-3"
|
||||
:photo-url="userPhotoUrl"
|
||||
:photo-url="user?.photoURL"
|
||||
:pt:icon:class="{
|
||||
'!text-2xl': !userPhotoUrl
|
||||
'!text-2xl': !user?.photoURL
|
||||
}"
|
||||
size="large"
|
||||
/>
|
||||
|
||||
<!-- User Details -->
|
||||
<h3 class="text-lg font-semibold truncate my-0 mb-1">
|
||||
{{ userDisplayName || $t('g.user') }}
|
||||
{{ user?.displayName || $t('g.user') }}
|
||||
</h3>
|
||||
<p v-if="userEmail" class="text-sm text-muted truncate my-0">
|
||||
{{ userEmail }}
|
||||
<p v-if="user?.email" class="text-sm text-muted truncate my-0">
|
||||
{{ user.email }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,18 +37,6 @@
|
||||
|
||||
<Divider class="my-2" />
|
||||
|
||||
<Button
|
||||
class="justify-start"
|
||||
:label="$t('credits.apiPricing')"
|
||||
icon="pi pi-external-link"
|
||||
text
|
||||
fluid
|
||||
severity="secondary"
|
||||
@click="handleOpenApiPricing"
|
||||
/>
|
||||
|
||||
<Divider class="my-2" />
|
||||
|
||||
<div class="w-full flex flex-col gap-2 p-2">
|
||||
<div class="text-muted text-sm">
|
||||
{{ $t('credits.yourCreditBalance') }}
|
||||
@@ -64,18 +52,20 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import { onMounted } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const { userDisplayName, userEmail, userPhotoUrl } = useCurrentUser()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authService = useFirebaseAuthService()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
const user = computed(() => authStore.currentUser)
|
||||
|
||||
const handleOpenUserSettings = () => {
|
||||
dialogService.showSettingsDialog('user')
|
||||
}
|
||||
@@ -84,11 +74,7 @@ const handleTopUp = () => {
|
||||
dialogService.showTopUpCreditsDialog()
|
||||
}
|
||||
|
||||
const handleOpenApiPricing = () => {
|
||||
window.open('https://docs.comfy.org/tutorials/api-nodes/pricing', '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void authActions.fetchBalance()
|
||||
void authService.fetchBalance()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
# Composables
|
||||
|
||||
This directory contains Vue composables for the ComfyUI frontend application. Composables are reusable pieces of logic that encapsulate stateful functionality and can be shared across components.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Composable Architecture](#composable-architecture)
|
||||
- [Composable Categories](#composable-categories)
|
||||
- [Usage Guidelines](#usage-guidelines)
|
||||
- [VueUse Library](#vueuse-library)
|
||||
- [Development Guidelines](#development-guidelines)
|
||||
- [Common Patterns](#common-patterns)
|
||||
|
||||
## Overview
|
||||
|
||||
Vue composables are a core part of Vue 3's Composition API and provide a way to extract and reuse stateful logic between multiple components. In ComfyUI, composables are used to encapsulate behaviors like:
|
||||
|
||||
- State management
|
||||
- DOM interactions
|
||||
- Feature-specific functionality
|
||||
- UI behaviors
|
||||
- Data fetching
|
||||
|
||||
Composables enable a more modular and functional approach to building components, allowing for better code reuse and separation of concerns. They help keep your component code cleaner by extracting complex logic into separate, reusable functions.
|
||||
|
||||
As described in the [Vue.js documentation](https://vuejs.org/guide/reusability/composables.html), composables are:
|
||||
> Functions that leverage Vue's Composition API to encapsulate and reuse stateful logic.
|
||||
|
||||
## Composable Architecture
|
||||
|
||||
The composable architecture in ComfyUI follows these principles:
|
||||
|
||||
1. **Single Responsibility**: Each composable should focus on a specific concern
|
||||
2. **Composition**: Composables can use other composables
|
||||
3. **Reactivity**: Composables leverage Vue's reactivity system
|
||||
4. **Reusability**: Composables are designed to be used across multiple components
|
||||
|
||||
The following diagram shows how composables fit into the application architecture:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Vue Components │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Component A │ │ Component B │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ │ │ │
|
||||
└────────────┼───────────────────┼────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────────┴───────────────────┴────────────────────────┐
|
||||
│ Composables │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ useFeatureA │ │ useFeatureB │ │ useFeatureC │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ │ │ │ │
|
||||
└─────────┼────────────────┼────────────────┼─────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────┴────────────────┴────────────────┴─────────────┐
|
||||
│ Services & Stores │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Composable Categories
|
||||
|
||||
ComfyUI's composables are organized into several categories:
|
||||
|
||||
### Auth
|
||||
|
||||
Composables for authentication and user management:
|
||||
- `useCurrentUser` - Provides access to the current user information
|
||||
- `useFirebaseAuthActions` - Handles Firebase authentication operations
|
||||
|
||||
### Element
|
||||
|
||||
Composables for DOM and element interactions:
|
||||
- `useAbsolutePosition` - Handles element positioning
|
||||
- `useDomClipping` - Manages clipping of DOM elements
|
||||
- `useResponsiveCollapse` - Manages responsive collapsing of elements
|
||||
|
||||
### Node
|
||||
|
||||
Composables for node-specific functionality:
|
||||
- `useNodeBadge` - Handles node badge display and interaction
|
||||
- `useNodeImage` - Manages node image preview
|
||||
- `useNodeDragAndDrop` - Handles drag and drop for nodes
|
||||
- `useNodeChatHistory` - Manages chat history for nodes
|
||||
|
||||
### Settings
|
||||
|
||||
Composables for settings management:
|
||||
- `useSettingSearch` - Provides search functionality for settings
|
||||
- `useSettingUI` - Manages settings UI interactions
|
||||
|
||||
### Sidebar
|
||||
|
||||
Composables for sidebar functionality:
|
||||
- `useNodeLibrarySidebarTab` - Manages the node library sidebar tab
|
||||
- `useQueueSidebarTab` - Manages the queue sidebar tab
|
||||
- `useWorkflowsSidebarTab` - Manages the workflows sidebar tab
|
||||
|
||||
### Widgets
|
||||
|
||||
Composables for widget functionality:
|
||||
- `useBooleanWidget` - Manages boolean widget interactions
|
||||
- `useComboWidget` - Manages combo box widget interactions
|
||||
- `useFloatWidget` - Manages float input widget interactions
|
||||
- `useImagePreviewWidget` - Manages image preview widget
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
When using composables in components, follow these guidelines:
|
||||
|
||||
1. **Import and call** composables at the top level of the `setup` function
|
||||
2. **Destructure returned values** to use in your component
|
||||
3. **Respect reactivity** by not destructuring reactive objects
|
||||
4. **Handle cleanup** by using `onUnmounted` when necessary
|
||||
5. **Use VueUse** for common functionality instead of writing from scratch
|
||||
|
||||
Example usage:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div
|
||||
:class="{ 'dragging': isDragging }"
|
||||
@mousedown="startDrag"
|
||||
@mouseup="endDrag"
|
||||
>
|
||||
<img v-if="imageUrl" :src="imageUrl" alt="Node preview" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop';
|
||||
import { useNodeImage } from '@/composables/node/useNodeImage';
|
||||
|
||||
// Use composables at the top level
|
||||
const { isDragging, startDrag, endDrag } = useNodeDragAndDrop();
|
||||
const { imageUrl, loadImage } = useNodeImage();
|
||||
|
||||
// Use returned values in your component
|
||||
</script>
|
||||
```
|
||||
|
||||
## VueUse Library
|
||||
|
||||
ComfyUI leverages the [VueUse](https://vueuse.org/) library, which provides a collection of essential Vue Composition API utilities. Instead of implementing common functionality from scratch, prefer using VueUse composables for:
|
||||
|
||||
- DOM event handling (`useEventListener`, `useMouseInElement`)
|
||||
- Element measurements (`useElementBounding`, `useElementSize`)
|
||||
- Asynchronous operations (`useAsyncState`, `useFetch`)
|
||||
- Animation and timing (`useTransition`, `useTimeout`, `useInterval`)
|
||||
- Browser APIs (`useLocalStorage`, `useClipboard`)
|
||||
- Sensors (`useDeviceMotion`, `useDeviceOrientation`)
|
||||
- State management (`createGlobalState`, `useStorage`)
|
||||
- ...and [more](https://vueuse.org/functions.html)
|
||||
|
||||
Examples:
|
||||
|
||||
```js
|
||||
// Instead of manually adding/removing event listeners
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
useEventListener(window, 'resize', handleResize)
|
||||
|
||||
// Instead of manually tracking element measurements
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
|
||||
const { width, height, top, left } = useElementBounding(elementRef)
|
||||
|
||||
// Instead of manual async state management
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
|
||||
const { state, isReady, isLoading } = useAsyncState(
|
||||
fetch('https://api.example.com/data').then(r => r.json()),
|
||||
{ data: [] }
|
||||
)
|
||||
```
|
||||
|
||||
For a complete list of available functions, see the [VueUse documentation](https://vueuse.org/functions.html).
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
When creating or modifying composables, follow these best practices:
|
||||
|
||||
1. **Name with `use` prefix**: All composables should start with "use"
|
||||
2. **Return an object**: Composables should return an object with named properties/methods
|
||||
3. **Handle cleanup**: Use `onUnmounted` to clean up resources
|
||||
4. **Document parameters and return values**: Add JSDoc comments
|
||||
5. **Test composables**: Write unit tests for composable functionality
|
||||
6. **Use VueUse**: Leverage VueUse composables instead of reimplementing common functionality
|
||||
7. **Implement proper cleanup**: Cancel debounced functions, pending requests, and clear maps
|
||||
8. **Use watchDebounced/watchThrottled**: For performance-sensitive reactive operations
|
||||
|
||||
### Composable Template
|
||||
|
||||
Here's a template for creating a new composable:
|
||||
|
||||
```typescript
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
/**
|
||||
* Composable for [functionality description]
|
||||
* @param options Configuration options
|
||||
* @returns Object containing state and methods
|
||||
*/
|
||||
export function useExample(options = {}) {
|
||||
// State
|
||||
const state = ref({
|
||||
// Initial state
|
||||
});
|
||||
|
||||
// Computed values
|
||||
const derivedValue = computed(() => {
|
||||
// Compute from state
|
||||
return state.value.someProperty;
|
||||
});
|
||||
|
||||
// Methods
|
||||
function doSomething() {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
// Setup
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// Cleanup
|
||||
});
|
||||
|
||||
// Return exposed state and methods
|
||||
return {
|
||||
state,
|
||||
derivedValue,
|
||||
doSomething
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
Composables in ComfyUI frequently use these patterns:
|
||||
|
||||
### State Management
|
||||
|
||||
```typescript
|
||||
export function useState() {
|
||||
const count = ref(0);
|
||||
|
||||
function increment() {
|
||||
count.value++;
|
||||
}
|
||||
|
||||
return {
|
||||
count,
|
||||
increment
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Event Handling with VueUse
|
||||
|
||||
```typescript
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
|
||||
export function useKeyPress(key) {
|
||||
const isPressed = ref(false);
|
||||
|
||||
useEventListener('keydown', (e) => {
|
||||
if (e.key === key) {
|
||||
isPressed.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
useEventListener('keyup', (e) => {
|
||||
if (e.key === key) {
|
||||
isPressed.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
return { isPressed };
|
||||
}
|
||||
```
|
||||
|
||||
### Fetch & Load with VueUse
|
||||
|
||||
```typescript
|
||||
import { useAsyncState } from '@vueuse/core';
|
||||
|
||||
export function useFetchData(url) {
|
||||
const { state: data, isLoading, error, execute: refresh } = useAsyncState(
|
||||
async () => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Failed to fetch data');
|
||||
return response.json();
|
||||
},
|
||||
null,
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return { data, isLoading, error, refresh };
|
||||
}
|
||||
```
|
||||
|
||||
For more information on Vue composables, refer to the [Vue.js Composition API documentation](https://vuejs.org/guide/reusability/composables.html) and the [VueUse documentation](https://vueuse.org/).
|
||||
@@ -1,101 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
export const useCurrentUser = () => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const commandStore = useCommandStore()
|
||||
const apiKeyStore = useApiKeyAuthStore()
|
||||
|
||||
const firebaseUser = computed(() => authStore.currentUser)
|
||||
const isApiKeyLogin = computed(() => apiKeyStore.isAuthenticated)
|
||||
const isLoggedIn = computed(
|
||||
() => !!isApiKeyLogin.value || firebaseUser.value !== null
|
||||
)
|
||||
|
||||
const userDisplayName = computed(() => {
|
||||
if (isApiKeyLogin.value) {
|
||||
return apiKeyStore.currentUser?.name
|
||||
}
|
||||
return firebaseUser.value?.displayName
|
||||
})
|
||||
|
||||
const userEmail = computed(() => {
|
||||
if (isApiKeyLogin.value) {
|
||||
return apiKeyStore.currentUser?.email
|
||||
}
|
||||
return firebaseUser.value?.email
|
||||
})
|
||||
|
||||
const providerName = computed(() => {
|
||||
if (isApiKeyLogin.value) {
|
||||
return 'Comfy API Key'
|
||||
}
|
||||
|
||||
const providerId = firebaseUser.value?.providerData[0]?.providerId
|
||||
if (providerId?.includes('google')) {
|
||||
return 'Google'
|
||||
}
|
||||
if (providerId?.includes('github')) {
|
||||
return 'GitHub'
|
||||
}
|
||||
return providerId
|
||||
})
|
||||
|
||||
const providerIcon = computed(() => {
|
||||
if (isApiKeyLogin.value) {
|
||||
return 'pi pi-key'
|
||||
}
|
||||
|
||||
const providerId = firebaseUser.value?.providerData[0]?.providerId
|
||||
if (providerId?.includes('google')) {
|
||||
return 'pi pi-google'
|
||||
}
|
||||
if (providerId?.includes('github')) {
|
||||
return 'pi pi-github'
|
||||
}
|
||||
return 'pi pi-user'
|
||||
})
|
||||
|
||||
const isEmailProvider = computed(() => {
|
||||
if (isApiKeyLogin.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
const providerId = firebaseUser.value?.providerData[0]?.providerId
|
||||
return providerId === 'password'
|
||||
})
|
||||
|
||||
const userPhotoUrl = computed(() => {
|
||||
if (isApiKeyLogin.value) return null
|
||||
return firebaseUser.value?.photoURL
|
||||
})
|
||||
|
||||
const handleSignOut = async () => {
|
||||
if (isApiKeyLogin.value) {
|
||||
await apiKeyStore.clearStoredApiKey()
|
||||
} else {
|
||||
await commandStore.execute('Comfy.User.SignOut')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSignIn = async () => {
|
||||
await commandStore.execute('Comfy.User.OpenSignInDialog')
|
||||
}
|
||||
|
||||
return {
|
||||
loading: authStore.loading,
|
||||
isLoggedIn,
|
||||
isApiKeyLogin,
|
||||
isEmailProvider,
|
||||
userDisplayName,
|
||||
userEmail,
|
||||
userPhotoUrl,
|
||||
providerName,
|
||||
providerIcon,
|
||||
handleSignOut,
|
||||
handleSignIn
|
||||
}
|
||||
}
|
||||
@@ -1,396 +0,0 @@
|
||||
import { ApiNodeCostRecord } from '@/types/apiNodeTypes'
|
||||
|
||||
/**
|
||||
* API Node cost data sourced from pricing database
|
||||
*
|
||||
* GENERATED FILE - DO NOT EDIT DIRECTLY
|
||||
* Generated from Price Run Range for each API Node CSV
|
||||
*/
|
||||
|
||||
export const apiNodeCosts: ApiNodeCostRecord = {
|
||||
FluxProCannyNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Flux 1: Canny Control Image',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.05',
|
||||
displayPrice: '$0.05/Run'
|
||||
},
|
||||
FluxProDepthNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Flux 1: Depth Control Image',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.05',
|
||||
displayPrice: '$0.05/Run'
|
||||
},
|
||||
FluxProExpandNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Flux 1: Expand Image',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.05',
|
||||
rateDocumentation: 'https://docs.bfl.ml/pricing/',
|
||||
displayPrice: '$0.05/Run'
|
||||
},
|
||||
FluxProFillNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Flux 1: Fill Image',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.05',
|
||||
displayPrice: '$0.05/Run'
|
||||
},
|
||||
FluxProUltraImageNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Flux 1.1: [pro] Ultra Image',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.06',
|
||||
displayPrice: '$0.06/Run'
|
||||
},
|
||||
IdeogramV1: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Ideogram V1',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.06',
|
||||
rateDocumentation: 'https://about.ideogram.ai/api-pricing',
|
||||
displayPrice: '$0.06/Run'
|
||||
},
|
||||
IdeogramV2: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Ideogram V2',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.08',
|
||||
displayPrice: '$0.08/Run'
|
||||
},
|
||||
IdeogramV3: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Ideogram V3',
|
||||
pricingParams: 'rendering_speed',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
KlingCameraControlI2VNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Kling Image to Video (Camera Control)',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.49',
|
||||
displayPrice: '$0.49/Run'
|
||||
},
|
||||
KlingCameraControlT2VNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Kling Text to Video (Camera Control)',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.14',
|
||||
displayPrice: '$0.14/Run'
|
||||
},
|
||||
KlingDualCharacterVideoEffectNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Kling Dual Character Video Effects',
|
||||
pricingParams: 'Priced the same as t2v based on mode, model, and duration.',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
KlingImage2VideoNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Kling Image to Video',
|
||||
pricingParams: 'Same as Text to Video',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
KlingImageGenerationNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Kling Image Generation',
|
||||
pricingParams: 'modality | model',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
KlingLipSyncAudioToVideoNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Kling Lip Sync Video with Audio',
|
||||
pricingParams: 'duration of input video',
|
||||
pricePerRunRange: '$0.07',
|
||||
displayPrice: '$0.07/Run'
|
||||
},
|
||||
KlingLipSyncTextToVideoNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Kling Lip Sync Video with Text',
|
||||
pricingParams: 'duration of input video',
|
||||
pricePerRunRange: '$0.07',
|
||||
displayPrice: '$0.07/Run'
|
||||
},
|
||||
KlingSingleImageVideoEffectNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Kling Video Effects',
|
||||
pricingParams: 'effect_scene',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
KlingStartEndFrameNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Kling Start-End Frame to Video',
|
||||
pricingParams: 'Same as text to video',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
KlingTextToVideoNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Kling Text to Video',
|
||||
pricingParams: 'model | duration | mode',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
KlingVideoExtendNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Kling Video Extend',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.28',
|
||||
displayPrice: '$0.28/Run'
|
||||
},
|
||||
KlingVirtualTryOnNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Kling Virtual Try On',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.07',
|
||||
displayPrice: '$0.07/Run'
|
||||
},
|
||||
LumaImageToVideoNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Luma Image to Video',
|
||||
pricingParams: 'Same as Text to Video',
|
||||
pricePerRunRange: 'dynamic',
|
||||
rateDocumentation: 'https://lumalabs.ai/api/pricing',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
LumaVideoNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Luma Text to Video',
|
||||
pricingParams: 'model | resolution | duration',
|
||||
pricePerRunRange: 'dynamic',
|
||||
rateDocumentation: 'https://lumalabs.ai/api/pricing',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
MinimaxImageToVideoNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'MiniMax Image to Video',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.43',
|
||||
rateDocumentation: 'https://www.minimax.io/price',
|
||||
displayPrice: '$0.43/Run'
|
||||
},
|
||||
MinimaxTextToVideoNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'MiniMax Text to Video',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.43',
|
||||
rateDocumentation: 'https://www.minimax.io/price',
|
||||
displayPrice: '$0.43/Run'
|
||||
},
|
||||
OpenAIDalle2: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'dall-e-2',
|
||||
pricingParams: 'size',
|
||||
pricePerRunRange: 'dynamic',
|
||||
rateDocumentation: 'https://platform.openai.com/docs/pricing',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
OpenAIDalle3: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'dall-e-3',
|
||||
pricingParams: '1024×1024 | hd',
|
||||
pricePerRunRange: '$0.08',
|
||||
displayPrice: '$0.08/Run'
|
||||
},
|
||||
OpenAIGPTImage1: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'gpt-image-1',
|
||||
pricingParams: 'medium',
|
||||
pricePerRunRange: '$[0.046 - 0.07]',
|
||||
displayPrice: '$0.07/Run'
|
||||
},
|
||||
PikaImageToVideoNode2_2: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Pika Image to Video',
|
||||
pricingParams: 'duration | resolution',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
PikaScenesV2_2: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Pika Scenes (Video Image Composition)',
|
||||
pricingParams: 'duration | resolution',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
PikaStartEndFrameNode2_2: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Pika Start and End Frame to Video',
|
||||
pricingParams: 'duration | resolution',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
PikaTextToVideoNode2_2: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Pika Text to Video',
|
||||
pricingParams: 'duration | resolution',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
Pikadditions: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Pikadditions (Video Object Insertion)',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.3',
|
||||
displayPrice: '$0.3/Run'
|
||||
},
|
||||
Pikaffects: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Pikaffects (Video Effects)',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '0.45',
|
||||
displayPrice: '$0.45/Run'
|
||||
},
|
||||
Pikaswaps: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Pika Swaps (Video Object Replacement)',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.3',
|
||||
displayPrice: '$0.3/Run'
|
||||
},
|
||||
PixverseImageToVideoNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'PixVerse Image to Video',
|
||||
pricingParams: 'same as text to video',
|
||||
pricePerRunRange: '$0.9',
|
||||
displayPrice: '$0.9/Run'
|
||||
},
|
||||
PixverseTextToVideoNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'PixVerse Text to Video',
|
||||
pricingParams: 'duration | quality | motion_mode',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
PixverseTransitionVideoNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'PixVerse Transition Video',
|
||||
pricingParams: 'same as text to video',
|
||||
pricePerRunRange: '$0.9',
|
||||
displayPrice: '$0.9/Run'
|
||||
},
|
||||
RecraftCrispUpscaleNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Recraft Crisp Upscale Image',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.004',
|
||||
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
|
||||
displayPrice: '$0.004/Run'
|
||||
},
|
||||
RecraftImageInpaintingNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Recraft Image Inpainting',
|
||||
pricingParams: 'n',
|
||||
pricePerRunRange: '$$0.04 x n',
|
||||
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
|
||||
displayPrice: '$$0.04 x n/Run'
|
||||
},
|
||||
RecraftImageToImageNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Recraft Image to Image',
|
||||
pricingParams: 'n',
|
||||
pricePerRunRange: '$0.04 x n',
|
||||
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
|
||||
displayPrice: '$0.04 x n/Run'
|
||||
},
|
||||
RecraftRemoveBackgroundNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Recraft Remove Background',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.01',
|
||||
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
|
||||
displayPrice: '$0.01/Run'
|
||||
},
|
||||
RecraftReplaceBackgroundNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Recraft Replace Background',
|
||||
pricingParams: 'n',
|
||||
pricePerRunRange: '$0.04',
|
||||
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
|
||||
displayPrice: '$0.04/Run'
|
||||
},
|
||||
RecraftTextToImageNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Recraft Text to Image',
|
||||
pricingParams: 'model | n',
|
||||
pricePerRunRange: '$0.04 x n',
|
||||
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
|
||||
displayPrice: '$0.04 x n/Run'
|
||||
},
|
||||
RecraftTextToVectorNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Recraft Text to Vector',
|
||||
pricingParams: 'model | n',
|
||||
pricePerRunRange: '$0.08 x n',
|
||||
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
|
||||
displayPrice: '$0.08 x n/Run'
|
||||
},
|
||||
RecraftVectorizeImageNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Recraft Vectorize Image',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.01',
|
||||
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
|
||||
displayPrice: '$0.01/Run'
|
||||
},
|
||||
StabilityStableImageSD_3_5Node: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Stability AI Stable Diffusion 3.5 Image',
|
||||
pricingParams: 'model',
|
||||
pricePerRunRange: 'dynamic',
|
||||
displayPrice: 'Variable pricing'
|
||||
},
|
||||
StabilityStableImageUltraNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Stability AI Stable Image Ultra',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.08',
|
||||
displayPrice: '$0.08/Run'
|
||||
},
|
||||
StabilityUpscaleConservativeNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Stability AI Upscale Conservative',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.25',
|
||||
displayPrice: '$0.25/Run'
|
||||
},
|
||||
StabilityUpscaleCreativeNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Stability AI Upscale Creative',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.25',
|
||||
displayPrice: '$0.25/Run'
|
||||
},
|
||||
StabilityUpscaleFastNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Stability AI Upscale Fast',
|
||||
pricingParams: '-',
|
||||
pricePerRunRange: '$0.01',
|
||||
displayPrice: '$0.01/Run'
|
||||
},
|
||||
VeoVideoGenerationNode: {
|
||||
vendor: 'Unknown',
|
||||
nodeName: 'Google Veo2 Video Generation',
|
||||
pricingParams: '10s',
|
||||
pricePerRunRange: '$5.0',
|
||||
rateDocumentation:
|
||||
'https://cloud.google.com/vertex-ai/generative-ai/pricing',
|
||||
displayPrice: '$5.0/Run'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display price for a node
|
||||
* Returns a default value if the node isn't found
|
||||
*/
|
||||
export function getNodeDisplayPrice(
|
||||
nodeName: string,
|
||||
defaultPrice = '0.02/Run (approx)'
|
||||
): string {
|
||||
return apiNodeCosts[nodeName]?.displayPrice || defaultPrice
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import {
|
||||
BadgePosition,
|
||||
LGraphBadge,
|
||||
type LGraphNode
|
||||
} from '@comfyorg/litegraph'
|
||||
import _ from 'lodash'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
|
||||
import { useNodePricing } from './useNodePricing'
|
||||
|
||||
/**
|
||||
* Add LGraphBadge to LGraphNode based on settings.
|
||||
*
|
||||
* Following badges are added:
|
||||
* - Node ID badge
|
||||
* - Node source badge
|
||||
* - Node life cycle badge
|
||||
* - API node credits badge
|
||||
*/
|
||||
export const useNodeBadge = () => {
|
||||
const settingStore = useSettingStore()
|
||||
const extensionStore = useExtensionStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const nodeSourceBadgeMode = computed(
|
||||
() =>
|
||||
settingStore.get('Comfy.NodeBadge.NodeSourceBadgeMode') as NodeBadgeMode
|
||||
)
|
||||
const nodeIdBadgeMode = computed(
|
||||
() => settingStore.get('Comfy.NodeBadge.NodeIdBadgeMode') as NodeBadgeMode
|
||||
)
|
||||
const nodeLifeCycleBadgeMode = computed(
|
||||
() =>
|
||||
settingStore.get(
|
||||
'Comfy.NodeBadge.NodeLifeCycleBadgeMode'
|
||||
) as NodeBadgeMode
|
||||
)
|
||||
|
||||
watch([nodeSourceBadgeMode, nodeIdBadgeMode, nodeLifeCycleBadgeMode], () => {
|
||||
app.graph?.setDirtyCanvas(true, true)
|
||||
})
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
function badgeTextVisible(
|
||||
nodeDef: ComfyNodeDefImpl | null,
|
||||
badgeMode: NodeBadgeMode
|
||||
): boolean {
|
||||
return !(
|
||||
badgeMode === NodeBadgeMode.None ||
|
||||
(nodeDef?.isCoreNode && badgeMode === NodeBadgeMode.HideBuiltIn)
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const nodePricing = useNodePricing()
|
||||
|
||||
extensionStore.registerExtension({
|
||||
name: 'Comfy.NodeBadge',
|
||||
nodeCreated(node: LGraphNode) {
|
||||
node.badgePosition = BadgePosition.TopRight
|
||||
|
||||
const badge = computed(() => {
|
||||
const nodeDef = nodeDefStore.fromLGraphNode(node)
|
||||
return new LGraphBadge({
|
||||
text: _.truncate(
|
||||
[
|
||||
badgeTextVisible(nodeDef, nodeIdBadgeMode.value)
|
||||
? `#${node.id}`
|
||||
: '',
|
||||
badgeTextVisible(nodeDef, nodeLifeCycleBadgeMode.value)
|
||||
? nodeDef?.nodeLifeCycleBadgeText ?? ''
|
||||
: '',
|
||||
badgeTextVisible(nodeDef, nodeSourceBadgeMode.value)
|
||||
? nodeDef?.nodeSource?.badgeText ?? ''
|
||||
: ''
|
||||
]
|
||||
.filter((s) => s.length > 0)
|
||||
.join(' '),
|
||||
{
|
||||
length: 31
|
||||
}
|
||||
),
|
||||
fgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_FG_COLOR,
|
||||
bgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_BG_COLOR
|
||||
})
|
||||
})
|
||||
|
||||
node.badges.push(() => badge.value)
|
||||
|
||||
if (node.constructor.nodeData?.api_node) {
|
||||
// Get price from our mapping service
|
||||
const price = nodePricing.getNodePriceDisplay(node)
|
||||
|
||||
const creditsBadge = computed(() => {
|
||||
return new LGraphBadge({
|
||||
text: price ?? '',
|
||||
iconOptions: {
|
||||
unicode: '\ue96b',
|
||||
fontFamily: 'PrimeIcons',
|
||||
color: '#FABC25',
|
||||
bgColor: '#353535',
|
||||
fontSize: 8
|
||||
},
|
||||
fgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_FG_COLOR,
|
||||
bgColor: '#8D6932'
|
||||
})
|
||||
})
|
||||
|
||||
node.badges.push(() => creditsBadge.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import type ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue'
|
||||
import { useChatHistoryWidget } from '@/composables/widgets/useChatHistoryWidget'
|
||||
|
||||
const CHAT_HISTORY_WIDGET_NAME = '$$node-chat-history'
|
||||
|
||||
/**
|
||||
* Composable for handling node text previews
|
||||
*/
|
||||
export function useNodeChatHistory(
|
||||
options: {
|
||||
minHeight?: number
|
||||
props?: Omit<InstanceType<typeof ChatHistoryWidget>['$props'], 'widget'>
|
||||
} = {}
|
||||
) {
|
||||
const chatHistoryWidget = useChatHistoryWidget(options)
|
||||
|
||||
const findChatHistoryWidget = (node: LGraphNode) =>
|
||||
node.widgets?.find((w) => w.name === CHAT_HISTORY_WIDGET_NAME)
|
||||
|
||||
const addChatHistoryWidget = (node: LGraphNode) =>
|
||||
chatHistoryWidget(node, {
|
||||
name: CHAT_HISTORY_WIDGET_NAME,
|
||||
type: 'chatHistory'
|
||||
})
|
||||
|
||||
/**
|
||||
* Shows chat history for a node
|
||||
* @param node The graph node to show the chat history for
|
||||
*/
|
||||
function showChatHistory(node: LGraphNode) {
|
||||
if (!findChatHistoryWidget(node)) {
|
||||
addChatHistoryWidget(node)
|
||||
}
|
||||
node.setDirtyCanvas?.(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes chat history from a node
|
||||
* @param node The graph node to remove the chat history from
|
||||
*/
|
||||
function removeChatHistory(node: LGraphNode) {
|
||||
if (!node.widgets) return
|
||||
|
||||
const widgetIdx = node.widgets.findIndex(
|
||||
(w) => w.name === CHAT_HISTORY_WIDGET_NAME
|
||||
)
|
||||
|
||||
if (widgetIdx > -1) {
|
||||
node.widgets[widgetIdx].onRemove?.()
|
||||
node.widgets.splice(widgetIdx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
showChatHistory,
|
||||
removeChatHistory
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
// Direct mapping of node names to prices
|
||||
export const NODE_PRICES: Record<string, string> = {
|
||||
// OpenAI models
|
||||
OpenAIDalle2: '$0.02/Run',
|
||||
OpenAIDalle3: '$0.08/Run',
|
||||
OpenAIGPTImage1: '$0.07/Run',
|
||||
// Ideogram models
|
||||
IdeogramV1: '$0.06/Run',
|
||||
IdeogramV2: '$0.08/Run',
|
||||
IdeogramV3: 'Variable pricing',
|
||||
// Minimax models
|
||||
MinimaxTextToVideoNode: '$0.43/Run',
|
||||
MinimaxImageToVideoNode: '$0.43/Run',
|
||||
// Google Veo
|
||||
VeoVideoGenerationNode: '$5.0/Run',
|
||||
// Kling models
|
||||
KlingTextToVideoNode: 'Variable pricing',
|
||||
KlingImage2VideoNode: 'Variable pricing',
|
||||
KlingCameraControlI2VNode: '$0.49/Run',
|
||||
KlingCameraControlT2VNode: '$0.14/Run',
|
||||
KlingStartEndFrameNode: 'Variable pricing',
|
||||
KlingVideoExtendNode: '$0.28/Run',
|
||||
KlingLipSyncAudioToVideoNode: '$0.07/Run',
|
||||
KlingLipSyncTextToVideoNode: '$0.07/Run',
|
||||
KlingVirtualTryOnNode: '$0.07/Run',
|
||||
KlingImageGenerationNode: 'Variable pricing',
|
||||
KlingSingleImageVideoEffectNode: 'Variable pricing',
|
||||
KlingDualCharacterVideoEffectNode: 'Variable pricing',
|
||||
// Flux Pro models
|
||||
FluxProUltraImageNode: '$0.06/Run',
|
||||
FluxProExpandNode: '$0.05/Run',
|
||||
FluxProFillNode: '$0.05/Run',
|
||||
FluxProCannyNode: '$0.05/Run',
|
||||
FluxProDepthNode: '$0.05/Run',
|
||||
// Luma models
|
||||
LumaVideoNode: 'Variable pricing',
|
||||
LumaImageToVideoNode: 'Variable pricing',
|
||||
LumaImageNode: 'Variable pricing',
|
||||
LumaImageModifyNode: 'Variable pricing',
|
||||
// Recraft models
|
||||
RecraftTextToImageNode: '$0.04/Run',
|
||||
RecraftImageToImageNode: '$0.04/Run',
|
||||
RecraftImageInpaintingNode: '$0.04/Run',
|
||||
RecraftTextToVectorNode: '$0.08/Run',
|
||||
RecraftVectorizeImageNode: '$0.01/Run',
|
||||
RecraftRemoveBackgroundNode: '$0.01/Run',
|
||||
RecraftReplaceBackgroundNode: '$0.04/Run',
|
||||
RecraftCrispUpscaleNode: '$0.004/Run',
|
||||
RecraftCreativeUpscaleNode: '$0.004/Run',
|
||||
// Pixverse models
|
||||
PixverseTextToVideoNode: '$0.9/Run',
|
||||
PixverseImageToVideoNode: '$0.9/Run',
|
||||
PixverseTransitionVideoNode: '$0.9/Run',
|
||||
// Stability models
|
||||
StabilityStableImageUltraNode: '$0.08/Run',
|
||||
StabilityStableImageSD_3_5Node: 'Variable pricing',
|
||||
StabilityUpscaleConservativeNode: '$0.25/Run',
|
||||
StabilityUpscaleCreativeNode: '$0.25/Run',
|
||||
StabilityUpscaleFastNode: '$0.01/Run',
|
||||
// Pika models
|
||||
PikaImageToVideoNode2_2: 'Variable pricing',
|
||||
PikaTextToVideoNode2_2: 'Variable pricing',
|
||||
PikaScenesV2_2: 'Variable pricing',
|
||||
PikaStartEndFrameNode2_2: 'Variable pricing',
|
||||
Pikadditions: '$0.3/Run',
|
||||
Pikaswaps: '$0.3/Run',
|
||||
Pikaffects: '$0.45/Run'
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple utility function to get the price for a node
|
||||
* Returns a formatted price string or default value if the node isn't found
|
||||
*/
|
||||
export function getNodePrice(
|
||||
node: LGraphNode,
|
||||
defaultPrice = '0.02/Run (approx)'
|
||||
): string {
|
||||
if (!node.constructor.nodeData?.api_node) {
|
||||
return ''
|
||||
}
|
||||
return NODE_PRICES[node.constructor.name] || defaultPrice
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable to get node pricing information for API nodes
|
||||
*/
|
||||
export const useNodePricing = () => {
|
||||
/**
|
||||
* Get the price display for a node
|
||||
*/
|
||||
const getNodePriceDisplay = (node: LGraphNode): string => {
|
||||
if (!node.constructor.nodeData?.api_node) {
|
||||
return ''
|
||||
}
|
||||
return NODE_PRICES[node.constructor.name] || '0.02/Run (approx)'
|
||||
}
|
||||
|
||||
return {
|
||||
getNodePriceDisplay
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useTextPreviewWidget } from '@/composables/widgets/useProgressTextWidget'
|
||||
|
||||
const TEXT_PREVIEW_WIDGET_NAME = '$$node-text-preview'
|
||||
|
||||
/**
|
||||
* Composable for handling node text previews
|
||||
*/
|
||||
export function useNodeProgressText() {
|
||||
const textPreviewWidget = useTextPreviewWidget()
|
||||
|
||||
const findTextPreviewWidget = (node: LGraphNode) =>
|
||||
node.widgets?.find((w) => w.name === TEXT_PREVIEW_WIDGET_NAME)
|
||||
|
||||
const addTextPreviewWidget = (node: LGraphNode) =>
|
||||
textPreviewWidget(node, {
|
||||
name: TEXT_PREVIEW_WIDGET_NAME,
|
||||
type: 'progressText'
|
||||
})
|
||||
|
||||
/**
|
||||
* Shows text preview for a node
|
||||
* @param node The graph node to show the preview for
|
||||
*/
|
||||
function showTextPreview(node: LGraphNode, text: string) {
|
||||
const widget = findTextPreviewWidget(node) ?? addTextPreviewWidget(node)
|
||||
widget.value = text
|
||||
node.setDirtyCanvas?.(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes text preview from a node
|
||||
* @param node The graph node to remove the preview from
|
||||
*/
|
||||
function removeTextPreview(node: LGraphNode) {
|
||||
if (!node.widgets) return
|
||||
|
||||
const widgetIdx = node.widgets.findIndex(
|
||||
(w) => w.name === TEXT_PREVIEW_WIDGET_NAME
|
||||
)
|
||||
|
||||
if (widgetIdx > -1) {
|
||||
node.widgets[widgetIdx].onRemove?.()
|
||||
node.widgets.splice(widgetIdx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
showTextPreview,
|
||||
removeTextPreview
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,13 @@ import {
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
|
||||
import type { SettingParams } from '@/types/settingTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
|
||||
import { useCurrentUser } from '../auth/useCurrentUser'
|
||||
|
||||
interface SettingPanelItem {
|
||||
node: SettingTreeNode
|
||||
component: Component
|
||||
@@ -30,7 +29,7 @@ export function useSettingUI(
|
||||
| 'credits'
|
||||
) {
|
||||
const { t } = useI18n()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const settingStore = useSettingStore()
|
||||
const activeCategory = ref<SettingTreeNode | null>(null)
|
||||
|
||||
@@ -166,7 +165,7 @@ export function useSettingUI(
|
||||
label: 'Account',
|
||||
children: [
|
||||
userPanel.node,
|
||||
...(isLoggedIn.value ? [creditsPanel.node] : [])
|
||||
...(firebaseAuthStore.isAuthenticated ? [creditsPanel.node] : [])
|
||||
].map(translateCategory)
|
||||
},
|
||||
// Normal settings stored in the settingStore
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useTitle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
const DEFAULT_TITLE = 'ComfyUI'
|
||||
const TITLE_SUFFIX = ' - ComfyUI'
|
||||
|
||||
export const useBrowserTabTitle = () => {
|
||||
const executionStore = useExecutionStore()
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const executionText = computed(() =>
|
||||
executionStore.isIdle
|
||||
? ''
|
||||
: `[${Math.round(executionStore.executionProgress * 100)}%]`
|
||||
)
|
||||
|
||||
const newMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
const isUnsavedText = computed(() =>
|
||||
workflowStore.activeWorkflow?.isModified ||
|
||||
!workflowStore.activeWorkflow?.isPersisted
|
||||
? ' *'
|
||||
: ''
|
||||
)
|
||||
const workflowNameText = computed(() => {
|
||||
const workflowName = workflowStore.activeWorkflow?.filename
|
||||
return workflowName
|
||||
? isUnsavedText.value + workflowName + TITLE_SUFFIX
|
||||
: DEFAULT_TITLE
|
||||
})
|
||||
|
||||
const nodeExecutionTitle = computed(() =>
|
||||
executionStore.executingNode && executionStore.executingNodeProgress
|
||||
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
|
||||
: ''
|
||||
)
|
||||
|
||||
const workflowTitle = computed(
|
||||
() =>
|
||||
executionText.value +
|
||||
(newMenuEnabled.value ? workflowNameText.value : DEFAULT_TITLE)
|
||||
)
|
||||
|
||||
const title = computed(() => nodeExecutionTitle.value || workflowTitle.value)
|
||||
useTitle(title)
|
||||
}
|
||||