Merge pull request #870 from zanllp/feature/topicSearch

feat: experimental natural-language categorization & search (persistent scope, clustering, retrieval)
pull/876/head
zanllp 2026-01-04 01:17:38 +08:00 committed by GitHub
commit 3ef90cface
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
85 changed files with 3956 additions and 113 deletions

View File

@ -29,15 +29,15 @@ IIB_DB_FILE_BACKUP_MAX=8
# The default value is 'auto', which will be determined based on the command-line parameters used to start sd-webui (such as --server-name, --share, --listen).
IIB_ACCESS_CONTROL=auto
# This variable is used to define a list of allowed paths for the application to access when access control mode is enabled.
# This variable is used to define a list of allowed paths for the application to access when access control mode is enabled.
# It can be set to a comma-separated string of file paths or directory paths, representing the resources that are allowed to be accessed by the application.
# In addition, if sd_webui_config or sd_webui_dir has been configured, or if you're running this repository as an extension of sd-webui,
# In addition, if sd_webui_config or sd_webui_dir has been configured, or if you're running this repository as an extension of sd-webui,
# you can use the following shortcuts (txt2img, img2img, extra, save) as values for the ALLOWED_PATHS variable.
# IIB_ACCESS_CONTROL_ALLOWED_PATHS=save,extra,/output ...etc
# This variable is used to control fine-grained access control for different types of requests, but only if access control mode is enabled.
# It can be set to a string value that represents a specific permission or set of permissions, such as "read-only", "write-only", "read-write", or "no-access".
# This variable can be used to restrict access to certain API endpoints or data sources based on the permissions required by the user.
# This variable is used to control fine-grained access control for different types of requests, but only if access control mode is enabled.
# It can be set to a string value that represents a specific permission or set of permissions, such as "read-only", "write-only", "read-write", or "no-access".
# This variable can be used to restrict access to certain API endpoints or data sources based on the permissions required by the user.
# IIB_ACCESS_CONTROL_PERMISSION=read-write
@ -47,3 +47,41 @@ IIB_ACCESS_CONTROL=auto
# Due to the high performance cost of parsing this type of file, it is disabled by default.
# Set to 'true' to enable it.
IIB_ENABLE_SD_WEBUI_STEALTH_PARSER=false
# ---------------------------- AI / EMBEDDINGS ----------------------------
# OpenAI-compatible API base url.
# - Keep it generic here (do NOT write any provider-specific or personal info into this example file).
# - Typical format: https://your-openai-compatible-host/v1
# - This project uses it for BOTH embeddings (/embeddings) and topic-title chat (/chat/completions).
OPENAI_BASE_URL=
# OpenAI-compatible API key (Bearer token).
# - Put your real key in x.env (or .env) only, never commit it.
OPENAI_API_KEY=
# Embedding model id (OpenAI-compatible).
# - Used for clustering images by prompt semantics.
# - Recommended: start with a small/cheap embedding model; switch to a larger one only if clustering quality is insufficient.
EMBEDDING_MODEL=text-embedding-3-small
# Default chat model id (OpenAI-compatible).
# - Used by the generic /ai_chat endpoint (if you use it) and as a fallback default.
# - If your provider applies content filtering that sometimes returns empty output, try switching to another chat model here.
AI_MODEL=gpt-4o-mini
# Topic title model id (OpenAI-compatible).
# - Used ONLY for generating short cluster titles/keywords (topic naming).
# - If not set, it falls back to AI_MODEL.
# - Tip: if a model frequently violates "JSON only" or gets truncated, switch this model first.
TOPIC_TITLE_MODEL=gpt-4o-mini
# Prompt normalization before embedding/clustering (remove boilerplate but keep distinctiveness).
# - Removes common "quality / camera / resolution" boilerplate from prompts before embedding.
# - Keep enabled for better clustering; disable only for debugging.
IIB_PROMPT_NORMALIZE=1
# Prompt normalization mode:
# - 'balanced' (recommended): remove generic boilerplate but keep some discriminative style words.
# - 'theme_only' (aggressive): focus more on subject/theme nouns; may lose some stylistic distinctiveness.
IIB_PROMPT_NORMALIZE_MODE=balanced

3
.gitignore vendored
View File

@ -20,3 +20,6 @@ plugins/*
!plugins/.gitkeep
test_data/*
.DS_Store
iib_output
iib.db-shm
iib.db-wal

View File

@ -159,4 +159,83 @@ https://user-images.githubusercontent.com/25872019/230768207-daab786b-d4ab-489f-
<img width="768" alt="image" src="https://user-images.githubusercontent.com/25872019/230064879-c95866ac-999d-4d4b-87ea-3e38c8479415.png">
## 自然语言分类&搜索(实验性)
这个功能用于把图片按**提示词语义相似度**自动分组(主题),并支持用一句自然语言做**语义检索**(类似 RAG 的召回阶段)。
它是实验性功能:效果强依赖模型与提示词质量,适合快速找回/整理生成图片。
### 使用方式(面向使用者)
1. 打开首页「**自然语言分类&搜索(实验性)**」
2. 点击「范围」选择要处理的文件夹(可多选,来源于 QuickMovePaths
3. **归类**:点「刷新」会在所选范围内生成主题列表(标题会按前端语言输出)
4. **搜索**输入一句话点「搜索」会自动打开结果页TopK 相似图片)
> 选择的范围会持久化到后端 KV`app_fe_setting["topic_search_scope"]`),下次打开会自动恢复并自动刷新一次结果。
### 接口(给高级用户/二次开发)
- **构建/刷新向量**`POST /infinite_image_browsing/db/build_iib_output_embeddings`
- 入参:`folder`, `model`, `force`, `batch_size`, `max_chars`
- **归类(聚类)**`POST /infinite_image_browsing/db/cluster_iib_output_job_start`,然后轮询 `GET /infinite_image_browsing/db/cluster_iib_output_job_status?job_id=...`
- 入参:`folder_paths`(必填,数组)、`threshold`, `min_cluster_size`, `force_embed`, `title_model`, `force_title`, `use_title_cache`, `assign_noise_threshold`, `lang`
- **语义检索RAG 召回)**`POST /infinite_image_browsing/db/search_iib_output_by_prompt`
- 入参:`query`, `folder_paths`(必填,数组)、`top_k`, `min_score`, `ensure_embed`, `model`, `max_chars`
### 原理(简单版)
- **1提示词抽取与清洗**
- 从 `image.exif` 中抽取提示词文本(只取 `Negative prompt:` 之前)
- 可选做“语义清洗”:去掉无意义的高频模板词(画质/摄影参数等),更聚焦主题语义(见 `IIB_PROMPT_NORMALIZE*`
- **2向量化Embedding**
- 调用 OpenAI 兼容的 `/embeddings` 得到向量
- 写入 SQLite 表 `image_embedding`(增量更新,避免重复花费)
- **3主题聚类**
- 用“簇向量求和方向”的增量聚类(近似在线聚类),再把高相似簇做一次合并(减少同主题被切碎)
- 可选把小簇成员重新分配到最相近的大簇,降低噪声
- **4主题命名LLM**
- 对每个簇取代表提示词样本,调用 `/chat/completions` 生成短标题与关键词
- 通过 tool/function calling 强制结构化输出JSON并写入 `topic_title_cache`
- **5语义检索**
- 把用户 query 向量化,然后和范围内所有图片向量做余弦相似度排序,返回 TopK
### 缓存与增量更新
#### 1向量缓存`image_embedding`
- **存储位置**SQLite 表 `image_embedding`(以 `image_id` 为主键)
- **增量跳过条件**:满足以下条件则跳过重新向量化:
- `model` 相同
- `text_hash` 相同
- 已存在 `vec`
- **“重新向量化”的缓存键**`text_hash = sha256(f"{normalize_version}:{prompt_text}")`
- `prompt_text`:用于 embedding 的最终文本(抽取 + 可选清洗)
- `normalize_version`:由代码对清洗规则/模式计算出的**指纹**(不允许用户用环境变量手动覆盖)
- **强制刷新**:在 `build_iib_output_embeddings``force=true`,或在 `cluster_iib_output_job_start``force_embed=true`
#### 2标题缓存`topic_title_cache`
- **存储位置**SQLite 表 `topic_title_cache`(主键 `cluster_hash`
- **命中条件**`use_title_cache=true` 且 `force_title=false` 时复用历史标题/关键词
- **缓存键 `cluster_hash` 包含**
- 成员图片 id排序后
- embedding `model`、`threshold`、`min_cluster_size`
- `title_model`、输出语言 `lang`
- 语义清洗指纹(`normalize_version`)与清洗模式
- **强制重新生成标题**`force_title=true`
### 配置(环境变量)
所有 AI 调用都基于 **OpenAI 兼容** 的服务:
- **`OPENAI_BASE_URL`**:例如 `https://your-host/v1`
- **`OPENAI_API_KEY`**:你的 Key
- **`EMBEDDING_MODEL`**:用于聚类的 embedding 模型
- **`AI_MODEL`**:默认 chat 模型(兜底默认)
- **`TOPIC_TITLE_MODEL`**:用于主题标题的 chat 模型(不配则回退到 `AI_MODEL`
- **`IIB_PROMPT_NORMALIZE`**`1/0` 是否开启提示词清洗
- **`IIB_PROMPT_NORMALIZE_MODE`**`balanced`(推荐)/ `theme_only`(更激进)
> 注意AI 调用**没有 mock 兜底**。只要服务端/模型返回异常或不符合约束,就会直接报错,避免产生“看似能跑但其实不可信”的结果。

View File

@ -176,3 +176,82 @@ https://user-images.githubusercontent.com/25872019/230768207-daab786b-d4ab-489f-
### Dark mode
<img width="768" alt="image" src="https://user-images.githubusercontent.com/25872019/230064879-c95866ac-999d-4d4b-87ea-3e38c8479415.png">
## Natural Language Categorization & Search (Experimental)
This feature groups images by **semantic similarity of prompts** and supports **natural-language retrieval** (similar to the retrieval stage in RAG).
Its experimental: results depend on the embedding/chat models and the quality of prompt metadata.
### How to Use (for end users)
1. Open **“Natural Language Categorization & Search (Experimental)”** from the startup page
2. Click **Scope** and select one or more folders (from QuickMovePaths)
3. **Categorize**: click **Refresh** to generate topic cards for the selected scope
4. **Search**: type a natural-language query and click **Search** (auto-opens the result grid)
> The selected scope is persisted in backend KV: `app_fe_setting["topic_search_scope"]`. Next time it will auto-restore and auto-refresh once.
### API Endpoints
- **Build/refresh embeddings**: `POST /infinite_image_browsing/db/build_iib_output_embeddings`
- Request: `folder`, `model`, `force`, `batch_size`, `max_chars`
- **Cluster (categorize)**: `POST /infinite_image_browsing/db/cluster_iib_output_job_start` then poll `GET /infinite_image_browsing/db/cluster_iib_output_job_status?job_id=...`
- Request: `folder_paths` (required, array), `threshold`, `min_cluster_size`, `force_embed`, `title_model`, `force_title`, `use_title_cache`, `assign_noise_threshold`, `lang`
- **Prompt retrieval (RAG-like)**: `POST /infinite_image_browsing/db/search_iib_output_by_prompt`
- Request: `query`, `folder_paths` (required, array), `top_k`, `min_score`, `ensure_embed`, `model`, `max_chars`
### How it Works (simple explanation)
- **1) Prompt extraction & normalization**
- Reads `image.exif` and keeps content before `Negative prompt:`
- Optionally removes “boilerplate” terms (quality/photography parameters, etc.) to focus on topic semantics (`IIB_PROMPT_NORMALIZE*`)
- **2) Embeddings**
- Calls OpenAI-compatible `/embeddings`
- Stores vectors in SQLite table `image_embedding` (incremental, to avoid repeated costs)
- **3) Clustering**
- Online centroid-sum clustering, plus a post-merge step for highly similar clusters
- Optionally reassigns members of small clusters into the closest large cluster to reduce noise
- **4) Title generation (LLM)**
- Calls `/chat/completions` with tool/function calling to force structured JSON output
- Stores titles/keywords in SQLite table `topic_title_cache`
- **5) Retrieval**
- Embeds the query and ranks images in the selected scope by cosine similarity, returning TopK
### Caching & Incremental Updates
#### 1) Embedding cache (`image_embedding`)
- **Where**: table `image_embedding` (keyed by `image_id`)
- **Skip rule (incremental update)**: an image is skipped if:
- same `model`
- same `text_hash`
- existing `vec` is present
- **Re-vectorization cache key**: `text_hash = sha256(f"{normalize_version}:{prompt_text}")`
- `prompt_text` is the extracted + (optionally) normalized text used for embeddings
- `normalize_version` is a **code-derived fingerprint** of normalization rules/mode (not user-configurable)
- **Force rebuild**: pass `force=true` to `build_iib_output_embeddings` or `force_embed=true` to `cluster_iib_output_job_start`
#### 2) Title cache (`topic_title_cache`)
- **Where**: table `topic_title_cache` keyed by `cluster_hash`
- **Hit rule**: when `use_title_cache=true` and `force_title=false`, titles/keywords are reused
- **Cache key (`cluster_hash`) includes**:
- member image IDs (sorted)
- embedding `model`, `threshold`, `min_cluster_size`
- `title_model`, output `lang`
- normalization fingerprint (`normalize_version`) and mode
- **Force title regeneration**: `force_title=true`
### Configuration (Environment Variables)
All calls use an **OpenAI-compatible** provider:
- **`OPENAI_BASE_URL`**: e.g. `https://your-host/v1`
- **`OPENAI_API_KEY`**: your API key
- **`EMBEDDING_MODEL`**: embeddings model used for clustering
- **`AI_MODEL`**: default chat model (fallback)
- **`TOPIC_TITLE_MODEL`**: chat model used for cluster titles (falls back to `AI_MODEL`)
- **`IIB_PROMPT_NORMALIZE`**: `1/0` enable prompt normalization
- **`IIB_PROMPT_NORMALIZE_MODE`**: `balanced` (recommended) / `theme_only`
> Note: There is **no mock fallback** for AI calls. If the provider/model fails or returns invalid output, the API will return an error directly.

View File

@ -13,8 +13,8 @@ Promise.resolve().then(async () => {
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Infinite Image Browsing</title>
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-5ed9cd5a.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-c290c403.css">
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-8b1d4076.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-d385cc4f.css">
</head>
<body>

View File

@ -7,4 +7,7 @@ pillow-avif-plugin
imageio
av>=14,<15
lxml
filetype
filetype
requests
numpy
hnswlib

View File

@ -6,7 +6,6 @@ from pathlib import Path
import shutil
import sqlite3
from scripts.iib.dir_cover_cache import get_top_4_media_info
from scripts.iib.tool import (
get_created_date_by_stat,
@ -48,6 +47,7 @@ from PIL import Image
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
import hashlib
from contextlib import closing
from scripts.iib.db.datamodel import (
DataBase,
ExtraPathType,
@ -58,9 +58,10 @@ from scripts.iib.db.datamodel import (
ExtraPath,
FileInfoDict,
Cursor,
GlobalSetting
GlobalSetting,
)
from scripts.iib.db.update_image_data import update_image_data, rebuild_image_index, add_image_data_single
from scripts.iib.topic_cluster import mount_topic_cluster_routes
from scripts.iib.logger import logger
from scripts.iib.seq import seq
import urllib.parse
@ -73,6 +74,13 @@ try:
except Exception as e:
logger.error(e)
import requests
import dotenv
# 加载环境变量
dotenv.load_dotenv()
index_html_path = get_data_file_path("vue/dist/index.html") if is_exe_ver else os.path.join(cwd, "vue/dist/index.html") # 在app.py也被使用
@ -83,6 +91,17 @@ secret_key = os.getenv("IIB_SECRET_KEY")
if secret_key:
print("Secret key loaded successfully. ")
# AI 配置
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
AI_MODEL = os.getenv("AI_MODEL", "gpt-4o-mini")
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small")
print(f"AI Model: {AI_MODEL or 'Not configured'}")
print(f"OpenAI Base URL: {OPENAI_BASE_URL}")
print(f"OpenAI API Key: {'Configured' if OPENAI_API_KEY else 'Not configured'}")
print(f"Embedding Model: {EMBEDDING_MODEL or 'Not configured'}")
WRITEABLE_PERMISSIONS = ["read-write", "write-only"]
is_api_writeable = not (os.getenv("IIB_ACCESS_CONTROL_PERMISSION")) or (
@ -376,6 +395,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
)
async def delete_files(req: DeleteFilesReq):
conn = DataBase.get_conn()
for path in req.file_paths:
check_path_trust(path)
try:
@ -390,10 +410,12 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
shutil.rmtree(path)
else:
close_video_file_reader(path)
os.remove(path)
txt_path = get_img_geninfo_txt_path(path)
os.remove(path)
if txt_path:
os.remove(txt_path)
img = DbImg.get(conn, os.path.normpath(path))
if img:
logger.info("delete file: %s", path)
@ -409,6 +431,8 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
)
raise HTTPException(400, detail=error_msg)
return {"ok": True}
class CreateFoldersReq(BaseModel):
dest_folder: str
@ -606,6 +630,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
img.thumbnail((int(w), int(h)))
os.makedirs(cache_dir, exist_ok=True)
img.save(cache_path, "webp")
# print(f"Image cache generated: {path}")
# 返回缓存文件
return FileResponse(
@ -1216,6 +1241,17 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
ImageTag.remove(conn, image_id=req.img_id, tag_id=req.tag_id)
# ===== 主题聚类 / Embedding拆分到独立模块减少 api.py 体积)=====
mount_topic_cluster_routes(
app=app,
db_api_base=db_api_base,
verify_secret=verify_secret,
write_permission_required=write_permission_required,
openai_base_url=OPENAI_BASE_URL,
openai_api_key=OPENAI_API_KEY,
embedding_model=EMBEDDING_MODEL,
ai_model=AI_MODEL,
)
class ExtraPathModel(BaseModel):
@ -1293,3 +1329,48 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
update_extra_paths(conn = DataBase.get_conn())
rebuild_image_index(search_dirs = get_img_search_dirs() + mem["extra_paths"])
# AI 相关路由
class AIChatRequest(BaseModel):
messages: List[dict]
temperature: Optional[float] = 0.7
max_tokens: Optional[int] = None
stream: Optional[bool] = False
@app.post(f"{api_base}/ai-chat", dependencies=[Depends(verify_secret), Depends(write_permission_required)])
async def ai_chat(req: AIChatRequest):
"""通用AI聊天接口转发到OpenAI兼容API"""
if not OPENAI_API_KEY:
raise HTTPException(status_code=500, detail="OpenAI API Key not configured")
try:
payload = {
"model": AI_MODEL,
"messages": req.messages,
"temperature": req.temperature,
"stream": req.stream
}
if req.max_tokens:
payload["max_tokens"] = req.max_tokens
headers = {
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json"
}
response = requests.post(
f"{OPENAI_BASE_URL}/chat/completions",
json=payload,
headers=headers,
timeout=60
)
if response.status_code != 200:
raise HTTPException(status_code=response.status_code, detail=response.text)
return response.json()
except requests.RequestException as e:
logger.error(f"AI API request failed: {e}")
raise HTTPException(status_code=500, detail=f"AI API request failed: {str(e)}")

View File

@ -18,6 +18,7 @@ from contextlib import closing
import os
import threading
import re
import hashlib
class FileInfoDict(TypedDict):
@ -80,6 +81,10 @@ class DataBase:
ExtraPath.create_table(conn)
DirCoverCache.create_table(conn)
GlobalSetting.create_table(conn)
ImageEmbedding.create_table(conn)
ImageEmbeddingFail.create_table(conn)
TopicTitleCache.create_table(conn)
TopicClusterCache.create_table(conn)
finally:
conn.commit()
clz.num += 1
@ -187,6 +192,13 @@ class Image:
@classmethod
def remove(cls, conn: Connection, image_id: int) -> None:
with closing(conn.cursor()) as cur:
# Manual cascade delete to avoid leaving orphan rows in related tables.
# NOTE: SQLite foreign key constraints are often disabled by default unless
# PRAGMA foreign_keys=ON is set. We still delete related rows explicitly
# so deletion works regardless of FK settings and keeps DB clean.
cur.execute("DELETE FROM image_embedding WHERE image_id = ?", (int(image_id),))
cur.execute("DELETE FROM image_embedding_fail WHERE image_id = ?", (int(image_id),))
cur.execute("DELETE FROM image_tag WHERE image_id = ?", (int(image_id),))
cur.execute("DELETE FROM image WHERE id = ?", (image_id,))
conn.commit()
@ -197,6 +209,16 @@ class Image:
with closing(conn.cursor()) as cur:
try:
placeholders = ",".join("?" * len(image_ids))
# Manual cascade delete for related tables.
# Keep this in sync with tables referencing image.id.
cur.execute(
f"DELETE FROM image_embedding WHERE image_id IN ({placeholders})",
image_ids,
)
cur.execute(
f"DELETE FROM image_embedding_fail WHERE image_id IN ({placeholders})",
image_ids,
)
cur.execute(
f"DELETE FROM image_tag WHERE image_id IN ({placeholders})",
image_ids,
@ -301,6 +323,328 @@ class Image:
return images
class ImageEmbedding:
"""
Store embeddings for image prompt text.
Notes:
- vec is stored as float32 bytes (little-endian), compatible with Python's array('f').
- text_hash is used to skip recomputation when prompt text doesn't change.
"""
@classmethod
def create_table(cls, conn: Connection):
with closing(conn.cursor()) as cur:
cur.execute(
"""CREATE TABLE IF NOT EXISTS image_embedding (
image_id INTEGER PRIMARY KEY,
model TEXT NOT NULL,
dim INTEGER NOT NULL,
text_hash TEXT NOT NULL,
vec BLOB NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (image_id) REFERENCES image(id)
)"""
)
cur.execute(
"CREATE INDEX IF NOT EXISTS image_embedding_idx_model_hash ON image_embedding(model, text_hash)"
)
@staticmethod
def compute_text_hash(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
@classmethod
def get_by_image_ids(cls, conn: Connection, image_ids: List[int]):
if not image_ids:
return {}
placeholders = ",".join("?" * len(image_ids))
query = f"SELECT image_id, model, dim, text_hash, vec, updated_at FROM image_embedding WHERE image_id IN ({placeholders})"
with closing(conn.cursor()) as cur:
cur.execute(query, image_ids)
rows = cur.fetchall()
res = {}
for row in rows:
res[row[0]] = {
"image_id": row[0],
"model": row[1],
"dim": row[2],
"text_hash": row[3],
"vec": row[4],
"updated_at": row[5],
}
return res
@classmethod
def upsert(
cls,
conn: Connection,
image_id: int,
model: str,
dim: int,
text_hash: str,
vec_blob: bytes,
updated_at: Optional[str] = None,
):
updated_at = updated_at or datetime.now().isoformat()
with closing(conn.cursor()) as cur:
cur.execute(
"""INSERT INTO image_embedding (image_id, model, dim, text_hash, vec, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(image_id) DO UPDATE SET
model = excluded.model,
dim = excluded.dim,
text_hash = excluded.text_hash,
vec = excluded.vec,
updated_at = excluded.updated_at
""",
(image_id, model, dim, text_hash, vec_blob, updated_at),
)
class ImageEmbeddingFail:
"""
Cache embedding failures per image+model+text_hash to avoid repeatedly hitting the API
for known-failing inputs. This helps keep clustering/search usable by skipping bad items.
"""
@classmethod
def create_table(cls, conn: Connection):
with closing(conn.cursor()) as cur:
cur.execute(
"""CREATE TABLE IF NOT EXISTS image_embedding_fail (
image_id INTEGER NOT NULL,
model TEXT NOT NULL,
text_hash TEXT NOT NULL,
error TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY(image_id, model, text_hash)
)"""
)
cur.execute("CREATE INDEX IF NOT EXISTS image_embedding_fail_idx_model ON image_embedding_fail(model)")
@classmethod
def get_by_image_ids(cls, conn: Connection, image_ids: List[int], model: str) -> Dict[int, Dict]:
if not image_ids:
return {}
ids = [int(x) for x in image_ids]
placeholders = ",".join(["?"] * len(ids))
with closing(conn.cursor()) as cur:
cur.execute(
f"SELECT image_id, text_hash, error, updated_at FROM image_embedding_fail WHERE model = ? AND image_id IN ({placeholders})",
(str(model), *ids),
)
rows = cur.fetchall()
out: Dict[int, Dict] = {}
for image_id, text_hash, error, updated_at in rows or []:
out[int(image_id)] = {
"text_hash": str(text_hash or ""),
"error": str(error or ""),
"updated_at": str(updated_at or ""),
}
return out
@classmethod
def upsert(
cls,
conn: Connection,
*,
image_id: int,
model: str,
text_hash: str,
error: str,
updated_at: Optional[str] = None,
):
updated_at = updated_at or datetime.now().isoformat()
with closing(conn.cursor()) as cur:
cur.execute(
"""INSERT INTO image_embedding_fail (image_id, model, text_hash, error, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(image_id, model, text_hash) DO UPDATE SET
error = excluded.error,
updated_at = excluded.updated_at
""",
(int(image_id), str(model), str(text_hash), str(error or "")[:600], str(updated_at)),
)
@classmethod
def delete(cls, conn: Connection, *, image_id: int, model: str):
with closing(conn.cursor()) as cur:
cur.execute("DELETE FROM image_embedding_fail WHERE image_id = ? AND model = ?", (int(image_id), str(model)))
class TopicTitleCache:
"""
Cache cluster titles/keywords to avoid repeated LLM calls.
"""
@classmethod
def create_table(cls, conn: Connection):
with closing(conn.cursor()) as cur:
cur.execute(
"""CREATE TABLE IF NOT EXISTS topic_title_cache (
cluster_hash TEXT PRIMARY KEY,
title TEXT NOT NULL,
keywords TEXT NOT NULL,
model TEXT NOT NULL,
updated_at TEXT NOT NULL
)"""
)
cur.execute(
"CREATE INDEX IF NOT EXISTS topic_title_cache_idx_model ON topic_title_cache(model)"
)
@classmethod
def get(cls, conn: Connection, cluster_hash: str):
with closing(conn.cursor()) as cur:
cur.execute(
"SELECT title, keywords, model, updated_at FROM topic_title_cache WHERE cluster_hash = ?",
(cluster_hash,),
)
row = cur.fetchone()
if not row:
return None
title, keywords, model, updated_at = row
try:
kw = json.loads(keywords) if isinstance(keywords, str) else []
except Exception:
kw = []
if not isinstance(kw, list):
kw = []
return {"title": title, "keywords": kw, "model": model, "updated_at": updated_at}
@classmethod
def upsert(
cls,
conn: Connection,
cluster_hash: str,
title: str,
keywords: List[str],
model: str,
updated_at: Optional[str] = None,
):
updated_at = updated_at or datetime.now().isoformat()
kw = json.dumps([str(x) for x in (keywords or [])], ensure_ascii=False)
with closing(conn.cursor()) as cur:
cur.execute(
"""INSERT INTO topic_title_cache (cluster_hash, title, keywords, model, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(cluster_hash) DO UPDATE SET
title = excluded.title,
keywords = excluded.keywords,
model = excluded.model,
updated_at = excluded.updated_at
""",
(cluster_hash, title, kw, model, updated_at),
)
class TopicClusterCache:
"""
Persist the final clustering result (clusters/noise) to avoid re-clustering when:
- embeddings haven't changed (by max(updated_at) & count), and
- clustering parameters are unchanged.
This is intentionally lightweight:
- result is stored as JSON text
- caller defines cache_key (sha1 over params + folders + normalize version + lang, etc.)
"""
@classmethod
def create_table(cls, conn: Connection):
with closing(conn.cursor()) as cur:
cur.execute(
"""CREATE TABLE IF NOT EXISTS topic_cluster_cache (
cache_key TEXT PRIMARY KEY,
folders TEXT NOT NULL,
model TEXT NOT NULL,
params TEXT NOT NULL,
embeddings_count INTEGER NOT NULL,
embeddings_max_updated_at TEXT NOT NULL,
result TEXT NOT NULL,
updated_at TEXT NOT NULL
)"""
)
cur.execute("CREATE INDEX IF NOT EXISTS topic_cluster_cache_idx_model ON topic_cluster_cache(model)")
@classmethod
def get(cls, conn: Connection, cache_key: str):
with closing(conn.cursor()) as cur:
cur.execute(
"SELECT folders, model, params, embeddings_count, embeddings_max_updated_at, result, updated_at FROM topic_cluster_cache WHERE cache_key = ?",
(cache_key,),
)
row = cur.fetchone()
if not row:
return None
folders, model, params, embeddings_count, embeddings_max_updated_at, result, updated_at = row
try:
folders_obj = json.loads(folders) if isinstance(folders, str) else []
except Exception:
folders_obj = []
try:
params_obj = json.loads(params) if isinstance(params, str) else {}
except Exception:
params_obj = {}
try:
result_obj = json.loads(result) if isinstance(result, str) else None
except Exception:
result_obj = None
return {
"cache_key": cache_key,
"folders": folders_obj if isinstance(folders_obj, list) else [],
"model": str(model),
"params": params_obj if isinstance(params_obj, dict) else {},
"embeddings_count": int(embeddings_count or 0),
"embeddings_max_updated_at": str(embeddings_max_updated_at or ""),
"result": result_obj,
"updated_at": str(updated_at or ""),
}
@classmethod
def upsert(
cls,
conn: Connection,
*,
cache_key: str,
folders: List[str],
model: str,
params: Dict,
embeddings_count: int,
embeddings_max_updated_at: str,
result: Dict,
updated_at: Optional[str] = None,
):
updated_at = updated_at or datetime.now().isoformat()
folders_s = json.dumps([str(x) for x in (folders or [])], ensure_ascii=False)
params_s = json.dumps(params or {}, ensure_ascii=False, sort_keys=True)
result_s = json.dumps(result or {}, ensure_ascii=False)
with closing(conn.cursor()) as cur:
cur.execute(
"""INSERT INTO topic_cluster_cache
(cache_key, folders, model, params, embeddings_count, embeddings_max_updated_at, result, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(cache_key) DO UPDATE SET
folders = excluded.folders,
model = excluded.model,
params = excluded.params,
embeddings_count = excluded.embeddings_count,
embeddings_max_updated_at = excluded.embeddings_max_updated_at,
result = excluded.result,
updated_at = excluded.updated_at
""",
(
cache_key,
folders_s,
str(model),
params_s,
int(embeddings_count or 0),
str(embeddings_max_updated_at or ""),
result_s,
updated_at,
),
)
class Tag:
def __init__(self, name: str, score: int, type: str, count=0, color = ""):
self.name = name

View File

@ -6,11 +6,14 @@ from concurrent.futures import ThreadPoolExecutor
import time
from PIL import Image
def generate_image_cache(dirs, size:str, verbose=False):
def generate_image_cache(dirs: List[str], size:str, verbose=True):
dirs = [r"C:\Users\zanllp\Desktop\repo\Z-Image-Turbo\client"]
start_time = time.time()
cache_base_dir = get_cache_dir()
verbose=True
def process_image(item):
if '\\node_modules\\' in item.path:
return
if item.is_dir():
verbose and print(f"Processing directory: {item.path}")
for sub_item in os.scandir(item.path):

1664
scripts/iib/topic_cluster.py Normal file

File diff suppressed because it is too large Load Diff

1
vue/components.d.ts vendored
View File

@ -31,6 +31,7 @@ declare module '@vue/runtime-core' {
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARow: typeof import('ant-design-vue/es')['Row']

View File

@ -1 +1 @@
import{d as E,bC as $,r as f,m as M,_ as T,a as c,an as W,h as m,c as v,P as z}from"./index-5ed9cd5a.js";var G=["prefixCls","name","id","type","disabled","readonly","tabindex","autofocus","value","required"],H={prefixCls:String,name:String,id:String,type:String,defaultChecked:{type:[Boolean,Number],default:void 0},checked:{type:[Boolean,Number],default:void 0},disabled:Boolean,tabindex:{type:[Number,String]},readonly:Boolean,autofocus:Boolean,value:z.any,required:Boolean};const L=E({compatConfig:{MODE:3},name:"Checkbox",inheritAttrs:!1,props:$(H,{prefixCls:"rc-checkbox",type:"checkbox",defaultChecked:!1}),emits:["click","change"],setup:function(a,d){var t=d.attrs,h=d.emit,g=d.expose,o=f(a.checked===void 0?a.defaultChecked:a.checked),i=f();M(function(){return a.checked},function(){o.value=a.checked}),g({focus:function(){var e;(e=i.value)===null||e===void 0||e.focus()},blur:function(){var e;(e=i.value)===null||e===void 0||e.blur()}});var l=f(),x=function(e){if(!a.disabled){a.checked===void 0&&(o.value=e.target.checked),e.shiftKey=l.value;var r={target:c(c({},a),{},{checked:e.target.checked}),stopPropagation:function(){e.stopPropagation()},preventDefault:function(){e.preventDefault()},nativeEvent:e};a.checked!==void 0&&(i.value.checked=!!a.checked),h("change",r),l.value=!1}},C=function(e){h("click",e),l.value=e.shiftKey};return function(){var n,e=a.prefixCls,r=a.name,s=a.id,p=a.type,b=a.disabled,K=a.readonly,P=a.tabindex,B=a.autofocus,S=a.value,N=a.required,_=T(a,G),q=t.class,D=t.onFocus,j=t.onBlur,w=t.onKeydown,A=t.onKeypress,F=t.onKeyup,y=c(c({},_),t),O=Object.keys(y).reduce(function(k,u){return(u.substr(0,5)==="aria-"||u.substr(0,5)==="data-"||u==="role")&&(k[u]=y[u]),k},{}),R=W(e,q,(n={},m(n,"".concat(e,"-checked"),o.value),m(n,"".concat(e,"-disabled"),b),n)),V=c(c({name:r,id:s,type:p,readonly:K,disabled:b,tabindex:P,class:"".concat(e,"-input"),checked:!!o.value,autofocus:B,value:S},O),{},{onChange:x,onClick:C,onFocus:D,onBlur:j,onKeydown:w,onKeypress:A,onKeyup:F,required:N});return v("span",{class:R},[v("input",c({ref:i},V),null),v("span",{class:"".concat(e,"-inner")},null)])}}});export{L as V};
import{d as E,bC as $,r as f,m as M,_ as T,a as c,an as W,h as m,c as v,P as z}from"./index-8b1d4076.js";var G=["prefixCls","name","id","type","disabled","readonly","tabindex","autofocus","value","required"],H={prefixCls:String,name:String,id:String,type:String,defaultChecked:{type:[Boolean,Number],default:void 0},checked:{type:[Boolean,Number],default:void 0},disabled:Boolean,tabindex:{type:[Number,String]},readonly:Boolean,autofocus:Boolean,value:z.any,required:Boolean};const L=E({compatConfig:{MODE:3},name:"Checkbox",inheritAttrs:!1,props:$(H,{prefixCls:"rc-checkbox",type:"checkbox",defaultChecked:!1}),emits:["click","change"],setup:function(a,d){var t=d.attrs,h=d.emit,g=d.expose,o=f(a.checked===void 0?a.defaultChecked:a.checked),i=f();M(function(){return a.checked},function(){o.value=a.checked}),g({focus:function(){var e;(e=i.value)===null||e===void 0||e.focus()},blur:function(){var e;(e=i.value)===null||e===void 0||e.blur()}});var l=f(),x=function(e){if(!a.disabled){a.checked===void 0&&(o.value=e.target.checked),e.shiftKey=l.value;var r={target:c(c({},a),{},{checked:e.target.checked}),stopPropagation:function(){e.stopPropagation()},preventDefault:function(){e.preventDefault()},nativeEvent:e};a.checked!==void 0&&(i.value.checked=!!a.checked),h("change",r),l.value=!1}},C=function(e){h("click",e),l.value=e.shiftKey};return function(){var n,e=a.prefixCls,r=a.name,s=a.id,p=a.type,b=a.disabled,K=a.readonly,P=a.tabindex,B=a.autofocus,S=a.value,N=a.required,_=T(a,G),q=t.class,D=t.onFocus,j=t.onBlur,w=t.onKeydown,A=t.onKeypress,F=t.onKeyup,y=c(c({},_),t),O=Object.keys(y).reduce(function(k,u){return(u.substr(0,5)==="aria-"||u.substr(0,5)==="data-"||u==="role")&&(k[u]=y[u]),k},{}),R=W(e,q,(n={},m(n,"".concat(e,"-checked"),o.value),m(n,"".concat(e,"-disabled"),b),n)),V=c(c({name:r,id:s,type:p,readonly:K,disabled:b,tabindex:P,class:"".concat(e,"-input"),checked:!!o.value,autofocus:B,value:S},O),{},{onChange:x,onClick:C,onFocus:D,onBlur:j,onKeydown:w,onKeypress:A,onKeyup:F,required:N});return v("span",{class:R},[v("input",c({ref:i},V),null),v("span",{class:"".concat(e,"-inner")},null)])}}});export{L as V};

File diff suppressed because one or more lines are too long

3
vue/dist/assets/FileItem-5c27aa5d.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{d as a,U as t,V as s,c as n,cD as _,a0 as o}from"./index-5ed9cd5a.js";const c={class:"img-sli-container"},i=a({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(l){return(e,r)=>(t(),s("div",c,[n(_,{left:e.left,right:e.right},null,8,["left","right"])]))}});const d=o(i,[["__scopeId","data-v-ae3fb9a8"]]);export{d as default};
import{d as a,U as t,V as s,c as n,cN as _,a0 as o}from"./index-8b1d4076.js";const c={class:"img-sli-container"},i=a({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(l){return(e,r)=>(t(),s("div",c,[n(_,{left:e.left,right:e.right},null,8,["left","right"])]))}});const d=o(i,[["__scopeId","data-v-ae3fb9a8"]]);export{d as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.container[data-v-aea581a5]{background:var(--zp-secondary-background);position:relative;height:var(--pane-max-height)}.action-bar[data-v-aea581a5]{display:flex;align-items:center;user-select:none;gap:6px;padding:6px 8px}.title[data-v-aea581a5]{font-weight:700;max-width:40vw}.file-list[data-v-aea581a5]{list-style:none;padding:8px;overflow:auto;height:calc(var(--pane-max-height) - 44px);width:100%}.no-res-hint[data-v-aea581a5]{height:calc(var(--pane-max-height) - 44px);display:flex;align-items:center;flex-direction:column;justify-content:center}.no-res-hint .hint[data-v-aea581a5]{font-size:1.2em;opacity:.7}.preview-switch[data-v-aea581a5]{position:fixed;bottom:24px;right:24px;display:flex;gap:8px;font-size:36px;user-select:none}.disable[data-v-aea581a5]{opacity:.3;pointer-events:none}

View File

@ -1 +0,0 @@
.full-screen-menu[data-v-50c80b83]{position:fixed;z-index:9999;background:var(--zp-primary-background);padding:8px 16px;box-shadow:0 0 4px var(--zp-secondary);border-radius:4px}.full-screen-menu .tags-container[data-v-50c80b83]{margin:4px 0}.full-screen-menu .tags-container .tag[data-v-50c80b83]{margin-right:4px;margin-bottom:4px;padding:2px 16px;border-radius:4px;display:inline-block;cursor:pointer;font-weight:700;transition:.5s all ease;border:2px solid var(--tag-color);color:var(--tag-color);background:var(--zp-primary-background);user-select:none}.full-screen-menu .tags-container .tag.selected[data-v-50c80b83]{background:var(--tag-color);color:#fff}.full-screen-menu .container[data-v-50c80b83]{height:100%;display:flex;overflow:hidden;flex-direction:column}.full-screen-menu .gen-info[data-v-50c80b83]{flex:1;word-break:break-all;white-space:pre-line;overflow:auto;z-index:1;padding-top:4px;position:relative}.full-screen-menu .gen-info code[data-v-50c80b83]{font-size:.9em;display:block;padding:4px;background:var(--zp-primary-background);border-radius:4px;margin-right:20px;white-space:pre-wrap;word-break:break-word;line-height:1.78em}.full-screen-menu .gen-info code[data-v-50c80b83] .natural-text{margin:.5em 0;line-height:1.6em;text-align:justify;color:var(--zp-primary)}.full-screen-menu .gen-info code[data-v-50c80b83] .short-tag{word-break:break-all;white-space:nowrap}.full-screen-menu .gen-info code[data-v-50c80b83] span.tag{background:var(--zp-secondary-variant-background);color:var(--zp-primary);padding:2px 4px;border-radius:6px;margin-right:6px;margin-top:4px;line-height:1.3em;display:inline-block}.full-screen-menu .gen-info code[data-v-50c80b83] .has-parentheses.tag{background:rgba(255,100,100,.14)}.full-screen-menu .gen-info code[data-v-50c80b83] span.tag:hover{background:rgba(120,0,0,.15)}.full-screen-menu .gen-info table[data-v-50c80b83]{font-size:1em;border-radius:4px;border-collapse:separate;margin-bottom:3em}.full-screen-menu .gen-info table tr td[data-v-50c80b83]:first-child{white-space:nowrap}.full-screen-menu .gen-info table td[data-v-50c80b83]{padding-right:14px;padding-left:4px;border-bottom:1px solid var(--zp-secondary);border-collapse:collapse}.full-screen-menu .gen-info .info-tags .info-tag[data-v-50c80b83]{display:inline-block;overflow:hidden;border-radius:4px;margin-right:8px;border:2px solid var(--zp-primary)}.full-screen-menu .gen-info .info-tags .name[data-v-50c80b83]{background-color:var(--zp-primary);color:var(--zp-primary-background);padding:4px;border-bottom-right-radius:4px}.full-screen-menu .gen-info .info-tags .value[data-v-50c80b83]{padding:4px}.full-screen-menu.unset-size[data-v-50c80b83]{width:unset!important;height:unset!important}.full-screen-menu .mouse-sensor[data-v-50c80b83]{position:absolute;bottom:0;right:0;transform:rotate(90deg);cursor:se-resize;z-index:1;background:var(--zp-primary-background);border-radius:2px}.full-screen-menu .mouse-sensor>*[data-v-50c80b83]{font-size:18px;padding:4px}.full-screen-menu .action-bar[data-v-50c80b83]{display:flex;align-items:center;user-select:none;gap:4px}.full-screen-menu .action-bar .icon[data-v-50c80b83]{font-size:1.5em;padding:2px 4px;border-radius:4px}.full-screen-menu .action-bar .icon[data-v-50c80b83]:hover{background:var(--zp-secondary-variant-background)}.full-screen-menu .action-bar>*[data-v-50c80b83]{flex-wrap:wrap}.full-screen-menu.lr[data-v-50c80b83]{top:var(--3eebcda7)!important;right:0!important;bottom:0!important;left:100vw!important;height:unset!important;width:var(--3e27f0da)!important;transition:left ease .3s}.full-screen-menu.lr.always-on[data-v-50c80b83],.full-screen-menu.lr.mouse-in[data-v-50c80b83]{left:var(--37c6591e)!important}.tag-alpha-item[data-v-50c80b83]{display:flex;margin-top:4px}.tag-alpha-item h4[data-v-50c80b83]{width:32px;flex-shrink:0}.sort-tag-switch[data-v-50c80b83]{display:inline-block;padding-right:16px;padding-left:8px;cursor:pointer;user-select:none}.sort-tag-switch span[data-v-50c80b83]{transition:all ease .3s;transform:scale(1.2)}.sort-tag-switch:hover span[data-v-50c80b83]{transform:scale(1.3)}.lr-layout-control[data-v-50c80b83]{display:flex;align-items:center;gap:16px;padding:4px 8px;flex-wrap:wrap;border-radius:2px;border-left:3px solid var(--zp-luminous);background-color:var(--zp-secondary-background)}.lr-layout-control .ctrl-item[data-v-50c80b83]{display:flex;align-items:center;gap:4px;flex-wrap:nowrap}.select-actions[data-v-b04c3508]>:not(:last-child){margin-right:4px}.float-panel[data-v-b04c3508]{position:absolute;bottom:32px;right:32px;background:var(--zp-primary-background);border-radius:4px;z-index:1000;padding:8px;box-shadow:0 0 4px var(--zp-secondary)}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.full-screen-menu[data-v-c7e0b9b7]{position:fixed;z-index:9999;background:var(--zp-primary-background);padding:8px 16px;box-shadow:0 0 4px var(--zp-secondary);border-radius:4px}.full-screen-menu .tags-container[data-v-c7e0b9b7]{margin:4px 0}.full-screen-menu .tags-container .tag[data-v-c7e0b9b7]{margin-right:4px;margin-bottom:4px;padding:2px 16px;border-radius:4px;display:inline-block;cursor:pointer;font-weight:700;transition:.5s all ease;border:2px solid var(--tag-color);color:var(--tag-color);background:var(--zp-primary-background);user-select:none}.full-screen-menu .tags-container .tag.selected[data-v-c7e0b9b7]{background:var(--tag-color);color:#fff}.full-screen-menu .container[data-v-c7e0b9b7]{height:100%;display:flex;overflow:hidden;flex-direction:column}.full-screen-menu .gen-info[data-v-c7e0b9b7]{flex:1;word-break:break-all;white-space:pre-line;overflow:auto;z-index:1;padding-top:4px;position:relative}.full-screen-menu .gen-info code[data-v-c7e0b9b7]{font-size:.9em;display:block;padding:4px;background:var(--zp-primary-background);border-radius:4px;margin-right:20px;white-space:pre-wrap;word-break:break-word;line-height:1.78em}.full-screen-menu .gen-info code[data-v-c7e0b9b7] .natural-text{margin:.5em 0;line-height:1.6em;text-align:justify;color:var(--zp-primary)}.full-screen-menu .gen-info code[data-v-c7e0b9b7] .short-tag{word-break:break-all;white-space:nowrap}.full-screen-menu .gen-info code[data-v-c7e0b9b7] span.tag{background:var(--zp-secondary-variant-background);color:var(--zp-primary);padding:2px 4px;border-radius:6px;margin-right:6px;margin-top:4px;line-height:1.3em;display:inline-block}.full-screen-menu .gen-info code[data-v-c7e0b9b7] .has-parentheses.tag{background:rgba(255,100,100,.14)}.full-screen-menu .gen-info code[data-v-c7e0b9b7] span.tag:hover{background:rgba(120,0,0,.15)}.full-screen-menu .gen-info table[data-v-c7e0b9b7]{font-size:1em;border-radius:4px;border-collapse:separate;margin-bottom:3em}.full-screen-menu .gen-info table tr td[data-v-c7e0b9b7]:first-child{white-space:nowrap;vertical-align:top}.full-screen-menu .gen-info table.extra-meta-table .extra-meta-value[data-v-c7e0b9b7]{display:block;max-height:200px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:.85em;background:var(--zp-secondary-variant-background);padding:8px;border-radius:4px}.full-screen-menu .gen-info table td[data-v-c7e0b9b7]{padding-right:14px;padding-left:4px;border-bottom:1px solid var(--zp-secondary);border-collapse:collapse}.full-screen-menu .gen-info .info-tags .info-tag[data-v-c7e0b9b7]{display:inline-block;overflow:hidden;border-radius:4px;margin-right:8px;border:2px solid var(--zp-primary)}.full-screen-menu .gen-info .info-tags .name[data-v-c7e0b9b7]{background-color:var(--zp-primary);color:var(--zp-primary-background);padding:4px;border-bottom-right-radius:4px}.full-screen-menu .gen-info .info-tags .value[data-v-c7e0b9b7]{padding:4px}.full-screen-menu.unset-size[data-v-c7e0b9b7]{width:unset!important;height:unset!important}.full-screen-menu .mouse-sensor[data-v-c7e0b9b7]{position:absolute;bottom:0;right:0;transform:rotate(90deg);cursor:se-resize;z-index:1;background:var(--zp-primary-background);border-radius:2px}.full-screen-menu .mouse-sensor>*[data-v-c7e0b9b7]{font-size:18px;padding:4px}.full-screen-menu .action-bar[data-v-c7e0b9b7]{display:flex;align-items:center;user-select:none;gap:4px}.full-screen-menu .action-bar .icon[data-v-c7e0b9b7]{font-size:1.5em;padding:2px 4px;border-radius:4px}.full-screen-menu .action-bar .icon[data-v-c7e0b9b7]:hover{background:var(--zp-secondary-variant-background)}.full-screen-menu .action-bar>*[data-v-c7e0b9b7]{flex-wrap:wrap}.full-screen-menu.lr[data-v-c7e0b9b7]{top:var(--b7cd59ce)!important;right:0!important;bottom:0!important;left:100vw!important;height:unset!important;width:var(--0e09e1cc)!important;transition:left ease .3s}.full-screen-menu.lr.always-on[data-v-c7e0b9b7],.full-screen-menu.lr.mouse-in[data-v-c7e0b9b7]{left:var(--62228ae0)!important}.tag-alpha-item[data-v-c7e0b9b7]{display:flex;margin-top:4px}.tag-alpha-item h4[data-v-c7e0b9b7]{width:32px;flex-shrink:0}.sort-tag-switch[data-v-c7e0b9b7]{display:inline-block;padding-right:16px;padding-left:8px;cursor:pointer;user-select:none}.sort-tag-switch span[data-v-c7e0b9b7]{transition:all ease .3s;transform:scale(1.2)}.sort-tag-switch:hover span[data-v-c7e0b9b7]{transform:scale(1.3)}.lr-layout-control[data-v-c7e0b9b7]{display:flex;align-items:center;gap:16px;padding:4px 8px;flex-wrap:wrap;border-radius:2px;border-left:3px solid var(--zp-luminous);background-color:var(--zp-secondary-background)}.lr-layout-control .ctrl-item[data-v-c7e0b9b7]{display:flex;align-items:center;gap:4px;flex-wrap:nowrap}.select-actions[data-v-b04c3508]>:not(:last-child){margin-right:4px}.float-panel[data-v-b04c3508]{position:absolute;bottom:32px;right:32px;background:var(--zp-primary-background);border-radius:4px;z-index:1000;padding:8px;box-shadow:0 0 4px var(--zp-secondary)}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{bU as i,b1 as t,e0 as f,bL as n}from"./index-5ed9cd5a.js";function u(e,s,r){if(!i(r))return!1;var a=typeof s;return(a=="number"?t(r)&&f(s,r.length):a=="string"&&s in r)?n(r[s],e):!1}export{u as i};
import{bV as i,b1 as t,e5 as f,bM as n}from"./index-8b1d4076.js";function u(e,s,r){if(!i(r))return!1;var a=typeof s;return(a=="number"?t(r)&&f(s,r.length):a=="string"&&s in r)?n(r[s],e):!1}export{u as i};

View File

@ -1 +0,0 @@
import{d as z,a1 as B,cE as $,cb as S,U as _,V as w,W as f,c as l,a3 as d,X as p,Y as c,a4 as s,a2 as A,af as E,cF as R,cG as y,z as V,B as x,ak as T,a0 as U}from"./index-5ed9cd5a.js";import{_ as N}from"./index-4e015155.js";import{u as L,a as G,f as H,F as O,d as W}from"./FileItem-2ecfe4d5.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-47577760.js";/* empty css */import"./_isIterateeCall-cd370691.js";import"./index-7cbf21fe.js";const j={class:"actions-panel actions"},q={class:"item"},P={key:0,class:"file-list"},Q={class:"hint"},X=z({__name:"batchDownload",props:{tabIdx:{},paneIdx:{},id:{}},setup(Y){const{stackViewEl:b}=L().toRefs(),{itemSize:h,gridItems:D,cellWidth:g}=G(),i=B(),m=H(),{selectdFiles:o}=$(m),r=S(),v=async e=>{const t=R(e);t&&m.addFiles(t.nodes)},C=async()=>{r.pushAction(async()=>{const e=await y.value.post("/zip",{paths:o.value.map(u=>u.fullpath),compress:i.batchDownloadCompress,pack_only:!1},{responseType:"blob"}),t=window.URL.createObjectURL(new Blob([e.data])),a=document.createElement("a");a.href=t,a.setAttribute("download",`iib_${new Date().toLocaleString()}.zip`),document.body.appendChild(a),a.click()})},I=async()=>{r.pushAction(async()=>{await y.value.post("/zip",{paths:o.value.map(e=>e.fullpath),compress:i.batchDownloadCompress,pack_only:!0},{responseType:"blob"}),V.success(x("success"))})},F=e=>{o.value.splice(e,1)};return(e,t)=>{const a=T,u=N;return _(),w("div",{class:"container",ref_key:"stackViewEl",ref:b,onDrop:v},[f("div",j,[l(a,{onClick:t[0]||(t[0]=n=>s(m).selectdFiles=[])},{default:d(()=>[p(c(e.$t("clear")),1)]),_:1}),f("div",q,[p(c(e.$t("compressFile"))+": ",1),l(u,{checked:s(i).batchDownloadCompress,"onUpdate:checked":t[1]||(t[1]=n=>s(i).batchDownloadCompress=n)},null,8,["checked"])]),l(a,{onClick:I,type:"primary",loading:!s(r).isIdle},{default:d(()=>[p(c(e.$t("packOnlyNotDownload")),1)]),_:1},8,["loading"]),l(a,{onClick:C,type:"primary",loading:!s(r).isIdle},{default:d(()=>[p(c(e.$t("zipDownload")),1)]),_:1},8,["loading"])]),s(o).length?(_(),A(s(W),{key:1,ref:"scroller",class:"file-list",items:s(o).slice(),"item-size":s(h).first,"key-field":"fullpath","item-secondary-size":s(h).second,gridItems:s(D)},{default:d(({item:n,index:k})=>[l(O,{idx:k,file:n,"cell-width":s(g),"enable-close-icon":"",onCloseIconClick:J=>F(k),"full-screen-preview-image-url":s(E)(n),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","onCloseIconClick","full-screen-preview-image-url"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])):(_(),w("div",P,[f("p",Q,c(e.$t("batchDownloaDDragAndDropHint")),1)]))],544)}}});const oe=U(X,[["__scopeId","data-v-a2642a17"]]);export{oe as default};

View File

@ -0,0 +1 @@
import{d as F,a1 as B,cO as $,cc as S,U as _,V as w,W as f,c as l,a3 as d,X as p,Y as c,a4 as s,a2 as A,af as R,cP as V,cQ as y,z as x,B as E,ak as T,a0 as U}from"./index-8b1d4076.js";import{_ as N}from"./index-19cfb514.js";import{u as L,a as O,f as H,F as P,d as Q}from"./FileItem-5c27aa5d.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-26786c56.js";/* empty css */import"./index-fd0b9b75.js";import"./_isIterateeCall-4f946453.js";import"./index-404f2353.js";import"./index-133a27d3.js";const W={class:"actions-panel actions"},j={class:"item"},q={key:0,class:"file-list"},G={class:"hint"},X=F({__name:"batchDownload",props:{tabIdx:{},paneIdx:{},id:{}},setup(Y){const{stackViewEl:D}=L().toRefs(),{itemSize:h,gridItems:b,cellWidth:g}=O(),i=B(),m=H(),{selectdFiles:a}=$(m),r=S(),v=async e=>{const t=V(e);t&&m.addFiles(t.nodes)},C=async()=>{r.pushAction(async()=>{const e=await y.value.post("/zip",{paths:a.value.map(u=>u.fullpath),compress:i.batchDownloadCompress,pack_only:!1},{responseType:"blob"}),t=window.URL.createObjectURL(new Blob([e.data])),o=document.createElement("a");o.href=t,o.setAttribute("download",`iib_${new Date().toLocaleString()}.zip`),document.body.appendChild(o),o.click()})},I=async()=>{r.pushAction(async()=>{await y.value.post("/zip",{paths:a.value.map(e=>e.fullpath),compress:i.batchDownloadCompress,pack_only:!0},{responseType:"blob"}),x.success(E("success"))})},z=e=>{a.value.splice(e,1)};return(e,t)=>{const o=T,u=N;return _(),w("div",{class:"container",ref_key:"stackViewEl",ref:D,onDrop:v},[f("div",W,[l(o,{onClick:t[0]||(t[0]=n=>s(m).selectdFiles=[])},{default:d(()=>[p(c(e.$t("clear")),1)]),_:1}),f("div",j,[p(c(e.$t("compressFile"))+": ",1),l(u,{checked:s(i).batchDownloadCompress,"onUpdate:checked":t[1]||(t[1]=n=>s(i).batchDownloadCompress=n)},null,8,["checked"])]),l(o,{onClick:I,type:"primary",loading:!s(r).isIdle},{default:d(()=>[p(c(e.$t("packOnlyNotDownload")),1)]),_:1},8,["loading"]),l(o,{onClick:C,type:"primary",loading:!s(r).isIdle},{default:d(()=>[p(c(e.$t("zipDownload")),1)]),_:1},8,["loading"])]),s(a).length?(_(),A(s(Q),{key:1,ref:"scroller",class:"file-list",items:s(a).slice(),"item-size":s(h).first,"key-field":"fullpath","item-secondary-size":s(h).second,gridItems:s(b)},{default:d(({item:n,index:k})=>[l(P,{idx:k,file:n,"cell-width":s(g),"enable-close-icon":"",onCloseIconClick:J=>z(k),"full-screen-preview-image-url":s(R)(n),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","onCloseIconClick","full-screen-preview-image-url"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])):(_(),w("div",q,[f("p",G,c(e.$t("batchDownloaDDragAndDropHint")),1)]))],544)}}});const le=U(X,[["__scopeId","data-v-a2642a17"]]);export{le as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
vue/dist/assets/gridView-64cad325.js vendored Normal file
View File

@ -0,0 +1 @@
import{u as w,a as y,F as k,d as x}from"./FileItem-5c27aa5d.js";import{d as h,a1 as F,c9 as b,r as D,bh as I,bl as C,U as V,V as E,c,a3 as z,a4 as e,af as S,cP as B,cR as R,a0 as A}from"./index-8b1d4076.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-26786c56.js";/* empty css */import"./index-fd0b9b75.js";import"./_isIterateeCall-4f946453.js";import"./index-404f2353.js";import"./index-133a27d3.js";const K=h({__name:"gridView",props:{tabIdx:{},paneIdx:{},id:{},removable:{type:Boolean},allowDragAndDrop:{type:Boolean},files:{},paneKey:{}},setup(p){const o=p,m=F(),{stackViewEl:d}=w().toRefs(),{itemSize:i,gridItems:u,cellWidth:f}=y(),g=b(),a=D(o.files??[]),_=async s=>{const l=B(s);o.allowDragAndDrop&&l&&(a.value=R([...a.value,...l.nodes]))},v=s=>{a.value.splice(s,1)};return I(()=>{m.pageFuncExportMap.set(o.paneKey,{getFiles:()=>C(a.value),setFiles:s=>a.value=s})}),(s,l)=>(V(),E("div",{class:"container",ref_key:"stackViewEl",ref:d,onDrop:_},[c(e(x),{ref:"scroller",class:"file-list",items:a.value.slice(),"item-size":e(i).first,"key-field":"fullpath","item-secondary-size":e(i).second,gridItems:e(u)},{default:z(({item:t,index:r})=>{var n;return[c(k,{idx:r,file:t,"cell-width":e(f),"enable-close-icon":o.removable,onCloseIconClick:T=>v(r),"full-screen-preview-image-url":e(S)(t),"extra-tags":(n=t==null?void 0:t.tags)==null?void 0:n.map(e(g).tagConvert),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","enable-close-icon","onCloseIconClick","full-screen-preview-image-url","extra-tags"])]}),_:1},8,["items","item-size","item-secondary-size","gridItems"])],544))}});const W=A(K,[["__scopeId","data-v-f35f4802"]]);export{W as default};

View File

@ -1 +0,0 @@
import{u as w,a as y,F as k,d as x}from"./FileItem-2ecfe4d5.js";import{d as F,a1 as h,c8 as b,r as D,bh as I,bl as C,U as V,V as E,c,a3 as z,a4 as e,af as S,cF as B,cH as R,a0 as A}from"./index-5ed9cd5a.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-47577760.js";/* empty css */import"./_isIterateeCall-cd370691.js";import"./index-7cbf21fe.js";const H=F({__name:"gridView",props:{tabIdx:{},paneIdx:{},id:{},removable:{type:Boolean},allowDragAndDrop:{type:Boolean},files:{},paneKey:{}},setup(p){const o=p,d=h(),{stackViewEl:m}=w().toRefs(),{itemSize:i,gridItems:u,cellWidth:f}=y(),g=b(),a=D(o.files??[]),_=async s=>{const l=B(s);o.allowDragAndDrop&&l&&(a.value=R([...a.value,...l.nodes]))},v=s=>{a.value.splice(s,1)};return I(()=>{d.pageFuncExportMap.set(o.paneKey,{getFiles:()=>C(a.value),setFiles:s=>a.value=s})}),(s,l)=>(V(),E("div",{class:"container",ref_key:"stackViewEl",ref:m,onDrop:_},[c(e(x),{ref:"scroller",class:"file-list",items:a.value.slice(),"item-size":e(i).first,"key-field":"fullpath","item-secondary-size":e(i).second,gridItems:e(u)},{default:z(({item:t,index:r})=>{var n;return[c(k,{idx:r,file:t,"cell-width":e(f),"enable-close-icon":o.removable,onCloseIconClick:K=>v(r),"full-screen-preview-image-url":e(S)(t),"extra-tags":(n=t==null?void 0:t.tags)==null?void 0:n.map(e(g).tagConvert),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","enable-close-icon","onCloseIconClick","full-screen-preview-image-url","extra-tags"])]}),_:1},8,["items","item-size","item-secondary-size","gridItems"])],544))}});const N=A(H,[["__scopeId","data-v-f35f4802"]]);export{N as default};

View File

@ -1 +1 @@
import{am as F,r as g,l as P,k as A,O as b,G as R,cb as q,cm as O,cr as z}from"./index-5ed9cd5a.js";import{u as L,a as Q,b as j,e as H}from"./FileItem-2ecfe4d5.js";import{a as T,b as U,c as W}from"./MultiSelectKeep-68ce9bb5.js";import{u as B}from"./useGenInfoDiff-b31dce1f.js";let K=0;const V=()=>++K,X=(n,i,{dataUpdateStrategy:l="replace"}={})=>{const o=F([""]),c=g(!1),t=g(),a=g(!1);let f=g(-1);const v=new Set,w=e=>{var s;l==="replace"?t.value=e:l==="merge"&&(b((Array.isArray(t.value)||typeof t.value>"u")&&Array.isArray(e),"数据更新策略为合并时仅可用于值为数组的情况"),t.value=[...(s=t==null?void 0:t.value)!==null&&s!==void 0?s:[],...e])},d=e=>A(void 0,void 0,void 0,function*(){if(a.value||c.value&&typeof e>"u")return!1;a.value=!0;const s=V();f.value=s;try{let r;if(typeof e=="number"){if(r=o[e],typeof r!="string")return!1}else r=o[o.length-1];const h=yield n(r);if(v.has(s))return v.delete(s),!1;w(i(h));const u=h.cursor;if((e===o.length-1||typeof e!="number")&&(c.value=!u.has_next,u.has_next)){const m=u.next_cursor||u.next;b(typeof m=="string"),o.push(m)}}finally{f.value===s&&(a.value=!1)}return!0}),p=()=>{v.add(f.value),a.value=!1},x=(e=!1)=>A(void 0,void 0,void 0,function*(){const{refetch:s,force:r}=typeof e=="object"?e:{refetch:e};r&&p(),b(!a.value),o.splice(0,o.length,""),a.value=!1,t.value=void 0,c.value=!1,s&&(yield d())}),I=()=>({next:()=>A(void 0,void 0,void 0,function*(){if(a.value)throw new Error("不允许同时迭代");return{done:!(yield d()),value:t.value}})});return P({abort:p,load:c,next:d,res:t,loading:a,cursorStack:o,reset:x,[Symbol.asyncIterator]:I,iter:{[Symbol.asyncIterator]:I}})},se=n=>F(X(n,i=>i.files,{dataUpdateStrategy:"merge"})),ne=n=>{const i=F(new Set),l=R(()=>(n.res??[]).filter(y=>!i.has(y.fullpath))),o=q(),{stackViewEl:c,multiSelectedIdxs:t,stack:a,scroller:f,props:v}=L({images:l}).toRefs(),{itemSize:w,gridItems:d,cellWidth:p,onScroll:x}=Q({fetchNext:()=>n.next()}),{showMenuIdx:I}=j(),{onFileDragStart:e,onFileDragEnd:s}=T(),{showGenInfo:r,imageGenInfo:h,q:u,onContextMenuClick:m,onFileItemClick:C}=U({openNext:O}),{previewIdx:_,previewing:E,onPreviewVisibleChange:M,previewImgMove:D,canPreview:G}=W({loadNext:()=>n.next()}),J=async(y,S,N)=>{a.value=[{curr:"",files:l.value}],await m(y,S,N)};H("removeFiles",async({paths:y})=>{y.forEach(S=>i.add(S))});const k=()=>{z(l.value)};return{images:l,scroller:f,queue:o,iter:n,onContextMenuClickU:J,stackViewEl:c,previewIdx:_,previewing:E,onPreviewVisibleChange:M,previewImgMove:D,canPreview:G,itemSize:w,gridItems:d,showGenInfo:r,imageGenInfo:h,q:u,onContextMenuClick:m,onFileItemClick:C,showMenuIdx:I,multiSelectedIdxs:t,onFileDragStart:e,onFileDragEnd:s,cellWidth:p,onScroll:x,saveLoadedFileAsJson:k,saveAllFileAsJson:async()=>{for(;!n.load;)await n.next();k()},props:v,...B()}};export{se as c,ne as u};
import{am as F,r as g,l as P,k as A,O as b,G as R,cc as q,cn as O,cs as z}from"./index-8b1d4076.js";import{u as L,a as Q,b as j,e as H}from"./FileItem-5c27aa5d.js";import{a as T,b as U,c as W}from"./MultiSelectKeep-4ce030ff.js";import{u as B}from"./useGenInfoDiff-068a10f2.js";let K=0;const V=()=>++K,X=(n,i,{dataUpdateStrategy:l="replace"}={})=>{const o=F([""]),c=g(!1),t=g(),a=g(!1);let f=g(-1);const v=new Set,w=e=>{var s;l==="replace"?t.value=e:l==="merge"&&(b((Array.isArray(t.value)||typeof t.value>"u")&&Array.isArray(e),"数据更新策略为合并时仅可用于值为数组的情况"),t.value=[...(s=t==null?void 0:t.value)!==null&&s!==void 0?s:[],...e])},d=e=>A(void 0,void 0,void 0,function*(){if(a.value||c.value&&typeof e>"u")return!1;a.value=!0;const s=V();f.value=s;try{let r;if(typeof e=="number"){if(r=o[e],typeof r!="string")return!1}else r=o[o.length-1];const h=yield n(r);if(v.has(s))return v.delete(s),!1;w(i(h));const u=h.cursor;if((e===o.length-1||typeof e!="number")&&(c.value=!u.has_next,u.has_next)){const m=u.next_cursor||u.next;b(typeof m=="string"),o.push(m)}}finally{f.value===s&&(a.value=!1)}return!0}),p=()=>{v.add(f.value),a.value=!1},x=(e=!1)=>A(void 0,void 0,void 0,function*(){const{refetch:s,force:r}=typeof e=="object"?e:{refetch:e};r&&p(),b(!a.value),o.splice(0,o.length,""),a.value=!1,t.value=void 0,c.value=!1,s&&(yield d())}),I=()=>({next:()=>A(void 0,void 0,void 0,function*(){if(a.value)throw new Error("不允许同时迭代");return{done:!(yield d()),value:t.value}})});return P({abort:p,load:c,next:d,res:t,loading:a,cursorStack:o,reset:x,[Symbol.asyncIterator]:I,iter:{[Symbol.asyncIterator]:I}})},se=n=>F(X(n,i=>i.files,{dataUpdateStrategy:"merge"})),ne=n=>{const i=F(new Set),l=R(()=>(n.res??[]).filter(y=>!i.has(y.fullpath))),o=q(),{stackViewEl:c,multiSelectedIdxs:t,stack:a,scroller:f,props:v}=L({images:l}).toRefs(),{itemSize:w,gridItems:d,cellWidth:p,onScroll:x}=Q({fetchNext:()=>n.next()}),{showMenuIdx:I}=j(),{onFileDragStart:e,onFileDragEnd:s}=T(),{showGenInfo:r,imageGenInfo:h,q:u,onContextMenuClick:m,onFileItemClick:C}=U({openNext:O}),{previewIdx:_,previewing:E,onPreviewVisibleChange:M,previewImgMove:D,canPreview:G}=W({loadNext:()=>n.next()}),J=async(y,S,N)=>{a.value=[{curr:"",files:l.value}],await m(y,S,N)};H("removeFiles",async({paths:y})=>{y.forEach(S=>i.add(S))});const k=()=>{z(l.value)};return{images:l,scroller:f,queue:o,iter:n,onContextMenuClickU:J,stackViewEl:c,previewIdx:_,previewing:E,onPreviewVisibleChange:M,previewImgMove:D,canPreview:G,itemSize:w,gridItems:d,showGenInfo:r,imageGenInfo:h,q:u,onContextMenuClick:m,onFileItemClick:C,showMenuIdx:I,multiSelectedIdxs:t,onFileDragStart:e,onFileDragEnd:s,cellWidth:p,onScroll:x,saveLoadedFileAsJson:k,saveAllFileAsJson:async()=>{for(;!n.load;)await n.next();k()},props:v,...B()}};export{se as c,ne as u};

1
vue/dist/assets/index-017a4092.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
vue/dist/assets/index-19cfb514.js vendored Normal file
View File

@ -0,0 +1 @@
import{cw as j,ay as z,d as K,j as U,du as $,w as g,r as b,G as S,m as A,u as D,o as E,az as G,h as d,c as s,a as C,aw as H,bf as L,g as _,dv as W,P as u,dw as x}from"./index-8b1d4076.js";var R=z("small","default"),q=function(){return{id:String,prefixCls:String,size:u.oneOf(R),disabled:{type:Boolean,default:void 0},checkedChildren:u.any,unCheckedChildren:u.any,tabindex:u.oneOfType([u.string,u.number]),autofocus:{type:Boolean,default:void 0},loading:{type:Boolean,default:void 0},checked:u.oneOfType([u.string,u.number,u.looseBool]),checkedValue:u.oneOfType([u.string,u.number,u.looseBool]).def(!0),unCheckedValue:u.oneOfType([u.string,u.number,u.looseBool]).def(!1),onChange:{type:Function},onClick:{type:Function},onKeydown:{type:Function},onMouseup:{type:Function},"onUpdate:checked":{type:Function},onBlur:Function,onFocus:Function}},J=K({compatConfig:{MODE:3},name:"ASwitch",__ANT_SWITCH:!0,inheritAttrs:!1,props:q(),slots:["checkedChildren","unCheckedChildren"],setup:function(n,r){var o=r.attrs,y=r.slots,B=r.expose,l=r.emit,m=U();$(function(){g(!("defaultChecked"in o),"Switch","'defaultChecked' is deprecated, please use 'v-model:checked'"),g(!("value"in o),"Switch","`value` is not validate prop, do you mean `checked`?")});var h=b(n.checked!==void 0?n.checked:o.defaultChecked),f=S(function(){return h.value===n.checkedValue});A(function(){return n.checked},function(){h.value=n.checked});var v=D("switch",n),c=v.prefixCls,F=v.direction,T=v.size,i=b(),w=function(){var e;(e=i.value)===null||e===void 0||e.focus()},V=function(){var e;(e=i.value)===null||e===void 0||e.blur()};B({focus:w,blur:V}),E(function(){G(function(){n.autofocus&&!n.disabled&&i.value.focus()})});var k=function(e,t){n.disabled||(l("update:checked",e),l("change",e,t),m.onFieldChange())},I=function(e){l("blur",e)},N=function(e){w();var t=f.value?n.unCheckedValue:n.checkedValue;k(t,e),l("click",t,e)},M=function(e){e.keyCode===x.LEFT?k(n.unCheckedValue,e):e.keyCode===x.RIGHT&&k(n.checkedValue,e),l("keydown",e)},O=function(e){var t;(t=i.value)===null||t===void 0||t.blur(),l("mouseup",e)},P=S(function(){var a;return a={},d(a,"".concat(c.value,"-small"),T.value==="small"),d(a,"".concat(c.value,"-loading"),n.loading),d(a,"".concat(c.value,"-checked"),f.value),d(a,"".concat(c.value,"-disabled"),n.disabled),d(a,c.value,!0),d(a,"".concat(c.value,"-rtl"),F.value==="rtl"),a});return function(){var a;return s(W,{insertExtraNode:!0},{default:function(){return[s("button",C(C(C({},H(n,["prefixCls","checkedChildren","unCheckedChildren","checked","autofocus","checkedValue","unCheckedValue","id","onChange","onUpdate:checked"])),o),{},{id:(a=n.id)!==null&&a!==void 0?a:m.id.value,onKeydown:M,onClick:N,onBlur:I,onMouseup:O,type:"button",role:"switch","aria-checked":h.value,disabled:n.disabled||n.loading,class:[o.class,P.value],ref:i}),[s("div",{class:"".concat(c.value,"-handle")},[n.loading?s(L,{class:"".concat(c.value,"-loading-icon")},null):null]),s("span",{class:"".concat(c.value,"-inner")},[f.value?_(y,n,"checkedChildren"):_(y,n,"unCheckedChildren")])])]}})}}});const X=j(J);export{X as _};

View File

@ -1 +1 @@
import{d as w,bC as A,av as D,cz as j,az as k,n as B,cA as V,cB as y,e as $,c as a,_ as T,h as r,a as P,cC as M,P as b}from"./index-5ed9cd5a.js";var O=["class","style"],W=function(){return{prefixCls:String,spinning:{type:Boolean,default:void 0},size:String,wrapperClassName:String,tip:b.any,delay:Number,indicator:b.any}},p=null;function q(t,n){return!!t&&!!n&&!isNaN(Number(n))}function G(t){var n=t.indicator;p=typeof n=="function"?n:function(){return a(n,null,null)}}const H=w({compatConfig:{MODE:3},name:"ASpin",inheritAttrs:!1,props:A(W(),{size:"default",spinning:!0,wrapperClassName:""}),setup:function(){return{originalUpdateSpinning:null,configProvider:D("configProvider",j)}},data:function(){var n=this.spinning,e=this.delay,i=q(n,e);return{sSpinning:n&&!i}},created:function(){this.originalUpdateSpinning=this.updateSpinning,this.debouncifyUpdateSpinning(this.$props)},mounted:function(){this.updateSpinning()},updated:function(){var n=this;k(function(){n.debouncifyUpdateSpinning(),n.updateSpinning()})},beforeUnmount:function(){this.cancelExistingSpin()},methods:{debouncifyUpdateSpinning:function(n){var e=n||this.$props,i=e.delay;i&&(this.cancelExistingSpin(),this.updateSpinning=B(this.originalUpdateSpinning,i))},updateSpinning:function(){var n=this.spinning,e=this.sSpinning;e!==n&&(this.sSpinning=n)},cancelExistingSpin:function(){var n=this.updateSpinning;n&&n.cancel&&n.cancel()},renderIndicator:function(n){var e="".concat(n,"-dot"),i=V(this,"indicator");return i===null?null:(Array.isArray(i)&&(i=i.length===1?i[0]:i),y(i)?$(i,{class:e}):p&&y(p())?$(p(),{class:e}):a("span",{class:"".concat(e," ").concat(n,"-dot-spin")},[a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null)]))}},render:function(){var n,e,i,o=this.$props,f=o.size,x=o.prefixCls,h=o.tip,d=h===void 0?(n=(e=this.$slots).tip)===null||n===void 0?void 0:n.call(e):h,C=o.wrapperClassName,l=this.$attrs,v=l.class,N=l.style,_=T(l,O),S=this.configProvider,U=S.getPrefixCls,z=S.direction,s=U("spin",x),u=this.sSpinning,E=(i={},r(i,s,!0),r(i,"".concat(s,"-sm"),f==="small"),r(i,"".concat(s,"-lg"),f==="large"),r(i,"".concat(s,"-spinning"),u),r(i,"".concat(s,"-show-text"),!!d),r(i,"".concat(s,"-rtl"),z==="rtl"),r(i,v,!!v),i),m=a("div",P(P({},_),{},{style:N,class:E}),[this.renderIndicator(s),d?a("div",{class:"".concat(s,"-text")},[d]):null]),g=M(this);if(g&&g.length){var c,I=(c={},r(c,"".concat(s,"-container"),!0),r(c,"".concat(s,"-blur"),u),c);return a("div",{class:["".concat(s,"-nested-loading"),C]},[u&&a("div",{key:"loading"},[m]),a("div",{class:I,key:"container"},[g])])}return m}});export{H as S,G as s};
import{d as w,bC as D,av as A,cJ as j,az as k,n as V,cK as B,cL as y,e as $,c as a,_ as M,h as r,a as P,cM as T,P as b}from"./index-8b1d4076.js";var J=["class","style"],K=function(){return{prefixCls:String,spinning:{type:Boolean,default:void 0},size:String,wrapperClassName:String,tip:b.any,delay:Number,indicator:b.any}},p=null;function L(t,n){return!!t&&!!n&&!isNaN(Number(n))}function W(t){var n=t.indicator;p=typeof n=="function"?n:function(){return a(n,null,null)}}const q=w({compatConfig:{MODE:3},name:"ASpin",inheritAttrs:!1,props:D(K(),{size:"default",spinning:!0,wrapperClassName:""}),setup:function(){return{originalUpdateSpinning:null,configProvider:A("configProvider",j)}},data:function(){var n=this.spinning,e=this.delay,i=L(n,e);return{sSpinning:n&&!i}},created:function(){this.originalUpdateSpinning=this.updateSpinning,this.debouncifyUpdateSpinning(this.$props)},mounted:function(){this.updateSpinning()},updated:function(){var n=this;k(function(){n.debouncifyUpdateSpinning(),n.updateSpinning()})},beforeUnmount:function(){this.cancelExistingSpin()},methods:{debouncifyUpdateSpinning:function(n){var e=n||this.$props,i=e.delay;i&&(this.cancelExistingSpin(),this.updateSpinning=V(this.originalUpdateSpinning,i))},updateSpinning:function(){var n=this.spinning,e=this.sSpinning;e!==n&&(this.sSpinning=n)},cancelExistingSpin:function(){var n=this.updateSpinning;n&&n.cancel&&n.cancel()},renderIndicator:function(n){var e="".concat(n,"-dot"),i=B(this,"indicator");return i===null?null:(Array.isArray(i)&&(i=i.length===1?i[0]:i),y(i)?$(i,{class:e}):p&&y(p())?$(p(),{class:e}):a("span",{class:"".concat(e," ").concat(n,"-dot-spin")},[a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null)]))}},render:function(){var n,e,i,o=this.$props,f=o.size,x=o.prefixCls,h=o.tip,d=h===void 0?(n=(e=this.$slots).tip)===null||n===void 0?void 0:n.call(e):h,N=o.wrapperClassName,l=this.$attrs,v=l.class,_=l.style,C=M(l,J),S=this.configProvider,U=S.getPrefixCls,z=S.direction,s=U("spin",x),u=this.sSpinning,E=(i={},r(i,s,!0),r(i,"".concat(s,"-sm"),f==="small"),r(i,"".concat(s,"-lg"),f==="large"),r(i,"".concat(s,"-spinning"),u),r(i,"".concat(s,"-show-text"),!!d),r(i,"".concat(s,"-rtl"),z==="rtl"),r(i,v,!!v),i),m=a("div",P(P({},C),{},{style:_,class:E}),[this.renderIndicator(s),d?a("div",{class:"".concat(s,"-text")},[d]):null]),g=T(this);if(g&&g.length){var c,I=(c={},r(c,"".concat(s,"-container"),!0),r(c,"".concat(s,"-blur"),u),c);return a("div",{class:["".concat(s,"-nested-loading"),N]},[u&&a("div",{key:"loading"},[m]),a("div",{class:I,key:"container"},[g])])}return m}});export{q as S,W as s};

1
vue/dist/assets/index-404f2353.js vendored Normal file
View File

@ -0,0 +1 @@
import{d as F,u as S,G as k,an as j,h as d,c as s,aq as U,e6 as W,r as q,bh as G,Z as V,dv as Z,P as N,cb as z}from"./index-8b1d4076.js";var H=function(){return{prefixCls:String,checked:{type:Boolean,default:void 0},onChange:{type:Function},onClick:{type:Function},"onUpdate:checked":Function}},J=F({compatConfig:{MODE:3},name:"ACheckableTag",props:H(),setup:function(e,i){var l=i.slots,r=i.emit,g=S("tag",e),u=g.prefixCls,o=function(C){var v=e.checked;r("update:checked",!v),r("change",!v),r("click",C)},p=k(function(){var a;return j(u.value,(a={},d(a,"".concat(u.value,"-checkable"),!0),d(a,"".concat(u.value,"-checkable-checked"),e.checked),a))});return function(){var a;return s("span",{class:p.value,onClick:o},[(a=l.default)===null||a===void 0?void 0:a.call(l)])}}});const b=J;var K=new RegExp("^(".concat(U.join("|"),")(-inverse)?$")),L=new RegExp("^(".concat(W.join("|"),")$")),Q=function(){return{prefixCls:String,color:{type:String},closable:{type:Boolean,default:!1},closeIcon:N.any,visible:{type:Boolean,default:void 0},onClose:{type:Function},"onUpdate:visible":Function,icon:N.any}},f=F({compatConfig:{MODE:3},name:"ATag",props:Q(),slots:["closeIcon","icon"],setup:function(e,i){var l=i.slots,r=i.emit,g=i.attrs,u=S("tag",e),o=u.prefixCls,p=u.direction,a=q(!0);G(function(){e.visible!==void 0&&(a.value=e.visible)});var C=function(t){t.stopPropagation(),r("update:visible",!1),r("close",t),!t.defaultPrevented&&e.visible===void 0&&(a.value=!1)},v=k(function(){var n=e.color;return n?K.test(n)||L.test(n):!1}),E=k(function(){var n;return j(o.value,(n={},d(n,"".concat(o.value,"-").concat(e.color),v.value),d(n,"".concat(o.value,"-has-color"),e.color&&!v.value),d(n,"".concat(o.value,"-hidden"),!a.value),d(n,"".concat(o.value,"-rtl"),p.value==="rtl"),n))});return function(){var n,t,h,m=e.icon,R=m===void 0?(n=l.icon)===null||n===void 0?void 0:n.call(l):m,y=e.color,_=e.closeIcon,P=_===void 0?(t=l.closeIcon)===null||t===void 0?void 0:t.call(l):_,x=e.closable,w=x===void 0?!1:x,B=function(){return w?P?s("span",{class:"".concat(o.value,"-close-icon"),onClick:C},[P]):s(z,{class:"".concat(o.value,"-close-icon"),onClick:C},null):null},O={backgroundColor:y&&!v.value?y:void 0},I=R||null,T=(h=l.default)===null||h===void 0?void 0:h.call(l),A=I?s(V,null,[I,s("span",null,[T])]):T,D="onClick"in g,$=s("span",{class:E.value,style:O},[A,B()]);return D?s(Z,null,{default:function(){return[$]}}):$}}});f.CheckableTag=b;f.install=function(c){return c.component(f.name,f),c.component(b.name,b),c};const Y=f;export{Y as _};

View File

@ -1 +0,0 @@
import{cv as P,ay as z,d as K,j as U,di as $,w,r as b,G as S,m as A,u as D,o as E,az as G,h as d,c as s,a as C,aw as H,bf as L,g as _,dj as W,P as c,dk as x}from"./index-5ed9cd5a.js";var R=z("small","default"),q=function(){return{id:String,prefixCls:String,size:c.oneOf(R),disabled:{type:Boolean,default:void 0},checkedChildren:c.any,unCheckedChildren:c.any,tabindex:c.oneOfType([c.string,c.number]),autofocus:{type:Boolean,default:void 0},loading:{type:Boolean,default:void 0},checked:c.oneOfType([c.string,c.number,c.looseBool]),checkedValue:c.oneOfType([c.string,c.number,c.looseBool]).def(!0),unCheckedValue:c.oneOfType([c.string,c.number,c.looseBool]).def(!1),onChange:{type:Function},onClick:{type:Function},onKeydown:{type:Function},onMouseup:{type:Function},"onUpdate:checked":{type:Function},onBlur:Function,onFocus:Function}},J=K({compatConfig:{MODE:3},name:"ASwitch",__ANT_SWITCH:!0,inheritAttrs:!1,props:q(),slots:["checkedChildren","unCheckedChildren"],setup:function(n,r){var o=r.attrs,y=r.slots,B=r.expose,l=r.emit,m=U();$(function(){w(!("defaultChecked"in o),"Switch","'defaultChecked' is deprecated, please use 'v-model:checked'"),w(!("value"in o),"Switch","`value` is not validate prop, do you mean `checked`?")});var h=b(n.checked!==void 0?n.checked:o.defaultChecked),f=S(function(){return h.value===n.checkedValue});A(function(){return n.checked},function(){h.value=n.checked});var v=D("switch",n),u=v.prefixCls,F=v.direction,T=v.size,i=b(),g=function(){var e;(e=i.value)===null||e===void 0||e.focus()},V=function(){var e;(e=i.value)===null||e===void 0||e.blur()};B({focus:g,blur:V}),E(function(){G(function(){n.autofocus&&!n.disabled&&i.value.focus()})});var k=function(e,t){n.disabled||(l("update:checked",e),l("change",e,t),m.onFieldChange())},I=function(e){l("blur",e)},N=function(e){g();var t=f.value?n.unCheckedValue:n.checkedValue;k(t,e),l("click",t,e)},M=function(e){e.keyCode===x.LEFT?k(n.unCheckedValue,e):e.keyCode===x.RIGHT&&k(n.checkedValue,e),l("keydown",e)},O=function(e){var t;(t=i.value)===null||t===void 0||t.blur(),l("mouseup",e)},j=S(function(){var a;return a={},d(a,"".concat(u.value,"-small"),T.value==="small"),d(a,"".concat(u.value,"-loading"),n.loading),d(a,"".concat(u.value,"-checked"),f.value),d(a,"".concat(u.value,"-disabled"),n.disabled),d(a,u.value,!0),d(a,"".concat(u.value,"-rtl"),F.value==="rtl"),a});return function(){var a;return s(W,{insertExtraNode:!0},{default:function(){return[s("button",C(C(C({},H(n,["prefixCls","checkedChildren","unCheckedChildren","checked","autofocus","checkedValue","unCheckedValue","id","onChange","onUpdate:checked"])),o),{},{id:(a=n.id)!==null&&a!==void 0?a:m.id.value,onKeydown:M,onClick:N,onBlur:I,onMouseup:O,type:"button",role:"switch","aria-checked":h.value,disabled:n.disabled||n.loading,class:[o.class,j.value],ref:i}),[s("div",{class:"".concat(u.value,"-handle")},[n.loading?s(L,{class:"".concat(u.value,"-loading-icon")},null):null]),s("span",{class:"".concat(u.value,"-inner")},[f.value?_(y,n,"checkedChildren"):_(y,n,"unCheckedChildren")])])]}})}}});const X=P(J);export{X as _};

View File

@ -1 +1 @@
import{r as F,o as P,cw as K,av as L,G as l,ax as T,ay as $,d as I,u as B,cx as _,b as y,bk as V,cy as A,an as E,h as c,c as M,a as G}from"./index-5ed9cd5a.js";const W=function(){var o=F(!1);return P(function(){o.value=K()}),o};var D=Symbol("rowContextKey"),k=function(r){T(D,r)},U=function(){return L(D,{gutter:l(function(){}),wrap:l(function(){}),supportFlexGap:l(function(){})})};$("top","middle","bottom","stretch");$("start","end","center","space-around","space-between");var q=function(){return{align:String,justify:String,prefixCls:String,gutter:{type:[Number,Array,Object],default:0},wrap:{type:Boolean,default:void 0}}},H=I({compatConfig:{MODE:3},name:"ARow",props:q(),setup:function(r,N){var m=N.slots,v=B("row",r),d=v.prefixCls,h=v.direction,j,x=F({xs:!0,sm:!0,md:!0,lg:!0,xl:!0,xxl:!0,xxxl:!0}),w=W();P(function(){j=_.subscribe(function(e){var t=r.gutter||0;(!Array.isArray(t)&&y(t)==="object"||Array.isArray(t)&&(y(t[0])==="object"||y(t[1])==="object"))&&(x.value=e)})}),V(function(){_.unsubscribe(j)});var S=l(function(){var e=[0,0],t=r.gutter,n=t===void 0?0:t,s=Array.isArray(n)?n:[n,0];return s.forEach(function(i,b){if(y(i)==="object")for(var a=0;a<A.length;a++){var p=A[a];if(x.value[p]&&i[p]!==void 0){e[b]=i[p];break}}else e[b]=i||0}),e});k({gutter:S,supportFlexGap:w,wrap:l(function(){return r.wrap})});var R=l(function(){var e;return E(d.value,(e={},c(e,"".concat(d.value,"-no-wrap"),r.wrap===!1),c(e,"".concat(d.value,"-").concat(r.justify),r.justify),c(e,"".concat(d.value,"-").concat(r.align),r.align),c(e,"".concat(d.value,"-rtl"),h.value==="rtl"),e))}),O=l(function(){var e=S.value,t={},n=e[0]>0?"".concat(e[0]/-2,"px"):void 0,s=e[1]>0?"".concat(e[1]/-2,"px"):void 0;return n&&(t.marginLeft=n,t.marginRight=n),w.value?t.rowGap="".concat(e[1],"px"):s&&(t.marginTop=s,t.marginBottom=s),t});return function(){var e;return M("div",{class:R.value,style:O.value},[(e=m.default)===null||e===void 0?void 0:e.call(m)])}}});const Y=H;function J(o){return typeof o=="number"?"".concat(o," ").concat(o," auto"):/^\d+(\.\d+)?(px|em|rem|%)$/.test(o)?"0 0 ".concat(o):o}var Q=function(){return{span:[String,Number],order:[String,Number],offset:[String,Number],push:[String,Number],pull:[String,Number],xs:{type:[String,Number,Object],default:void 0},sm:{type:[String,Number,Object],default:void 0},md:{type:[String,Number,Object],default:void 0},lg:{type:[String,Number,Object],default:void 0},xl:{type:[String,Number,Object],default:void 0},xxl:{type:[String,Number,Object],default:void 0},xxxl:{type:[String,Number,Object],default:void 0},prefixCls:String,flex:[String,Number]}};const Z=I({compatConfig:{MODE:3},name:"ACol",props:Q(),setup:function(r,N){var m=N.slots,v=U(),d=v.gutter,h=v.supportFlexGap,j=v.wrap,x=B("col",r),w=x.prefixCls,S=x.direction,R=l(function(){var e,t=r.span,n=r.order,s=r.offset,i=r.push,b=r.pull,a=w.value,p={};return["xs","sm","md","lg","xl","xxl","xxxl"].forEach(function(g){var f,u={},C=r[g];typeof C=="number"?u.span=C:y(C)==="object"&&(u=C||{}),p=G(G({},p),{},(f={},c(f,"".concat(a,"-").concat(g,"-").concat(u.span),u.span!==void 0),c(f,"".concat(a,"-").concat(g,"-order-").concat(u.order),u.order||u.order===0),c(f,"".concat(a,"-").concat(g,"-offset-").concat(u.offset),u.offset||u.offset===0),c(f,"".concat(a,"-").concat(g,"-push-").concat(u.push),u.push||u.push===0),c(f,"".concat(a,"-").concat(g,"-pull-").concat(u.pull),u.pull||u.pull===0),c(f,"".concat(a,"-rtl"),S.value==="rtl"),f))}),E(a,(e={},c(e,"".concat(a,"-").concat(t),t!==void 0),c(e,"".concat(a,"-order-").concat(n),n),c(e,"".concat(a,"-offset-").concat(s),s),c(e,"".concat(a,"-push-").concat(i),i),c(e,"".concat(a,"-pull-").concat(b),b),e),p)}),O=l(function(){var e=r.flex,t=d.value,n={};if(t&&t[0]>0){var s="".concat(t[0]/2,"px");n.paddingLeft=s,n.paddingRight=s}if(t&&t[1]>0&&!h.value){var i="".concat(t[1]/2,"px");n.paddingTop=i,n.paddingBottom=i}return e&&(n.flex=J(e),j.value===!1&&!n.minWidth&&(n.minWidth=0)),n});return function(){var e;return M("div",{class:R.value,style:O.value},[(e=m.default)===null||e===void 0?void 0:e.call(m)])}}});export{Z as C,Y as R};
import{r as F,o as P,cx as K,av as L,G as i,ax as T,ay as I,d as $,u as B,cy as _,b as y,bk as V,cz as A,an as E,h as c,c as M,a as G}from"./index-8b1d4076.js";const W=function(){var o=F(!1);return P(function(){o.value=K()}),o};var D=Symbol("rowContextKey"),k=function(r){T(D,r)},U=function(){return L(D,{gutter:i(function(){}),wrap:i(function(){}),supportFlexGap:i(function(){})})};I("top","middle","bottom","stretch");I("start","end","center","space-around","space-between");var q=function(){return{align:String,justify:String,prefixCls:String,gutter:{type:[Number,Array,Object],default:0},wrap:{type:Boolean,default:void 0}}},z=$({compatConfig:{MODE:3},name:"ARow",props:q(),setup:function(r,N){var m=N.slots,v=B("row",r),d=v.prefixCls,h=v.direction,j,x=F({xs:!0,sm:!0,md:!0,lg:!0,xl:!0,xxl:!0,xxxl:!0}),S=W();P(function(){j=_.subscribe(function(e){var t=r.gutter||0;(!Array.isArray(t)&&y(t)==="object"||Array.isArray(t)&&(y(t[0])==="object"||y(t[1])==="object"))&&(x.value=e)})}),V(function(){_.unsubscribe(j)});var w=i(function(){var e=[0,0],t=r.gutter,n=t===void 0?0:t,s=Array.isArray(n)?n:[n,0];return s.forEach(function(l,b){if(y(l)==="object")for(var a=0;a<A.length;a++){var p=A[a];if(x.value[p]&&l[p]!==void 0){e[b]=l[p];break}}else e[b]=l||0}),e});k({gutter:w,supportFlexGap:S,wrap:i(function(){return r.wrap})});var R=i(function(){var e;return E(d.value,(e={},c(e,"".concat(d.value,"-no-wrap"),r.wrap===!1),c(e,"".concat(d.value,"-").concat(r.justify),r.justify),c(e,"".concat(d.value,"-").concat(r.align),r.align),c(e,"".concat(d.value,"-rtl"),h.value==="rtl"),e))}),O=i(function(){var e=w.value,t={},n=e[0]>0?"".concat(e[0]/-2,"px"):void 0,s=e[1]>0?"".concat(e[1]/-2,"px"):void 0;return n&&(t.marginLeft=n,t.marginRight=n),S.value?t.rowGap="".concat(e[1],"px"):s&&(t.marginTop=s,t.marginBottom=s),t});return function(){var e;return M("div",{class:R.value,style:O.value},[(e=m.default)===null||e===void 0?void 0:e.call(m)])}}});const X=z;function H(o){return typeof o=="number"?"".concat(o," ").concat(o," auto"):/^\d+(\.\d+)?(px|em|rem|%)$/.test(o)?"0 0 ".concat(o):o}var J=function(){return{span:[String,Number],order:[String,Number],offset:[String,Number],push:[String,Number],pull:[String,Number],xs:{type:[String,Number,Object],default:void 0},sm:{type:[String,Number,Object],default:void 0},md:{type:[String,Number,Object],default:void 0},lg:{type:[String,Number,Object],default:void 0},xl:{type:[String,Number,Object],default:void 0},xxl:{type:[String,Number,Object],default:void 0},xxxl:{type:[String,Number,Object],default:void 0},prefixCls:String,flex:[String,Number]}};const Y=$({compatConfig:{MODE:3},name:"ACol",props:J(),setup:function(r,N){var m=N.slots,v=U(),d=v.gutter,h=v.supportFlexGap,j=v.wrap,x=B("col",r),S=x.prefixCls,w=x.direction,R=i(function(){var e,t=r.span,n=r.order,s=r.offset,l=r.push,b=r.pull,a=S.value,p={};return["xs","sm","md","lg","xl","xxl","xxxl"].forEach(function(g){var f,u={},C=r[g];typeof C=="number"?u.span=C:y(C)==="object"&&(u=C||{}),p=G(G({},p),{},(f={},c(f,"".concat(a,"-").concat(g,"-").concat(u.span),u.span!==void 0),c(f,"".concat(a,"-").concat(g,"-order-").concat(u.order),u.order||u.order===0),c(f,"".concat(a,"-").concat(g,"-offset-").concat(u.offset),u.offset||u.offset===0),c(f,"".concat(a,"-").concat(g,"-push-").concat(u.push),u.push||u.push===0),c(f,"".concat(a,"-").concat(g,"-pull-").concat(u.pull),u.pull||u.pull===0),c(f,"".concat(a,"-rtl"),w.value==="rtl"),f))}),E(a,(e={},c(e,"".concat(a,"-").concat(t),t!==void 0),c(e,"".concat(a,"-order-").concat(n),n),c(e,"".concat(a,"-offset-").concat(s),s),c(e,"".concat(a,"-push-").concat(l),l),c(e,"".concat(a,"-pull-").concat(b),b),e),p)}),O=i(function(){var e=r.flex,t=d.value,n={};if(t&&t[0]>0){var s="".concat(t[0]/2,"px");n.paddingLeft=s,n.paddingRight=s}if(t&&t[1]>0&&!h.value){var l="".concat(t[1]/2,"px");n.paddingTop=l,n.paddingBottom=l}return e&&(n.flex=H(e),j.value===!1&&!n.minWidth&&(n.minWidth=0)),n});return function(){var e;return M("div",{class:R.value,style:O.value},[(e=m.default)===null||e===void 0?void 0:e.call(m)])}}});export{Y as C,X as R};

1
vue/dist/assets/index-63826c0f.css vendored Normal file
View File

@ -0,0 +1 @@
.ant-tag{box-sizing:border-box;margin:0 8px 0 0;color:#000000d9;font-size:14px;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";display:inline-block;height:auto;padding:0 7px;font-size:12px;line-height:20px;white-space:nowrap;background:#fafafa;border:1px solid #d9d9d9;border-radius:2px;opacity:1;transition:all .3s}.ant-tag,.ant-tag a,.ant-tag a:hover{color:#000000d9}.ant-tag>a:first-child:last-child{display:inline-block;margin:0 -8px;padding:0 8px}.ant-tag-close-icon{margin-left:3px;color:#00000073;font-size:10px;cursor:pointer;transition:all .3s}.ant-tag-close-icon:hover{color:#000000d9}.ant-tag-has-color{border-color:transparent}.ant-tag-has-color,.ant-tag-has-color a,.ant-tag-has-color a:hover,.ant-tag-has-color .anticon-close,.ant-tag-has-color .anticon-close:hover{color:#fff}.ant-tag-checkable{background-color:transparent;border-color:transparent;cursor:pointer}.ant-tag-checkable:not(.ant-tag-checkable-checked):hover{color:#d03f0a}.ant-tag-checkable:active,.ant-tag-checkable-checked{color:#fff}.ant-tag-checkable-checked{background-color:#d03f0a}.ant-tag-checkable:active{background-color:#ab2800}.ant-tag-hidden{display:none}.ant-tag-pink{color:#c41d7f;background:#fff0f6;border-color:#ffadd2}.ant-tag-pink-inverse{color:#fff;background:#eb2f96;border-color:#eb2f96}.ant-tag-magenta{color:#c41d7f;background:#fff0f6;border-color:#ffadd2}.ant-tag-magenta-inverse{color:#fff;background:#eb2f96;border-color:#eb2f96}.ant-tag-red{color:#cf1322;background:#fff1f0;border-color:#ffa39e}.ant-tag-red-inverse{color:#fff;background:#f5222d;border-color:#f5222d}.ant-tag-volcano{color:#d4380d;background:#fff2e8;border-color:#ffbb96}.ant-tag-volcano-inverse{color:#fff;background:#fa541c;border-color:#fa541c}.ant-tag-orange{color:#d46b08;background:#fff7e6;border-color:#ffd591}.ant-tag-orange-inverse{color:#fff;background:#fa8c16;border-color:#fa8c16}.ant-tag-yellow{color:#d4b106;background:#feffe6;border-color:#fffb8f}.ant-tag-yellow-inverse{color:#fff;background:#fadb14;border-color:#fadb14}.ant-tag-gold{color:#d48806;background:#fffbe6;border-color:#ffe58f}.ant-tag-gold-inverse{color:#fff;background:#faad14;border-color:#faad14}.ant-tag-cyan{color:#08979c;background:#e6fffb;border-color:#87e8de}.ant-tag-cyan-inverse{color:#fff;background:#13c2c2;border-color:#13c2c2}.ant-tag-lime{color:#7cb305;background:#fcffe6;border-color:#eaff8f}.ant-tag-lime-inverse{color:#fff;background:#a0d911;border-color:#a0d911}.ant-tag-green{color:#389e0d;background:#f6ffed;border-color:#b7eb8f}.ant-tag-green-inverse{color:#fff;background:#52c41a;border-color:#52c41a}.ant-tag-blue{color:#096dd9;background:#e6f7ff;border-color:#91d5ff}.ant-tag-blue-inverse{color:#fff;background:#1890ff;border-color:#1890ff}.ant-tag-geekblue{color:#1d39c4;background:#f0f5ff;border-color:#adc6ff}.ant-tag-geekblue-inverse{color:#fff;background:#2f54eb;border-color:#2f54eb}.ant-tag-purple{color:#531dab;background:#f9f0ff;border-color:#d3adf7}.ant-tag-purple-inverse{color:#fff;background:#722ed1;border-color:#722ed1}.ant-tag-success{color:#52c41a;background:#f6ffed;border-color:#b7eb8f}.ant-tag-processing{color:#d03f0a;background:#fff1e6;border-color:#f7ae83}.ant-tag-error{color:#ff4d4f;background:#fff2f0;border-color:#ffccc7}.ant-tag-warning{color:#faad14;background:#fffbe6;border-color:#ffe58f}.ant-tag>.anticon+span,.ant-tag>span+.anticon{margin-left:7px}.ant-tag.ant-tag-rtl{margin-right:0;margin-left:8px;direction:rtl;text-align:right}.ant-tag-rtl .ant-tag-close-icon{margin-right:3px;margin-left:0}.ant-tag-rtl.ant-tag>.anticon+span,.ant-tag-rtl.ant-tag>span+.anticon{margin-right:7px;margin-left:0}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
vue/dist/assets/index-d55a76b1.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{d as x,a1 as $,aK as g,cI as b,r as w,U as p,V as i,W as a,c as r,a3 as d,X as u,Y as n,Z as B,a8 as I,a4 as m,y as V,z as _,B as v,aj as W,ak as D,cJ as N,a0 as R}from"./index-5ed9cd5a.js";/* empty css */const F={class:"container"},K={class:"actions"},L={class:"uni-desc"},U={class:"snapshot"},j=x({__name:"index",props:{tabIdx:{},paneIdx:{},id:{},paneKey:{}},setup(q){const h=$(),t=g(),f=e=>{h.tabList=V(e.tabs)},k=b(async e=>{await N(`workspace_snapshot_${e.id}`),t.snapshots=t.snapshots.filter(c=>c.id!==e.id),_.success(v("deleteSuccess"))}),o=w(""),y=async()=>{if(!o.value){_.error(v("nameRequired"));return}const e=t.createSnapshot(o.value);await t.addSnapshot(e),_.success(v("saveCompleted"))};return(e,c)=>{const C=W,l=D;return p(),i("div",F,[a("div",K,[r(C,{value:o.value,"onUpdate:value":c[0]||(c[0]=s=>o.value=s),placeholder:e.$t("name"),style:{"max-width":"300px"}},null,8,["value","placeholder"]),r(l,{type:"primary",onClick:y},{default:d(()=>[u(n(e.$t("saveWorkspaceSnapshot")),1)]),_:1})]),a("p",L,n(e.$t("WorkspaceSnapshotDesc")),1),a("ul",U,[(p(!0),i(B,null,I(m(t).snapshots,s=>(p(),i("li",{key:s.id},[a("div",null,[a("span",null,n(s.name),1)]),a("div",null,[r(l,{onClick:S=>f(s)},{default:d(()=>[u(n(e.$t("restore")),1)]),_:2},1032,["onClick"]),r(l,{onClick:S=>m(k)(s)},{default:d(()=>[u(n(e.$t("remove")),1)]),_:2},1032,["onClick"])])]))),128))])])}}});const E=R(j,[["__scopeId","data-v-2c44013c"]]);export{E as default};
import{d as x,a1 as $,aK as g,cS as b,r as w,U as p,V as i,W as a,c as r,a3 as d,X as u,Y as n,Z as B,a8 as I,a4 as m,y as V,z as _,B as v,aj as W,ak as D,cT as N,a0 as R}from"./index-8b1d4076.js";/* empty css */const F={class:"container"},K={class:"actions"},L={class:"uni-desc"},T={class:"snapshot"},U=x({__name:"index",props:{tabIdx:{},paneIdx:{},id:{},paneKey:{}},setup(j){const h=$(),t=g(),f=e=>{h.tabList=V(e.tabs)},k=b(async e=>{await N(`workspace_snapshot_${e.id}`),t.snapshots=t.snapshots.filter(c=>c.id!==e.id),_.success(v("deleteSuccess"))}),o=w(""),y=async()=>{if(!o.value){_.error(v("nameRequired"));return}const e=t.createSnapshot(o.value);await t.addSnapshot(e),_.success(v("saveCompleted"))};return(e,c)=>{const C=W,l=D;return p(),i("div",F,[a("div",K,[r(C,{value:o.value,"onUpdate:value":c[0]||(c[0]=s=>o.value=s),placeholder:e.$t("name"),style:{"max-width":"300px"}},null,8,["value","placeholder"]),r(l,{type:"primary",onClick:y},{default:d(()=>[u(n(e.$t("saveWorkspaceSnapshot")),1)]),_:1})]),a("p",L,n(e.$t("WorkspaceSnapshotDesc")),1),a("ul",T,[(p(!0),i(B,null,I(m(t).snapshots,s=>(p(),i("li",{key:s.id},[a("div",null,[a("span",null,n(s.name),1)]),a("div",null,[r(l,{onClick:S=>f(s)},{default:d(()=>[u(n(e.$t("restore")),1)]),_:2},1032,["onClick"]),r(l,{onClick:S=>m(k)(s)},{default:d(()=>[u(n(e.$t("remove")),1)]),_:2},1032,["onClick"])])]))),128))])])}}});const A=R(U,[["__scopeId","data-v-2c44013c"]]);export{A as default};

1
vue/dist/assets/index-fd0b9b75.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{ck as e,cl as i,cm as r,cn as a,b1 as n}from"./index-5ed9cd5a.js";function o(s,t){return e(i(s,t,r),s+"")}function b(s){return a(s)&&n(s)}export{o as b,b as i};

View File

@ -0,0 +1 @@
import{cl as e,cm as i,cn as r,co as a,b1 as n}from"./index-8b1d4076.js";function c(s,t){return e(i(s,t,r),s+"")}function b(s){return a(s)&&n(s)}export{c as b,b as i};

1
vue/dist/assets/numInput-2acaf603.css vendored Normal file
View File

@ -0,0 +1 @@
.ant-slider{box-sizing:border-box;color:#000000d9;font-size:14px;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";position:relative;height:12px;margin:10px 6px;padding:4px 0;cursor:pointer;touch-action:none}.ant-slider-vertical{width:12px;height:100%;margin:6px 10px;padding:0 4px}.ant-slider-vertical .ant-slider-rail{width:4px;height:100%}.ant-slider-vertical .ant-slider-track{width:4px}.ant-slider-vertical .ant-slider-handle{margin-top:-6px;margin-left:-5px}.ant-slider-vertical .ant-slider-mark{top:0;left:12px;width:18px;height:100%}.ant-slider-vertical .ant-slider-mark-text{left:4px;white-space:nowrap}.ant-slider-vertical .ant-slider-step{width:4px;height:100%}.ant-slider-vertical .ant-slider-dot{top:auto;left:2px;margin-bottom:-4px}.ant-slider-tooltip .ant-tooltip-inner{min-width:unset}.ant-slider-rtl.ant-slider-vertical .ant-slider-handle{margin-right:-5px;margin-left:0}.ant-slider-rtl.ant-slider-vertical .ant-slider-mark{right:12px;left:auto}.ant-slider-rtl.ant-slider-vertical .ant-slider-mark-text{right:4px;left:auto}.ant-slider-rtl.ant-slider-vertical .ant-slider-dot{right:2px;left:auto}.ant-slider-with-marks{margin-bottom:28px}.ant-slider-rail{position:absolute;width:100%;height:4px;background-color:#f5f5f5;border-radius:2px;transition:background-color .3s}.ant-slider-track{position:absolute;height:4px;background-color:#f7ae83;border-radius:2px;transition:background-color .3s}.ant-slider-handle{position:absolute;width:14px;height:14px;margin-top:-5px;background-color:#fff;border:solid 2px #f7ae83;border-radius:50%;box-shadow:0;cursor:pointer;transition:border-color .3s,box-shadow .6s,transform .3s cubic-bezier(.18,.89,.32,1.28)}.ant-slider-handle-dragging.ant-slider-handle-dragging.ant-slider-handle-dragging{border-color:#d9653b;box-shadow:0 0 0 5px #d03f0a1f}.ant-slider-handle:focus{border-color:#d9653b;outline:none;box-shadow:0 0 0 5px #d03f0a1f}.ant-slider-handle.ant-tooltip-open{border-color:#d03f0a}.ant-slider:hover .ant-slider-rail{background-color:#e1e1e1}.ant-slider:hover .ant-slider-track{background-color:#eb8857}.ant-slider:hover .ant-slider-handle:not(.ant-tooltip-open){border-color:#eb8857}.ant-slider-mark{position:absolute;top:14px;left:0;width:100%;font-size:14px}.ant-slider-mark-text{position:absolute;display:inline-block;color:#00000073;text-align:center;word-break:keep-all;cursor:pointer;user-select:none}.ant-slider-mark-text-active{color:#000000d9}.ant-slider-step{position:absolute;width:100%;height:4px;background:transparent}.ant-slider-dot{position:absolute;top:-2px;width:8px;height:8px;margin-left:-4px;background-color:#fff;border:2px solid #f0f0f0;border-radius:50%;cursor:pointer}.ant-slider-dot:first-child{margin-left:-4px}.ant-slider-dot:last-child{margin-left:-4px}.ant-slider-dot-active{border-color:#e89f85}.ant-slider-disabled{cursor:not-allowed}.ant-slider-disabled .ant-slider-rail{background-color:#f5f5f5!important}.ant-slider-disabled .ant-slider-track{background-color:#00000040!important}.ant-slider-disabled .ant-slider-handle,.ant-slider-disabled .ant-slider-dot{background-color:#fff;border-color:#00000040!important;box-shadow:none;cursor:not-allowed}.ant-slider-disabled .ant-slider-mark-text,.ant-slider-disabled .ant-slider-dot{cursor:not-allowed!important}.ant-slider-rtl{direction:rtl}.ant-slider-rtl .ant-slider-mark{right:0;left:auto}.ant-slider-rtl .ant-slider-dot,.ant-slider-rtl .ant-slider-dot:first-child{margin-right:-4px;margin-left:0}.ant-slider-rtl .ant-slider-dot:last-child{margin-right:-4px;margin-left:0}.num-input[data-v-55978858]{display:flex}.num-input .slide[data-v-55978858]{flex:1;min-width:128px;max-width:256px;margin-left:8px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{d as Z,a1 as ee,r as F,J as te,K as le,o as ie,U as v,V as N,c as i,a4 as e,W as g,a3 as n,X as k,Y as u,a5 as R,L as se,a6 as ae,af as oe,ag as $,$ as A,a2 as ne,z as w,B as re,cU as ce,cV as de,ak as ue,ai as me,T as fe,a0 as pe}from"./index-8b1d4076.js";import{u as ve,c as ge,a as ke,F as we,d as he}from"./FileItem-5c27aa5d.js";import{a as Ce,b as Se,c as _e,M as Ie,o as z,L as ye,R as xe,f as be}from"./MultiSelectKeep-4ce030ff.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-26786c56.js";/* empty css */import"./index-fd0b9b75.js";import"./_isIterateeCall-4f946453.js";import"./index-404f2353.js";import"./index-133a27d3.js";import"./shortcut-d7b854eb.js";import"./Checkbox-8b8e8d31.js";import"./index-19cfb514.js";const Ve={class:"refresh-button"},Me={class:"hint"},Te={key:0,class:"preview-switch"},Fe=Z({__name:"randomImage",props:{tabIdx:{},paneIdx:{},id:{},paneKey:{}},setup(Ne){const B=ee(),m=F(!1),l=F([]),r=l,h=te(`${le}randomImageSettingNotificationShown`,!1),P=()=>{h.value||(w.info({content:re("randomImageSettingNotification"),duration:6,key:"randomImageSetting"}),h.value=!0)},f=async()=>{try{m.value=!0;const s=await ce();s.length===0&&w.warn("No data, please generate index in image search page first"),l.value=s}finally{m.value=!1,_()}},C=()=>{if(l.value.length===0){w.warn("没有图片可以浏览");return}z(l.value,o.value||0)};ie(()=>{f(),setTimeout(()=>{P()},2e3)});const{stackViewEl:U,multiSelectedIdxs:p,stack:K,scroller:L}=ve({images:l}).toRefs(),{onClearAllSelected:D,onSelectAll:E,onReverseSelect:G}=ge();Ce();const{itemSize:S,gridItems:O,cellWidth:W,onScroll:_}=ke(),{showGenInfo:c,imageGenInfo:I,q,onContextMenuClick:H,onFileItemClick:J}=Se({openNext:de}),{previewIdx:o,previewing:y,onPreviewVisibleChange:Q,previewImgMove:x,canPreview:b}=_e(),V=async(s,t,d)=>{K.value=[{curr:"",files:l.value}],await H(s,t,d)};return(s,t)=>{var M;const d=ue,X=me,Y=fe;return v(),N("div",{class:"container",ref_key:"stackViewEl",ref:U},[i(Ie,{show:!!e(p).length||e(B).keepMultiSelect,onClearAllSelected:e(D),onSelectAll:e(E),onReverseSelect:e(G)},null,8,["show","onClearAllSelected","onSelectAll","onReverseSelect"]),g("div",Ve,[i(d,{onClick:f,onTouchstart:R(f,["prevent"]),type:"primary",loading:m.value,shape:"round"},{default:n(()=>[k(u(s.$t("shuffle")),1)]),_:1},8,["onTouchstart","loading"]),i(d,{onClick:C,onTouchstart:R(C,["prevent"]),type:"default",disabled:!((M=l.value)!=null&&M.length),shape:"round"},{default:n(()=>[k(u(s.$t("tiktokView")),1)]),_:1},8,["onTouchstart","disabled"])]),i(Y,{visible:e(c),"onUpdate:visible":t[1]||(t[1]=a=>ae(c)?c.value=a:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=a=>c.value=!1)},{cancelText:n(()=>[]),default:n(()=>[i(X,{active:"",loading:!e(q).isIdle},{default:n(()=>[g("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=a=>e(se)(e(I)))},[g("div",Me,u(s.$t("doubleClickToCopy")),1),k(" "+u(e(I)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),i(e(he),{ref_key:"scroller",ref:L,class:"file-list",items:l.value.slice(),"item-size":e(S).first,"key-field":"fullpath","item-secondary-size":e(S).second,gridItems:e(O),onScroll:e(_)},{default:n(({item:a,index:T})=>[i(we,{idx:T,file:a,"cell-width":e(W),"full-screen-preview-image-url":e(r)[e(o)]?e(oe)(e(r)[e(o)]):"",onContextMenuClick:V,onPreviewVisibleChange:e(Q),"is-selected-mutil-files":e(p).length>1,selected:e(p).includes(T),onFileItemClick:e(J),onTiktokView:(Re,j)=>e(z)(l.value,j)},null,8,["idx","file","cell-width","full-screen-preview-image-url","onPreviewVisibleChange","is-selected-mutil-files","selected","onFileItemClick","onTiktokView"])]),_:1},8,["items","item-size","item-secondary-size","gridItems","onScroll"]),e(y)?(v(),N("div",Te,[i(e(ye),{onClick:t[3]||(t[3]=a=>e(x)("prev")),class:$({disable:!e(b)("prev")})},null,8,["class"]),i(e(xe),{onClick:t[4]||(t[4]=a=>e(x)("next")),class:$({disable:!e(b)("next")})},null,8,["class"])])):A("",!0),e(y)&&e(r)&&e(r)[e(o)]?(v(),ne(be,{key:1,file:e(r)[e(o)],idx:e(o),onContextMenuClick:V},null,8,["file","idx"])):A("",!0)],512)}}});const We=pe(Fe,[["__scopeId","data-v-49082269"]]);export{We as default};

View File

@ -1 +0,0 @@
import{d as Z,a1 as ee,r as F,J as te,K as le,o as ie,U as v,V as N,c as i,a4 as e,W as g,a3 as n,X as k,Y as u,a5 as R,L as se,a6 as ae,af as oe,ag as $,$ as A,a2 as ne,z as w,B as re,cK as ce,cL as de,ak as ue,ai as me,T as fe,a0 as pe}from"./index-5ed9cd5a.js";import{u as ve,c as ge,a as ke,F as we,d as he}from"./FileItem-2ecfe4d5.js";import{a as Ce,b as Se,c as _e,M as Ie,o as z,L as ye,R as xe,f as be}from"./MultiSelectKeep-68ce9bb5.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-47577760.js";/* empty css */import"./_isIterateeCall-cd370691.js";import"./index-7cbf21fe.js";import"./shortcut-bf073698.js";import"./Checkbox-bbe5a1a5.js";import"./index-4e015155.js";const Ve={class:"refresh-button"},Me={class:"hint"},Te={key:0,class:"preview-switch"},Fe=Z({__name:"randomImage",props:{tabIdx:{},paneIdx:{},id:{},paneKey:{}},setup(Ne){const B=ee(),m=F(!1),l=F([]),r=l,h=te(`${le}randomImageSettingNotificationShown`,!1),K=()=>{h.value||(w.info({content:re("randomImageSettingNotification"),duration:6,key:"randomImageSetting"}),h.value=!0)},f=async()=>{try{m.value=!0;const s=await ce();s.length===0&&w.warn("No data, please generate index in image search page first"),l.value=s}finally{m.value=!1,_()}},C=()=>{if(l.value.length===0){w.warn("没有图片可以浏览");return}z(l.value,o.value||0)};ie(()=>{f(),setTimeout(()=>{K()},2e3)});const{stackViewEl:L,multiSelectedIdxs:p,stack:P,scroller:U}=ve({images:l}).toRefs(),{onClearAllSelected:D,onSelectAll:E,onReverseSelect:G}=ge();Ce();const{itemSize:S,gridItems:O,cellWidth:W,onScroll:_}=ke(),{showGenInfo:c,imageGenInfo:I,q,onContextMenuClick:H,onFileItemClick:J}=Se({openNext:de}),{previewIdx:o,previewing:y,onPreviewVisibleChange:Q,previewImgMove:x,canPreview:b}=_e(),V=async(s,t,d)=>{P.value=[{curr:"",files:l.value}],await H(s,t,d)};return(s,t)=>{var M;const d=ue,X=me,Y=fe;return v(),N("div",{class:"container",ref_key:"stackViewEl",ref:L},[i(Ie,{show:!!e(p).length||e(B).keepMultiSelect,onClearAllSelected:e(D),onSelectAll:e(E),onReverseSelect:e(G)},null,8,["show","onClearAllSelected","onSelectAll","onReverseSelect"]),g("div",Ve,[i(d,{onClick:f,onTouchstart:R(f,["prevent"]),type:"primary",loading:m.value,shape:"round"},{default:n(()=>[k(u(s.$t("shuffle")),1)]),_:1},8,["onTouchstart","loading"]),i(d,{onClick:C,onTouchstart:R(C,["prevent"]),type:"default",disabled:!((M=l.value)!=null&&M.length),shape:"round"},{default:n(()=>[k(u(s.$t("tiktokView")),1)]),_:1},8,["onTouchstart","disabled"])]),i(Y,{visible:e(c),"onUpdate:visible":t[1]||(t[1]=a=>ae(c)?c.value=a:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=a=>c.value=!1)},{cancelText:n(()=>[]),default:n(()=>[i(X,{active:"",loading:!e(q).isIdle},{default:n(()=>[g("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=a=>e(se)(e(I)))},[g("div",Me,u(s.$t("doubleClickToCopy")),1),k(" "+u(e(I)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),i(e(he),{ref_key:"scroller",ref:U,class:"file-list",items:l.value.slice(),"item-size":e(S).first,"key-field":"fullpath","item-secondary-size":e(S).second,gridItems:e(O),onScroll:e(_)},{default:n(({item:a,index:T})=>[i(we,{idx:T,file:a,"cell-width":e(W),"full-screen-preview-image-url":e(r)[e(o)]?e(oe)(e(r)[e(o)]):"",onContextMenuClick:V,onPreviewVisibleChange:e(Q),"is-selected-mutil-files":e(p).length>1,selected:e(p).includes(T),onFileItemClick:e(J),onTiktokView:(Re,j)=>e(z)(l.value,j)},null,8,["idx","file","cell-width","full-screen-preview-image-url","onPreviewVisibleChange","is-selected-mutil-files","selected","onFileItemClick","onTiktokView"])]),_:1},8,["items","item-size","item-secondary-size","gridItems","onScroll"]),e(y)?(v(),N("div",Te,[i(e(ye),{onClick:t[3]||(t[3]=a=>e(x)("prev")),class:$({disable:!e(b)("prev")})},null,8,["class"]),i(e(xe),{onClick:t[4]||(t[4]=a=>e(x)("next")),class:$({disable:!e(b)("next")})},null,8,["class"])])):A("",!0),e(y)&&e(r)&&e(r)[e(o)]?(v(),ne(be,{key:1,file:e(r)[e(o)],idx:e(o),onContextMenuClick:V},null,8,["file","idx"])):A("",!0)],512)}}});const Ge=pe(Fe,[["__scopeId","data-v-49082269"]]);export{Ge as default};

View File

@ -1 +1 @@
import{R as y,C as v}from"./index-eec830e6.js";import{cv as f,c as d,A as P,d as w,U as o,V as c,W as r,Z as S,a8 as V,aG as O,a3 as R,X as u,Y as p,a4 as b,ak as $,a0 as x,R as H,J as _,K as m}from"./index-5ed9cd5a.js";const A=f(y),E=f(v);var L={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M878.3 392.1L631.9 145.7c-6.5-6.5-15-9.7-23.5-9.7s-17 3.2-23.5 9.7L423.8 306.9c-12.2-1.4-24.5-2-36.8-2-73.2 0-146.4 24.1-206.5 72.3-15.4 12.3-16.6 35.4-2.7 49.4l181.7 181.7-215.4 215.2a15.8 15.8 0 00-4.6 9.8l-3.4 37.2c-.9 9.4 6.6 17.4 15.9 17.4.5 0 1 0 1.5-.1l37.2-3.4c3.7-.3 7.2-2 9.8-4.6l215.4-215.4 181.7 181.7c6.5 6.5 15 9.7 23.5 9.7 9.7 0 19.3-4.2 25.9-12.4 56.3-70.3 79.7-158.3 70.2-243.4l161.1-161.1c12.9-12.8 12.9-33.8 0-46.8z"}}]},name:"pushpin",theme:"filled"};const C=L;function h(t){for(var e=1;e<arguments.length;e++){var s=arguments[e]!=null?Object(arguments[e]):{},n=Object.keys(s);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(s).filter(function(i){return Object.getOwnPropertyDescriptor(s,i).enumerable}))),n.forEach(function(i){N(t,i,s[i])})}return t}function N(t,e,s){return e in t?Object.defineProperty(t,e,{value:s,enumerable:!0,configurable:!0,writable:!0}):t[e]=s,t}var l=function(e,s){var n=h({},e,s.attrs);return d(P,h({},n,{icon:C}),null)};l.displayName="PushpinFilled";l.inheritAttrs=!1;const z=l,F={class:"record-container"},k={style:{flex:"1"}},I={class:"rec-actions"},B=["onClick"],J=w({__name:"HistoryRecord",props:{records:{}},emits:["reuseRecord"],setup(t){return(e,s)=>{const n=$;return o(),c("div",null,[r("ul",F,[(o(!0),c(S,null,V(e.records.getRecords(),i=>(o(),c("li",{key:i.id,class:"record"},[r("div",k,[O(e.$slots,"default",{record:i},void 0,!0)]),r("div",I,[d(n,{onClick:g=>e.$emit("reuseRecord",i),type:"primary"},{default:R(()=>[u(p(e.$t("restore")),1)]),_:2},1032,["onClick"]),r("div",{class:"pin",onClick:g=>e.records.switchPin(i)},[d(b(z)),u(" "+p(e.records.isPinned(i)?e.$t("unpin"):e.$t("pin")),1)],8,B)])]))),128))])])}}});const q=x(J,[["__scopeId","data-v-834a248f"]]);class a{constructor(e=128,s=[],n=[]){this.maxLength=e,this.records=s,this.pinnedValues=n}isPinned(e){return this.pinnedValues.some(s=>s.id===e.id)}add(e){this.records.length>=this.maxLength&&this.records.pop(),this.records.unshift({...e,id:H()+Date.now(),time:new Date().toLocaleString()})}pin(e){const s=this.records.findIndex(n=>n.id===e.id);s!==-1&&this.records.splice(s,1),this.pinnedValues.push(e)}unpin(e){const s=this.pinnedValues.findIndex(n=>n.id===e.id);s!==-1&&this.pinnedValues.splice(s,1),this.records.unshift(e)}switchPin(e){this.isPinned(e)?this.unpin(e):this.pin(e)}getRecords(){return[...this.pinnedValues,...this.records]}getPinnedValues(){return this.pinnedValues}}const G=_(`${m}fuzzy-search-HistoryRecord`,new a,{serializer:{read:t=>{const e=JSON.parse(t);return new a(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}}),M=_(`${m}tag-search-HistoryRecord`,new a,{serializer:{read:t=>{const e=JSON.parse(t);return new a(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}});export{q as H,E as _,A as a,G as f,M as t};
import{R as y,C as v}from"./index-53d59921.js";import{cw as f,c as d,A as w,d as P,U as o,V as c,W as r,Z as S,a8 as V,aG as O,a3 as R,X as u,Y as p,a4 as b,ak as $,a0 as x,R as H,J as _,K as m}from"./index-8b1d4076.js";const A=f(y),E=f(v);var L={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M878.3 392.1L631.9 145.7c-6.5-6.5-15-9.7-23.5-9.7s-17 3.2-23.5 9.7L423.8 306.9c-12.2-1.4-24.5-2-36.8-2-73.2 0-146.4 24.1-206.5 72.3-15.4 12.3-16.6 35.4-2.7 49.4l181.7 181.7-215.4 215.2a15.8 15.8 0 00-4.6 9.8l-3.4 37.2c-.9 9.4 6.6 17.4 15.9 17.4.5 0 1 0 1.5-.1l37.2-3.4c3.7-.3 7.2-2 9.8-4.6l215.4-215.4 181.7 181.7c6.5 6.5 15 9.7 23.5 9.7 9.7 0 19.3-4.2 25.9-12.4 56.3-70.3 79.7-158.3 70.2-243.4l161.1-161.1c12.9-12.8 12.9-33.8 0-46.8z"}}]},name:"pushpin",theme:"filled"};const C=L;function h(t){for(var e=1;e<arguments.length;e++){var s=arguments[e]!=null?Object(arguments[e]):{},n=Object.keys(s);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(s).filter(function(i){return Object.getOwnPropertyDescriptor(s,i).enumerable}))),n.forEach(function(i){N(t,i,s[i])})}return t}function N(t,e,s){return e in t?Object.defineProperty(t,e,{value:s,enumerable:!0,configurable:!0,writable:!0}):t[e]=s,t}var l=function(e,s){var n=h({},e,s.attrs);return d(w,h({},n,{icon:C}),null)};l.displayName="PushpinFilled";l.inheritAttrs=!1;const z=l,F={class:"record-container"},k={style:{flex:"1"}},I={class:"rec-actions"},B=["onClick"],J=P({__name:"HistoryRecord",props:{records:{}},emits:["reuseRecord"],setup(t){return(e,s)=>{const n=$;return o(),c("div",null,[r("ul",F,[(o(!0),c(S,null,V(e.records.getRecords(),i=>(o(),c("li",{key:i.id,class:"record"},[r("div",k,[O(e.$slots,"default",{record:i},void 0,!0)]),r("div",I,[d(n,{onClick:g=>e.$emit("reuseRecord",i),type:"primary"},{default:R(()=>[u(p(e.$t("restore")),1)]),_:2},1032,["onClick"]),r("div",{class:"pin",onClick:g=>e.records.switchPin(i)},[d(b(z)),u(" "+p(e.records.isPinned(i)?e.$t("unpin"):e.$t("pin")),1)],8,B)])]))),128))])])}}});const q=x(J,[["__scopeId","data-v-834a248f"]]);class a{constructor(e=128,s=[],n=[]){this.maxLength=e,this.records=s,this.pinnedValues=n}isPinned(e){return this.pinnedValues.some(s=>s.id===e.id)}add(e){this.records.length>=this.maxLength&&this.records.pop(),this.records.unshift({...e,id:H()+Date.now(),time:new Date().toLocaleString()})}pin(e){const s=this.records.findIndex(n=>n.id===e.id);s!==-1&&this.records.splice(s,1),this.pinnedValues.push(e)}unpin(e){const s=this.pinnedValues.findIndex(n=>n.id===e.id);s!==-1&&this.pinnedValues.splice(s,1),this.records.unshift(e)}switchPin(e){this.isPinned(e)?this.unpin(e):this.pin(e)}getRecords(){return[...this.pinnedValues,...this.records]}getPinnedValues(){return this.pinnedValues}}const G=_(`${m}fuzzy-search-HistoryRecord`,new a,{serializer:{read:t=>{const e=JSON.parse(t);return new a(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}}),M=_(`${m}tag-search-HistoryRecord`,new a,{serializer:{read:t=>{const e=JSON.parse(t);return new a(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}});export{q as H,E as _,A as a,G as f,M as t};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
vue/dist/assets/stackView-e8919fce.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{u as G,g as d}from"./FileItem-5c27aa5d.js";import{r as b,t as j,ct as m,cu as y,cv as D}from"./index-8b1d4076.js";const r=new Map,A=()=>{const{useEventListen:k,sortedFiles:f,getViewableAreaFiles:w}=G().toRefs(),c=b(d.defaultChangeIndchecked),u=b(d.defaultSeedChangeChecked),g=async()=>{if(await j(100),!c.value)return;const o=w.value().filter(e=>m(e.fullpath)&&!e.gen_info_obj);if(!o.length)return;const t=await y(o.map(e=>e.fullpath).filter(e=>!r.has(e)));o.forEach(e=>{const i=t[e.fullpath]||r.get(e.fullpath)||"";r.set(e.fullpath,i),e.gen_info_obj=D(i),e.gen_info_raw=i})};k.value("viewableAreaFilesChange",g);const F=o=>{const t=f.value;return[o,u.value,t[o-1],t[o],t[o+1]]};function I(o,t,e,i){const a={diff:{},empty:!0,ownFile:"",otherFile:""};if(t+e<0||t+e>=f.value.length||f.value[t]==null||!("gen_info_obj"in f.value[t])||!("gen_info_obj"in f.value[t+e]))return a;const l=o,s=f.value[t+e].gen_info_obj;if(s==null)return a;const h=["hashes","resources"];a.diff={},a.ownFile=i.name,a.otherFile=f.value[t+e].name,a.empty=!1,u.value||h.push("seed");for(const n in l)if(!h.includes(n)){if(!(n in s)){a.diff[n]="+";continue}if(l[n]!=s[n])if(n.includes("rompt")&&l[n]!=""&&s[n]!=""){const p=l[n].split(","),C=s[n].split(",");let _=0;for(const v in p)p[v]!=C[v]&&_++;a.diff[n]=_}else a.diff[n]=[l[n],s[n]]}return a}return{getGenDiff:I,changeIndchecked:c,seedChangeChecked:u,getRawGenParams:()=>g(),getGenDiffWatchDep:F}};export{A as u};

View File

@ -1 +0,0 @@
import{u as G,g as d}from"./FileItem-2ecfe4d5.js";import{r as b,t as j,cs as m,ct as y,cu as D}from"./index-5ed9cd5a.js";const r=new Map,A=()=>{const{useEventListen:k,sortedFiles:s,getViewableAreaFiles:w}=G().toRefs(),c=b(d.defaultChangeIndchecked),u=b(d.defaultSeedChangeChecked),g=async()=>{if(await j(100),!c.value)return;const o=w.value().filter(e=>m(e.fullpath)&&!e.gen_info_obj);if(!o.length)return;const t=await y(o.map(e=>e.fullpath).filter(e=>!r.has(e)));o.forEach(e=>{const i=t[e.fullpath]||r.get(e.fullpath)||"";r.set(e.fullpath,i),e.gen_info_obj=D(i),e.gen_info_raw=i})};k.value("viewableAreaFilesChange",g);const F=o=>{const t=s.value;return[o,u.value,t[o-1],t[o],t[o+1]]};function I(o,t,e,i){const a={diff:{},empty:!0,ownFile:"",otherFile:""};if(t+e<0||t+e>=s.value.length||s.value[t]==null||!("gen_info_obj"in s.value[t])||!("gen_info_obj"in s.value[t+e]))return a;const l=o,f=s.value[t+e].gen_info_obj;if(f==null)return a;const h=["hashes","resources"];a.diff={},a.ownFile=i.name,a.otherFile=s.value[t+e].name,a.empty=!1,u.value||h.push("seed");for(const n in l)if(!h.includes(n)){if(!(n in f)){a.diff[n]="+";continue}if(l[n]!=f[n])if(n.includes("rompt")&&l[n]!=""&&f[n]!=""){const p=l[n].split(","),C=f[n].split(",");let _=0;for(const v in p)p[v]!=C[v]&&_++;a.diff[n]=_}else a.diff[n]=[l[n],f[n]]}return a}return{getGenDiff:I,changeIndchecked:c,seedChangeChecked:u,getRawGenParams:()=>g(),getGenDiffWatchDep:F}};export{A as u};

4
vue/dist/index.html vendored
View File

@ -7,8 +7,8 @@
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Infinite Image Browsing</title>
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-5ed9cd5a.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-c290c403.css">
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-8b1d4076.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-d385cc4f.css">
</head>
<body>

View File

@ -161,4 +161,151 @@ export interface RenameFileParams {
export const renameFile = async (data: RenameFileParams) => {
const resp = await axiosInst.value.post('/db/rename', data)
return resp.data as Promise<{ new_path:string }>
}
// ===== Natural language topic clustering =====
export interface BuildIibOutputEmbeddingsReq {
folder?: string
model?: string
force?: boolean
batch_size?: number
max_chars?: number
}
export interface BuildIibOutputEmbeddingsResp {
folder: string
count: number
updated: number
skipped: number
model: string
}
export const buildIibOutputEmbeddings = async (req: BuildIibOutputEmbeddingsReq) => {
const resp = await axiosInst.value.post('/db/build_iib_output_embeddings', req)
return resp.data as BuildIibOutputEmbeddingsResp
}
export interface ClusterIibOutputReq {
folder?: string
folder_paths?: string[]
model?: string
force_embed?: boolean
threshold?: number
batch_size?: number
max_chars?: number
min_cluster_size?: number
lang?: string
// advanced (backend-supported; optional)
force_title?: boolean
use_title_cache?: boolean
assign_noise_threshold?: number
}
export interface ClusterIibOutputResp {
folder: string
model: string
threshold: number
min_cluster_size: number
count: number
clusters: Array<{
id: string
title: string
size: number
paths: string[]
sample_prompt: string
}>
noise: string[]
}
// ===== Async clustering job (progress polling) =====
export interface ClusterIibOutputJobStartResp {
job_id: string
}
export interface ClusterIibOutputJobStatusResp {
job_id: string
status: 'queued' | 'running' | 'done' | 'error'
stage?: string
folders?: string[]
progress?: {
// embedding totals
scanned?: number
to_embed?: number
embedded_done?: number
updated?: number
skipped?: number
folder?: string
// clustering
items_total?: number
items_done?: number
// titling
clusters_total?: number
clusters_done?: number
}
error?: string
result?: ClusterIibOutputResp
}
export const startClusterIibOutputJob = async (req: ClusterIibOutputReq) => {
const resp = await axiosInst.value.post('/db/cluster_iib_output_job_start', req)
return resp.data as ClusterIibOutputJobStartResp
}
export const getClusterIibOutputJobStatus = async (job_id: string) => {
const resp = await axiosInst.value.get('/db/cluster_iib_output_job_status', { params: { job_id } })
return resp.data as ClusterIibOutputJobStatusResp
}
export interface ClusterIibOutputCachedResp {
cache_key: string
cache_hit: boolean
cached_at?: string
stale: boolean
stale_reason?: {
folders_changed?: boolean
reason?: string
path?: string
stored?: string
current?: string
embeddings_changed?: boolean
embeddings_count?: number
embeddings_max_updated_at?: string
}
result?: ClusterIibOutputResp | null
}
export const getClusterIibOutputCached = async (req: ClusterIibOutputReq) => {
const resp = await axiosInst.value.post('/db/cluster_iib_output_cached', req)
return resp.data as ClusterIibOutputCachedResp
}
// ===== Natural language prompt query (RAG-like retrieval) =====
export interface PromptSearchReq {
query: string
folder?: string
folder_paths?: string[]
model?: string
top_k?: number
min_score?: number
ensure_embed?: boolean
max_chars?: number
}
export interface PromptSearchResp {
query: string
folder: string
model: string
count: number
top_k: number
results: Array<{
id: number
path: string
score: number
sample_prompt: string
}>
}
export const searchIibOutputByPrompt = async (req: PromptSearchReq) => {
const resp = await axiosInst.value.post('/db/search_iib_output_by_prompt', req, { timeout: Infinity })
return resp.data as PromptSearchResp
}

View File

@ -30,7 +30,7 @@ export const getTargetFolderFiles = async (folder_path: string) => {
export const deleteFiles = async (file_paths: string[]) => {
const resp = await axiosInst.value.post('/delete_files', { file_paths })
return resp.data as { files: FileNodeInfo[] }
return resp.data as { ok: true }
}
export const moveFiles = async (

View File

@ -69,7 +69,7 @@ const addInterceptor = (axiosInst: AxiosInstance) => {
(resp) => resp,
async (err) => {
if (isAxiosError(err)) {
if (err.response?.status === 401) {
if (err.response?.status === 401 && err.response?.data?.detail?.type === 'secret_verification_failed') {
const key = await promptServerKeyOnce()
if (!key) {
// user cancelled; leave the request rejected as-is
@ -154,6 +154,15 @@ export const getGlobalSetting = async () => {
return data
}
/**
* global_setting app_fe_setting isSync()
* 使 KVGlobalSetting 使
*/
export const getGlobalSettingRaw = async () => {
const resp = await axiosInst.value.get('/global_setting')
return resp.data as GlobalConf
}
export const getVersion = async () => {
const resp = await axiosInst.value.get('/version')
return resp.data as { hash?: string, tag?: string }
@ -207,6 +216,14 @@ export const setAppFeSetting = async (name: keyof GlobalConf['app_fe_setting'],
await axiosInst.value.post('/app_fe_setting', { name, value: JSON.stringify(setting) })
}
/**
* app_fe_setting KV isSync()
* / TopicSearch
*/
export const setAppFeSettingForce = async (name: string, setting: Record<string, any>) => {
await axiosInst.value.post('/app_fe_setting', { name, value: JSON.stringify(setting) })
}
export const removeAppFeSetting = async (name: keyof GlobalConf['app_fe_setting']) => {
if (!isSync()) return
await axiosInst.value.delete('/app_fe_setting', { data: { name } })
@ -214,4 +231,39 @@ export const removeAppFeSetting = async (name: keyof GlobalConf['app_fe_setting'
export const setTargetFrameAsCover = async (body: { path: string, base64_img: string, updated_time: string }) => {
await axiosInst.value.post('/set_target_frame_as_video_cover', body)
}
// AI 相关 API
export interface AIChatMessage {
role: 'user' | 'assistant' | 'system'
content: string
}
export interface AIChatRequest {
messages: AIChatMessage[]
temperature?: number
max_tokens?: number
stream?: boolean
}
export interface AIChatResponse {
id: string
object: string
created: number
model: string
choices: Array<{
index: number
message: AIChatMessage
finish_reason: string
}>
usage: {
prompt_tokens: number
completion_tokens: number
total_tokens: number
}
}
export const aiChat = async (req: AIChatRequest) => {
const resp = await axiosInst.value.post('/ai-chat', req)
return resp.data as AIChatResponse
}

View File

@ -20,6 +20,53 @@ export const de: Partial<IIBI18nMap> = {
exclude: 'Ausschliessen',
showTiktokNavigator: 'Navigationsschaltflächen anzeigen',
showTiktokNavigatorDesc: 'Zeigen Sie die Navigationstasten (Pfeile nach oben/unten) in der TikTok-Ansicht an',
// ===== Topic Search (Experimental) =====
topicSearchExperimental: 'Natürliche Sprach-Kategorisierung & Suche (Experimentell)',
topicSearchTitleExperimental: 'Natürliche Sprach-Kategorisierung & Suche (Experimentell)',
topicSearchScope: 'Bereich',
topicSearchNeedScope: 'Bitte zuerst einen Bereich (Ordner) auswählen',
topicSearchQueryPlaceholder: 'Geben Sie einen Satz ein, um ähnliche Bilder zu finden (RAG-ähnliche Suche)',
topicSearchOpenResults: 'Ergebnisse öffnen',
topicSearchThreshold: 'Schwelle',
topicSearchMinClusterSize: 'Min. Cluster',
topicSearchEmptyNoScope: 'Bitte zuerst einen Bereich auswählen, dann aktualisieren/kategorisieren',
topicSearchEmptyNoTopics: 'Noch keine Themen (versuchen Sie „Min. Cluster“ zu verringern oder „Schwelle“ anzupassen)',
topicSearchChooseScope: 'Bereich auswählen',
topicSearchRefreshAndCluster: 'Aktualisieren/Kategorisieren',
topicSearchScopeModalTitle: 'Vektorisierungsbereich auswählen (Ordner)',
topicSearchScopeTip: 'Standardmäßig ist kein Bereich aktiviert. Sie müssen Ordner auswählen, um zu kategorisieren/suchen. Bereich stammt aus QuickMovePaths.',
topicSearchSavingToBackend: 'Wird im Backend gespeichert...',
topicSearchScopePlaceholder: 'Ordner auswählen (Mehrfachauswahl)',
topicSearchRecallMsg: '{0} / {1} abgerufen (TopK={2})',
topicSearchCacheStale: 'Zwischengespeichertes Ergebnis wird angezeigt (Update verfügbar)',
topicSearchCacheStaleDesc: 'Ordner im ausgewählten Bereich könnten geändert worden sein. Der Cache könnte veraltet sein. Klicken Sie auf „Update“, um Themen neu zu erzeugen (Index wird zuerst aktualisiert).',
topicSearchCacheUpdate: 'Cache aktualisieren',
topicSearchGuideTitle: 'Schnellstart (Experimentell)',
topicSearchGuideStep1: 'Wählen Sie die Ordner (Bereich) zur Analyse aus (Mehrfachauswahl)',
topicSearchGuideStep2: 'Klicken Sie auf „Aktualisieren", um Themenkarten zu erzeugen (inkrementelle Vektorisierung)',
topicSearchGuideStep3: 'Geben Sie einen Satz ein, um zu suchen; ähnliche Bilder werden abgerufen und die Ergebnisse geöffnet',
topicSearchGuideAdvantage1: 'Automatische Gruppierung nach semantischer Ähnlichkeit: KI entdeckt automatisch ähnliche Themen ohne manuelle Kategorisierung',
topicSearchGuideAdvantage2: 'Natürliche Sprachsemantiksuche: Schnelles Finden verwandter Bilder mit einem Satz, ähnlich der RAG-Suche',
topicSearchGuideEmptyReasonNoScope: 'Leer, weil: kein Bereich ausgewählt (standardmäßig deaktiviert). Klicken Sie auf „Bereich“, um Ordner zu wählen.',
topicSearchGuideEmptyReasonNoTopics: 'Leer, weil: für diesen Bereich noch keine Themen erzeugt wurden (Aktualisieren oder Min. Cluster/Schwelle senken).',
topicSearchRequirementsTitle: 'Voraussetzungen',
topicSearchRequirementsOpenai: 'Erforderlich: OPENAI_BASE_URL und OPENAI_API_KEY (Backend-Umgebungsvariablen)',
topicSearchRequirementsDepsPython: 'Erforderlich: Python-Abhängigkeiten numpy und hnswlib (ohne sie ist die Funktion deaktiviert)',
topicSearchRequirementsDepsDesktop: 'Desktop-App: Abhängigkeiten sind enthalten (numpy/hnswlib müssen nicht installiert werden)',
topicSearchRequirementsInstallCmd: 'Installation: pip install numpy hnswlib',
topicSearchJobFailed: 'Job fehlgeschlagen',
topicSearchJobStage: 'Phase',
topicSearchJobQueued: 'Job in Warteschlange…',
topicSearchJobStageEmbedding: 'Vektorisierung…',
topicSearchJobStageClustering: 'Clustering…',
topicSearchJobStageTitling: 'Titel werden erzeugt…',
topicSearchJobStageDone: 'Fertig',
topicSearchJobStageError: 'Fehler',
topicSearchJobEmbeddingDesc: '{0}/{1} vektorisiert (gescannt {2}); aktuell: {3}',
topicSearchJobClusteringDesc: 'Clustering {0}/{1}',
topicSearchJobTitlingDesc: 'Titel {0}/{1}',
'auto.refreshed': 'Automatische Aktualisierung erfolgreich durchgeführt!',
copied: 'In die Zwischenablage kopiert!',
'index.expired': 'Index abgelaufen, automatische Aktualisierung wird durchgeführt',
@ -81,6 +128,7 @@ export const de: Partial<IIBI18nMap> = {
'Fehler beim Senden des Bildes. Bitte kontaktieren Sie den Entwickler mit der Fehlermeldung aus der Konsole.',
confirmDelete: 'Sind Sie sicher, dass Sie dies löschen möchten?',
deleteSuccess: 'Erfolgreich gelöscht',
moveToTrashSuccess: 'In den Papierkorb verschoben',
doubleClickToCopy: 'Doppelklick zum Kopieren',
root: 'Root',
drive: ' Laufwerk',
@ -148,5 +196,13 @@ export const de: Partial<IIBI18nMap> = {
sortByDate: 'Nach Datum sortieren',
fileTypeFilter: 'Dateityp-Filter',
allFiles: 'Alle Dateien',
audio: 'Audio'
audio: 'Audio',
aiAnalyzeTags: 'KI-Tags analysieren',
aiAnalyzeTagsNoPrompt: 'Kein Prompt gefunden',
aiAnalyzeTagsNoCustomTags: 'Keine benutzerdefinierten Tags verfügbar',
aiAnalyzeTagsNoMatchedTags: 'KI hat keine passenden Tags gefunden',
aiAnalyzeTagsNoValidTags: 'Keine gültigen passenden Tags gefunden',
aiAnalyzeTagsAllTagsAlreadyAdded: 'Alle passenden Tags wurden bereits zum Bild hinzugefügt',
aiAnalyzeTagsSuccess: '{0} Tags hinzugefügt: {1}',
aiAnalyzeTagsFailed: 'KI-Tag-Analyse fehlgeschlagen, bitte Konfiguration überprüfen'
}

View File

@ -20,6 +20,53 @@ export const en: IIBI18nMap = {
clearCacheIfNotTakeEffect: 'If the changes do not take effect, try clearing the page cache',
showTiktokNavigator: 'Show Navigation Buttons',
showTiktokNavigatorDesc: 'Show the navigation buttons (up/down arrows) in TikTok view',
// ===== Topic Search (Experimental) =====
topicSearchExperimental: 'Natural Language Categorization & Search (Experimental)',
topicSearchTitleExperimental: 'Natural Language Categorization & Search (Experimental)',
topicSearchScope: 'Scope',
topicSearchNeedScope: 'Please select a scope folder first',
topicSearchQueryPlaceholder: 'Type a sentence to retrieve similar images (RAG-like retrieval)',
topicSearchOpenResults: 'Open results',
topicSearchThreshold: 'Threshold',
topicSearchMinClusterSize: 'Min cluster',
topicSearchEmptyNoScope: 'Please select a scope, then refresh/categorize',
topicSearchEmptyNoTopics: 'No topics yet (try lowering “Min cluster” or adjusting “Threshold”)',
topicSearchChooseScope: 'Select scope',
topicSearchRefreshAndCluster: 'Refresh/Categorize',
topicSearchScopeModalTitle: 'Select vectorization scope (folders)',
topicSearchScopeTip: 'No scope is enabled by default. You must select folders to categorize/search. Scope comes from QuickMovePaths.',
topicSearchSavingToBackend: 'Saving to backend...',
topicSearchScopePlaceholder: 'Select folders (multi-select)',
topicSearchRecallMsg: 'Retrieved {0} / {1} (TopK={2})',
topicSearchCacheStale: 'Showing cached result (update available)',
topicSearchCacheStaleDesc: 'Folders in the selected scope may have changed. The cache may be stale. Click Update to regenerate topics (will update index first).',
topicSearchCacheUpdate: 'Update cache',
topicSearchGuideTitle: 'Quick Start (Experimental)',
topicSearchGuideStep1: 'Select the scope folders to analyze (multi-select)',
topicSearchGuideStep2: 'Click Refresh to generate topic cards (incremental vectorization)',
topicSearchGuideStep3: 'Type a sentence to search; it will retrieve similar images and open the result page',
topicSearchGuideAdvantage1: 'Auto-grouping by semantic similarity: AI automatically discovers similar themes without manual categorization',
topicSearchGuideAdvantage2: 'Natural language semantic search: Quickly find related images with a sentence, similar to RAG retrieval',
topicSearchGuideEmptyReasonNoScope: 'Empty because: no scope selected (disabled by default). Click “Scope” to choose folders.',
topicSearchGuideEmptyReasonNoTopics: 'Empty because: no topics generated yet for this scope (try Refresh or lower Min cluster/Threshold).',
topicSearchRequirementsTitle: 'Requirements',
topicSearchRequirementsOpenai: 'Required: OPENAI_BASE_URL and OPENAI_API_KEY (backend environment variables)',
topicSearchRequirementsDepsPython: 'Required: Python deps numpy and hnswlib (feature is disabled if missing)',
topicSearchRequirementsDepsDesktop: 'Desktop app: deps are bundled (no need to install numpy/hnswlib)',
topicSearchRequirementsInstallCmd: 'Install: pip install numpy hnswlib',
topicSearchJobFailed: 'Job failed',
topicSearchJobStage: 'Stage',
topicSearchJobQueued: 'Job queued…',
topicSearchJobStageEmbedding: 'Embedding…',
topicSearchJobStageClustering: 'Clustering…',
topicSearchJobStageTitling: 'Generating titles…',
topicSearchJobStageDone: 'Done',
topicSearchJobStageError: 'Error',
topicSearchJobEmbeddingDesc: 'Embedded {0}/{1} (scanned {2}); current: {3}',
topicSearchJobClusteringDesc: 'Clustering {0}/{1}',
topicSearchJobTitlingDesc: 'Titling {0}/{1}',
success: 'Success',
setCurrFrameAsVideoPoster: 'Set Current Frame as Video Cover',
sync: 'Sync',
@ -269,6 +316,7 @@ You can specify which snapshot to restore to when starting IIB in the global set
'Failed to send image. Please contact the developer with the error message from the console.',
confirmDelete: 'Are you sure you want to delete?',
deleteSuccess: 'Deleted successfully',
moveToTrashSuccess: 'Moved to trash',
doubleClickToCopy: 'Double-click to copy',
root: 'Root',
drive: ' drive',
@ -384,5 +432,13 @@ You can specify which snapshot to restore to when starting IIB in the global set
'autoTag.fields.seed': 'Seed',
'autoTag.operators.contains': 'Contains',
'autoTag.operators.equals': 'Equals',
'autoTag.operators.regex': 'Regex'
'autoTag.operators.regex': 'Regex',
aiAnalyzeTags: 'AI Analyze Tags',
aiAnalyzeTagsNoPrompt: 'No prompt found',
aiAnalyzeTagsNoCustomTags: 'No custom tags available',
aiAnalyzeTagsNoMatchedTags: 'AI found no matching tags',
aiAnalyzeTagsNoValidTags: 'No valid matching tags found',
aiAnalyzeTagsAllTagsAlreadyAdded: 'All matched tags have already been added to the image',
aiAnalyzeTagsSuccess: 'Added {0} tags: {1}',
aiAnalyzeTagsFailed: 'AI tag analysis failed, please check configuration'
}

View File

@ -18,6 +18,53 @@ export const zhHans = {
clearCacheIfNotTakeEffect: '如果更改没有生效,请尝试清理页面缓存',
showTiktokNavigator: '显示导航按钮',
showTiktokNavigatorDesc: '在 TikTok 视图中显示导航按钮(上/下箭头)',
// ===== Topic Search (Experimental) =====
topicSearchExperimental: '自然语言分类&搜索(实验性)',
topicSearchTitleExperimental: '自然语言分类 & 搜索(实验性)',
topicSearchScope: '范围',
topicSearchNeedScope: '请先选择向量化范围(文件夹)',
topicSearchQueryPlaceholder: '输入一句话召回相似图片RAG 召回)',
topicSearchOpenResults: '打开结果',
topicSearchThreshold: '阈值',
topicSearchMinClusterSize: '最小组',
topicSearchEmptyNoScope: '请先选择范围,然后刷新/归类',
topicSearchEmptyNoTopics: '暂无主题结果(可尝试降低“最小组”或调整“阈值”)',
topicSearchChooseScope: '选择范围',
topicSearchRefreshAndCluster: '刷新/归类',
topicSearchScopeModalTitle: '选择向量化范围(文件夹)',
topicSearchScopeTip: '默认不启用任何范围;必须选择后才能归类/搜索。范围来源于 QuickMovePaths。',
topicSearchSavingToBackend: '正在保存到后端...',
topicSearchScopePlaceholder: '选择文件夹(可多选)',
topicSearchRecallMsg: '召回 {0} / {1}TopK={2}',
topicSearchCacheStale: '已显示缓存结果(可更新)',
topicSearchCacheStaleDesc: '检测到范围内文件夹可能有变更,缓存可能已过期。可点击更新重新生成主题(会先更新索引)。',
topicSearchCacheUpdate: '更新缓存',
topicSearchGuideTitle: '快速上手(实验性)',
topicSearchGuideStep1: '选择要分析的文件夹范围(可多选)',
topicSearchGuideStep2: '点击刷新,生成主题卡片(会增量向量化)',
topicSearchGuideStep3: '输入一句话搜索,会召回相似图片并打开结果页',
topicSearchGuideAdvantage1: '基于语义相似度自动分组AI自动发现相似主题无需手动分类',
topicSearchGuideAdvantage2: '自然语言语义检索用一句话快速找到相关图片类似RAG检索',
topicSearchGuideEmptyReasonNoScope: '当前为空:未选择范围(已默认关闭),请先点“范围”选择文件夹',
topicSearchGuideEmptyReasonNoTopics: '当前为空:该范围内还未生成主题(可点刷新,或调低最小组/阈值)',
topicSearchRequirementsTitle: '使用前置条件',
topicSearchRequirementsOpenai: '必须配置OPENAI_BASE_URL、OPENAI_API_KEY后端环境变量',
topicSearchRequirementsDepsPython: '必须安装Python 依赖 numpy、hnswlib缺少则功能不可用',
topicSearchRequirementsDepsDesktop: '桌面客户端:已内置依赖(无需手动安装 numpy/hnswlib',
topicSearchRequirementsInstallCmd: '安装命令pip install numpy hnswlib',
topicSearchJobFailed: '任务失败',
topicSearchJobStage: '阶段',
topicSearchJobQueued: '已提交任务,准备开始…',
topicSearchJobStageEmbedding: '向量化中Embedding',
topicSearchJobStageClustering: '归类中Clustering',
topicSearchJobStageTitling: '生成标题中LLM',
topicSearchJobStageDone: '完成',
topicSearchJobStageError: '失败',
topicSearchJobEmbeddingDesc: '已向量化 {0}/{1}(扫描 {2});当前:{3}',
topicSearchJobClusteringDesc: '正在归类 {0}/{1}',
topicSearchJobTitlingDesc: '正在生成标题 {0}/{1}',
success: '成功',
setCurrFrameAsVideoPoster: '设置当前帧为视频封面',
sync: '同步',
@ -128,6 +175,7 @@ export const zhHans = {
sendImageFailed: '发送图像失败请携带console的错误消息找开发者',
confirmDelete: '确认删除?',
deleteSuccess: '删除成功',
moveToTrashSuccess: '已移动到回收站',
doubleClickToCopy: '双击复制',
root: '根',
drive: '盘',
@ -362,5 +410,13 @@ export const zhHans = {
'autoTag.fields.seed': 'Seed',
'autoTag.operators.contains': '包含 (Contains)',
'autoTag.operators.equals': '等于 (Equals)',
'autoTag.operators.regex': '正则 (Regex)'
'autoTag.operators.regex': '正则 (Regex)',
aiAnalyzeTags: 'AI分析标签',
aiAnalyzeTagsNoPrompt: '没有找到提示词',
aiAnalyzeTagsNoCustomTags: '没有自定义标签',
aiAnalyzeTagsNoMatchedTags: 'AI没有找到匹配的标签',
aiAnalyzeTagsNoValidTags: '没有找到有效的匹配标签',
aiAnalyzeTagsAllTagsAlreadyAdded: '所有匹配的标签已经添加到图像上了',
aiAnalyzeTagsSuccess: '已添加 {0} 个标签:{1}',
aiAnalyzeTagsFailed: 'AI分析标签失败请检查配置'
}

View File

@ -20,6 +20,53 @@ export const zhHant: Partial<IIBI18nMap> = {
addNewCustomTag: '添加新的自定義標籤',
showTiktokNavigator: '顯示導航按鈕',
showTiktokNavigatorDesc: '在 TikTok 視圖中顯示導航按鈕(上/下箭頭)',
// ===== Topic Search (Experimental) =====
topicSearchExperimental: '自然語言分類&搜尋(實驗性)',
topicSearchTitleExperimental: '自然語言分類 & 搜尋(實驗性)',
topicSearchScope: '範圍',
topicSearchNeedScope: '請先選擇向量化範圍(資料夾)',
topicSearchQueryPlaceholder: '輸入一句話召回相似圖片RAG 召回)',
topicSearchOpenResults: '打開結果',
topicSearchThreshold: '閾值',
topicSearchMinClusterSize: '最小組',
topicSearchEmptyNoScope: '請先選擇範圍,然後刷新/歸類',
topicSearchEmptyNoTopics: '暫無主題結果(可嘗試降低「最小組」或調整「閾值」)',
topicSearchChooseScope: '選擇範圍',
topicSearchRefreshAndCluster: '刷新/歸類',
topicSearchScopeModalTitle: '選擇向量化範圍(資料夾)',
topicSearchScopeTip: '預設不啟用任何範圍;必須選擇後才能歸類/搜尋。範圍來源於 QuickMovePaths。',
topicSearchSavingToBackend: '正在保存到後端...',
topicSearchScopePlaceholder: '選擇資料夾(可多選)',
topicSearchRecallMsg: '召回 {0} / {1}TopK={2}',
topicSearchCacheStale: '已顯示快取結果(可更新)',
topicSearchCacheStaleDesc: '偵測到範圍內資料夾可能有變更,快取可能已過期。可點擊更新重新生成主題(會先更新索引)。',
topicSearchCacheUpdate: '更新快取',
topicSearchGuideTitle: '快速上手(實驗性)',
topicSearchGuideStep1: '選擇要分析的資料夾範圍(可多選)',
topicSearchGuideStep2: '點擊刷新,生成主題卡片(會增量向量化)',
topicSearchGuideStep3: '輸入一句話搜尋,召回相似圖片並打開結果頁',
topicSearchGuideAdvantage1: '基於語義相似度自動分組AI自動發現相似主題無需手動分類',
topicSearchGuideAdvantage2: '自然語言語義檢索用一句話快速找到相關圖片類似RAG檢索',
topicSearchGuideEmptyReasonNoScope: '目前為空:尚未選擇範圍(預設關閉),請先點「範圍」選擇資料夾',
topicSearchGuideEmptyReasonNoTopics: '目前為空:此範圍尚未生成主題(可點刷新,或調低最小組/閾值)',
topicSearchRequirementsTitle: '使用前置條件',
topicSearchRequirementsOpenai: '必須配置OPENAI_BASE_URL、OPENAI_API_KEY後端環境變數',
topicSearchRequirementsDepsPython: '必須安裝Python 依賴 numpy、hnswlib缺少則功能不可用',
topicSearchRequirementsDepsDesktop: '桌面客戶端:已內建依賴(無需手動安裝 numpy/hnswlib',
topicSearchRequirementsInstallCmd: '安裝命令pip install numpy hnswlib',
topicSearchJobFailed: '任務失敗',
topicSearchJobStage: '階段',
topicSearchJobQueued: '已提交任務,準備開始…',
topicSearchJobStageEmbedding: '向量化中Embedding',
topicSearchJobStageClustering: '歸類中Clustering',
topicSearchJobStageTitling: '生成標題中LLM',
topicSearchJobStageDone: '完成',
topicSearchJobStageError: '失敗',
topicSearchJobEmbeddingDesc: '已向量化 {0}/{1}(掃描 {2});目前:{3}',
topicSearchJobClusteringDesc: '正在歸類 {0}/{1}',
topicSearchJobTitlingDesc: '正在生成標題 {0}/{1}',
clearCacheIfNotTakeEffect: '如果更改沒有生效,請嘗試清理頁面緩存',
success: '成功',
setCurrFrameAsVideoPoster: '設置當前幀為視頻封面',
@ -134,6 +181,7 @@ export const zhHant: Partial<IIBI18nMap> = {
sendImageFailed: '發送圖像失敗,請攜帶console的錯誤訊息找開發者',
confirmDelete: '確認刪除?',
deleteSuccess: '刪除成功',
moveToTrashSuccess: '已移動到回收站',
doubleClickToCopy: '雙擊複製',
promptcompare: 'Compare Prompts',
root: '根',
@ -366,5 +414,13 @@ export const zhHant: Partial<IIBI18nMap> = {
'autoTag.fields.seed': 'Seed',
'autoTag.operators.contains': '包含 (Contains)',
'autoTag.operators.equals': '等於 (Equals)',
'autoTag.operators.regex': '正則 (Regex)'
'autoTag.operators.regex': '正則 (Regex)',
aiAnalyzeTags: 'AI分析標籤',
aiAnalyzeTagsNoPrompt: '沒有找到提示詞',
aiAnalyzeTagsNoCustomTags: '沒有自定義標籤',
aiAnalyzeTagsNoMatchedTags: 'AI沒有找到匹配的標籤',
aiAnalyzeTagsNoValidTags: '沒有找到有效的匹配標籤',
aiAnalyzeTagsAllTagsAlreadyAdded: '所有匹配的標籤已經添加到圖像上了',
aiAnalyzeTagsSuccess: '已添加 {0} 個標籤:{1}',
aiAnalyzeTagsFailed: 'AI分析標籤失敗請檢查配置'
}

View File

@ -21,8 +21,12 @@ const compMap: Record<TabPane['type'], ReturnType<typeof defineAsyncComponent>>
'tag-search-matched-image-grid': defineAsyncComponent(
() => import('@/page/TagSearch/MatchedImageGrid.vue')
),
'topic-search-matched-image-grid': defineAsyncComponent(
() => import('@/page/TopicSearch/MatchedImageGrid.vue')
),
'tag-search': defineAsyncComponent(() => import('@/page/TagSearch/TagSearch.vue')),
'fuzzy-search': defineAsyncComponent(() => import('@/page/TagSearch/SubstrSearch.vue')),
'topic-search': defineAsyncComponent(() => import('@/page/TopicSearch/TopicSearch.vue')),
'img-sli': defineAsyncComponent(() => import('@/page/ImgSli/ImgSliPagePane.vue')),
'batch-download': defineAsyncComponent(() => import('@/page/batchDownload/batchDownload.vue')),
'grid-view': defineAsyncComponent(() => import('@/page/gridView/gridView.vue')),

View File

@ -40,6 +40,7 @@ const compCnMap: Partial<Record<TabPane['type'], string>> = {
local: t('local'),
'tag-search': t('imgSearch'),
'fuzzy-search': t('fuzzy-search'),
'topic-search': t('topicSearchExperimental'),
'batch-download': t('batchDownload') + ' / ' + t('archive'),
'workspace-snapshot': t('WorkspaceSnapshot'),
'random-image': t('randomImage'),
@ -51,6 +52,7 @@ const createPane = (type: TabPane['type'], path?: string, mode?: FileTransModeIn
switch (type) {
case 'grid-view':
case 'tag-search-matched-image-grid':
case 'topic-search-matched-image-grid':
case 'img-sli':
return
case 'global-setting':
@ -58,6 +60,7 @@ const createPane = (type: TabPane['type'], path?: string, mode?: FileTransModeIn
case 'batch-download':
case 'workspace-snapshot':
case 'fuzzy-search':
case 'topic-search':
case 'random-image':
case 'empty':
pane = { type, name: compCnMap[type]!, key: Date.now() + uniqueId() }

View File

@ -0,0 +1,264 @@
<script lang="ts" setup>
import fileItemCell from '@/components/FileItem.vue'
import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css'
// @ts-ignore
import { RecycleScroller } from '@zanllp/vue-virtual-scroller'
import { toRawFileUrl } from '@/util/file'
import { nextTick, watch, reactive } from 'vue'
import { copy2clipboardI18n } from '@/util'
import fullScreenContextMenu from '@/page/fileTransfer/fullScreenContextMenu.vue'
import { LeftCircleOutlined, RightCircleOutlined } from '@/icon'
import { useImageSearch } from '@/page/TagSearch/hook'
import { useGlobalStore } from '@/store/useGlobalStore'
import { useKeepMultiSelect } from '@/page/fileTransfer/hook'
import { openTiktokViewWithFiles } from '@/util/tiktokHelper'
import { batchGetFilesInfo } from '@/api/files'
const props = defineProps<{
tabIdx: number
paneIdx: number
id: string
title: string
paths: string[]
}>()
type Iter = {
res: any[]
load: boolean
loading: boolean
next: () => Promise<void>
reset: (opts?: { refetch?: boolean }) => Promise<void>
}
const iter = reactive<Iter>({
res: [],
load: false,
loading: false,
async next() {
if (iter.loading || iter.load) return
iter.loading = true
try {
const pageSize = 200
const offset = iter.res.length
const slice = (props.paths ?? []).slice(offset, offset + pageSize)
if (!slice.length) {
iter.load = true
return
}
const infoMap = await batchGetFilesInfo(slice)
const files = slice.map((p) => infoMap[p]).filter(Boolean)
iter.res.push(...files)
if (offset + pageSize >= (props.paths?.length ?? 0)) {
iter.load = true
}
} finally {
iter.loading = false
}
},
async reset() {
iter.res = []
iter.load = false
await iter.next()
}
})
const {
queue,
images,
onContextMenuClickU,
stackViewEl,
previewIdx,
previewing,
onPreviewVisibleChange,
previewImgMove,
canPreview,
itemSize,
gridItems,
showGenInfo,
imageGenInfo,
q: genInfoQueue,
multiSelectedIdxs,
onFileItemClick,
scroller,
showMenuIdx,
onFileDragStart,
onFileDragEnd,
cellWidth,
onScroll,
saveAllFileAsJson,
saveLoadedFileAsJson,
changeIndchecked,
seedChangeChecked,
getGenDiff,
getGenDiffWatchDep
} = useImageSearch(iter as any)
watch(
() => props.paths,
async () => {
await iter.reset({ refetch: true } as any)
await nextTick()
scroller.value?.scrollToItem(0)
onScroll()
},
{ immediate: true }
)
const g = useGlobalStore()
const { onClearAllSelected, onSelectAll, onReverseSelect } = useKeepMultiSelect()
const onTiktokViewClick = () => {
if (images.value.length === 0) return
openTiktokViewWithFiles(images.value, 0)
}
</script>
<template>
<div class="container" ref="stackViewEl">
<MultiSelectKeep
:show="!!multiSelectedIdxs.length || g.keepMultiSelect"
@clear-all-selected="onClearAllSelected"
@select-all="onSelectAll"
@reverse-select="onReverseSelect"
/>
<ASpin size="large" :spinning="!queue.isIdle || iter.loading">
<AModal v-model:visible="showGenInfo" width="70vw" mask-closable @ok="showGenInfo = false">
<template #cancelText />
<ASkeleton active :loading="!genInfoQueue.isIdle">
<div
style="
width: 100%;
word-break: break-all;
white-space: pre-line;
max-height: 70vh;
overflow: auto;
"
@dblclick="copy2clipboardI18n(imageGenInfo)"
>
<div class="hint">{{ $t('doubleClickToCopy') }}</div>
{{ imageGenInfo }}
</div>
</ASkeleton>
</AModal>
<div class="action-bar">
<div class="title line-clamp-1">🧩 {{ props.title }}</div>
<div flex-placeholder />
<a-button @click="onTiktokViewClick" :disabled="!images?.length">{{ $t('tiktokView') }}</a-button>
<a-button @click="saveLoadedFileAsJson" :disabled="!images?.length">{{ $t('saveLoadedImageAsJson') }}</a-button>
<a-button @click="saveAllFileAsJson" :disabled="!images?.length">{{ $t('saveAllAsJson') }}</a-button>
</div>
<RecycleScroller
ref="scroller"
class="file-list"
v-if="images?.length"
:items="images"
:item-size="itemSize.first"
key-field="fullpath"
:item-secondary-size="itemSize.second"
:gridItems="gridItems"
@scroll="onScroll"
>
<template #after>
<div style="padding: 16px 0 512px;" />
</template>
<template v-slot="{ item: file, index: idx }">
<file-item-cell
:idx="idx"
:file="file"
:cell-width="cellWidth"
v-model:show-menu-idx="showMenuIdx"
@dragstart="onFileDragStart"
@dragend="onFileDragEnd"
@file-item-click="onFileItemClick"
@tiktok-view="(_file, idx) => openTiktokViewWithFiles(images, idx)"
:full-screen-preview-image-url="images[previewIdx] ? toRawFileUrl(images[previewIdx]) : ''"
:selected="multiSelectedIdxs.includes(idx)"
@context-menu-click="onContextMenuClickU"
@preview-visible-change="onPreviewVisibleChange"
:is-selected-mutil-files="multiSelectedIdxs.length > 1"
:enable-change-indicator="changeIndchecked"
:seed-change-checked="seedChangeChecked"
:get-gen-diff="getGenDiff"
:get-gen-diff-watch-dep="getGenDiffWatchDep"
/>
</template>
</RecycleScroller>
<div v-else class="no-res-hint">
<p class="hint">暂无结果</p>
</div>
<div v-if="previewing" class="preview-switch">
<LeftCircleOutlined @click="previewImgMove('prev')" :class="{ disable: !canPreview('prev') }" />
<RightCircleOutlined @click="previewImgMove('next')" :class="{ disable: !canPreview('next') }" />
</div>
</ASpin>
<fullScreenContextMenu
v-if="previewing && images && images[previewIdx]"
:file="images[previewIdx]"
:idx="previewIdx"
@context-menu-click="onContextMenuClickU"
/>
</div>
</template>
<style scoped lang="scss">
.container {
background: var(--zp-secondary-background);
position: relative;
height: var(--pane-max-height);
}
.action-bar {
display: flex;
align-items: center;
user-select: none;
gap: 6px;
padding: 6px 8px;
}
.title {
font-weight: 700;
max-width: 40vw;
}
.file-list {
list-style: none;
padding: 8px;
overflow: auto;
height: calc(var(--pane-max-height) - 44px);
width: 100%;
}
.no-res-hint {
height: calc(var(--pane-max-height) - 44px);
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
.hint {
font-size: 1.2em;
opacity: 0.7;
}
}
.preview-switch {
position: fixed;
bottom: 24px;
right: 24px;
display: flex;
gap: 8px;
font-size: 36px;
user-select: none;
}
.disable {
opacity: 0.3;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,668 @@
<script setup lang="ts">
import { getGlobalSettingRaw, setAppFeSettingForce } from '@/api'
import {
getClusterIibOutputJobStatus,
getClusterIibOutputCached,
searchIibOutputByPrompt,
startClusterIibOutputJob,
type ClusterIibOutputJobStatusResp,
type ClusterIibOutputCachedResp,
type ClusterIibOutputResp,
type PromptSearchResp
} from '@/api/db'
import { updateImageData } from '@/api/db'
import { t } from '@/i18n'
import { useGlobalStore } from '@/store/useGlobalStore'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { uniqueId } from 'lodash-es'
import { message } from 'ant-design-vue'
import { isTauri } from '@/util/env'
import { useLocalStorage } from '@vueuse/core'
const props = defineProps<{ tabIdx: number; paneIdx: number }>()
const g = useGlobalStore()
const loading = ref(false)
// Default stricter to avoid over-merged broad topics for natural-language prompts.
const threshold = ref(0.9)
const minClusterSize = ref(2)
const result = ref<ClusterIibOutputResp | null>(null)
const cacheInfo = ref<ClusterIibOutputCachedResp | null>(null)
const _REQS_LS_KEY = 'iib_topic_search_hide_requirements_v1'
// true = show requirements; false = hidden
const showRequirements = useLocalStorage<boolean>(_REQS_LS_KEY, true)
const hideRequirements = () => {
showRequirements.value = false
}
const job = ref<ClusterIibOutputJobStatusResp | null>(null)
const jobId = ref<string>('')
let _jobTimer: any = null
const jobRunning = computed(() => {
const st = job.value?.status
return st === 'queued' || st === 'running'
})
const jobStageText = computed(() => {
const st = String(job.value?.stage || '')
if (!st || st === 'queued' || st === 'init') return t('topicSearchJobQueued')
if (st === 'embedding') return t('topicSearchJobStageEmbedding')
if (st === 'clustering') return t('topicSearchJobStageClustering')
if (st === 'titling') return t('topicSearchJobStageTitling')
if (st === 'done') return t('topicSearchJobStageDone')
if (st === 'error') return t('topicSearchJobStageError')
return `${t('topicSearchJobStage')}: ${st}`
})
const jobPercent = computed(() => {
const p = job.value?.progress
if (!p) return 0
const total = Number(p.to_embed ?? 0)
const done = Number(p.embedded_done ?? 0)
if (total <= 0) return 0
const v = Math.floor((done / total) * 100)
return Math.max(0, Math.min(100, v))
})
const jobDesc = computed(() => {
const p = job.value?.progress
if (!p) return ''
if (job.value?.stage === 'embedding') {
const done = Number(p.embedded_done ?? 0)
const total = Number(p.to_embed ?? 0)
const scanned = Number(p.scanned ?? 0)
const folder = String(p.folder ?? '')
return t('topicSearchJobEmbeddingDesc', [done, total, scanned, folder])
}
if (job.value?.stage === 'clustering') {
const done = Number(p.items_done ?? 0)
const total = Number(p.items_total ?? 0)
return t('topicSearchJobClusteringDesc', [done, total])
}
if (job.value?.stage === 'titling') {
const done = Number(p.clusters_done ?? 0)
const total = Number(p.clusters_total ?? 0)
return t('topicSearchJobTitlingDesc', [done, total])
}
return ''
})
const query = ref('')
const qLoading = ref(false)
const qResult = ref<PromptSearchResp | null>(null)
const scopeOpen = ref(false)
const selectedFolders = ref<string[]>([])
const _scopeInitDone = ref(false)
const _SCOPE_SETTING_NAME = 'topic_search_scope'
const _saving = ref(false)
let _saveTimer: any = null
let _lastSavedSig = ''
const excludedDirs = computed(() => {
const list = (g.quickMovePaths ?? []) as any[]
return list
.filter((v) => {
const key = String(v?.key ?? '')
const types = (v?.types ?? []) as string[]
// 3 / home /
return types.includes('preset') && ['cwd', 'home', 'desktop'].includes(key)
})
.map((v) => String(v?.dir ?? ''))
.filter(Boolean)
})
watch(
excludedDirs,
(dirs) => {
if (!dirs?.length) return
selectedFolders.value = (selectedFolders.value ?? []).filter((p) => !dirs.includes(p))
},
{ immediate: true }
)
const folderOptions = computed(() => {
const list = (g.quickMovePaths ?? []) as any[]
return list
.filter((v) => {
const key = String(v?.key ?? '')
const types = (v?.types ?? []) as string[]
return !(types.includes('preset') && ['cwd', 'home', 'desktop'].includes(key))
})
.map((v) => ({ value: v.dir, label: v.zh || v.dir }))
})
const scopeCount = computed(() => (selectedFolders.value ?? []).filter(Boolean).length)
const scopeFolders = computed(() => (selectedFolders.value ?? []).filter(Boolean))
const clusters = computed(() => result.value?.clusters ?? [])
const loadScopeFromBackend = async () => {
if (_scopeInitDone.value) return
try {
const conf = await getGlobalSettingRaw()
// store
g.conf = conf as any
} catch (e) {
// ignoreaxios interceptor
}
const app = (g.conf?.app_fe_setting as any) || {}
const savedPaths = app?.[_SCOPE_SETTING_NAME]?.folder_paths
if (Array.isArray(savedPaths) && savedPaths.length && (!selectedFolders.value?.length)) {
selectedFolders.value = savedPaths.map((x: any) => String(x)).filter(Boolean)
}
_scopeInitDone.value = true
}
const saveScopeToBackend = async () => {
if (g.conf?.is_readonly) return
if (!_scopeInitDone.value) return
const payload = {
folder_paths: scopeFolders.value,
updated_at: Date.now()
}
await setAppFeSettingForce(_SCOPE_SETTING_NAME, payload)
if (g.conf?.app_fe_setting) {
(g.conf.app_fe_setting as any)[_SCOPE_SETTING_NAME] = payload
}
}
const scheduleSaveScope = () => {
if (g.conf?.is_readonly) return
if (!_scopeInitDone.value) return
const sig = JSON.stringify(scopeFolders.value)
if (sig === _lastSavedSig) return
if (_saveTimer) clearTimeout(_saveTimer)
_saveTimer = setTimeout(async () => {
_saving.value = true
try {
await saveScopeToBackend()
_lastSavedSig = sig
} finally {
_saving.value = false
}
}, 500)
}
const stopJobPoll = () => {
if (_jobTimer) {
clearInterval(_jobTimer)
_jobTimer = null
}
}
const pollJob = async () => {
const id = jobId.value
if (!id) return
const st = await getClusterIibOutputJobStatus(id)
job.value = st
if (st.status === 'done') {
stopJobPoll()
loading.value = false
if (st.result) result.value = st.result
cacheInfo.value = null
} else if (st.status === 'error') {
stopJobPoll()
loading.value = false
message.error(st.error || t('topicSearchJobFailed'))
}
}
const loadCached = async () => {
if (!scopeCount.value) return
// best-effort: do not block entering the page
try {
const cached = await getClusterIibOutputCached({
threshold: threshold.value,
min_cluster_size: minClusterSize.value,
lang: g.lang,
folder_paths: scopeFolders.value
})
cacheInfo.value = cached
if (cached.cache_hit && cached.result) {
result.value = cached.result
}
} catch (e) {
// ignore
}
}
const refresh = async () => {
if (g.conf?.is_readonly) return
if (!scopeCount.value) {
message.warning(t('topicSearchNeedScope'))
scopeOpen.value = true
return
}
stopJobPoll()
loading.value = true
job.value = null
jobId.value = ''
try {
// Ensure DB file index is up to date before clustering (so newly added/moved images are included).
await updateImageData()
const started = await startClusterIibOutputJob({
threshold: threshold.value,
min_cluster_size: minClusterSize.value,
lang: g.lang,
folder_paths: scopeFolders.value
})
jobId.value = started.job_id
// poll immediately + interval
await pollJob()
_jobTimer = setInterval(() => {
void pollJob()
}, 800)
} catch (e) {
loading.value = false
throw e
}
}
const runQuery = async () => {
const q = (query.value || '').trim()
if (!q) return
if (!scopeCount.value) {
message.warning(t('topicSearchNeedScope'))
scopeOpen.value = true
return
}
qLoading.value = true
try {
qResult.value = await searchIibOutputByPrompt({
query: q,
top_k: 80,
ensure_embed: true,
folder_paths: scopeFolders.value
})
//
openQueryResult()
} finally {
qLoading.value = false
}
}
const openQueryResult = () => {
const paths = (qResult.value?.results ?? []).map((r) => r.path).filter(Boolean)
if (!paths.length) return
const title = `Query: ${query.value.trim()}${paths.length}`
const pane = {
type: 'topic-search-matched-image-grid' as const,
name: title,
key: Date.now() + uniqueId(),
id: uniqueId(),
title,
paths
}
const tab = g.tabList[props.tabIdx]
tab.panes.push(pane as any)
tab.key = (pane as any).key
}
const openCluster = (item: ClusterIibOutputResp['clusters'][0]) => {
const pane = {
type: 'topic-search-matched-image-grid' as const,
name: `${item.title}${item.size}`,
key: Date.now() + uniqueId(),
id: uniqueId(),
title: item.title,
paths: item.paths
}
const tab = g.tabList[props.tabIdx]
tab.panes.push(pane as any)
tab.key = (pane as any).key
}
onMounted(() => {
//
void (async () => {
await loadScopeFromBackend()
if (scopeCount.value) {
await loadCached()
}
})()
})
onBeforeUnmount(() => {
stopJobPoll()
})
watch(
() => scopeFolders.value,
() => {
// OK
scheduleSaveScope()
},
{ deep: true }
)
</script>
<template>
<div class="topic-search">
<div class="toolbar">
<div class="left">
<div class="title">
<span class="icon">🧠</span>
<span>{{ $t('topicSearchTitleExperimental') }}</span>
</div>
<a-tag v-if="result" color="blue"> {{ result.count }} </a-tag>
<a-tag v-if="result" color="geekblue"> {{ result.clusters.length }}</a-tag>
<a-tag v-if="result" color="default"> {{ result.noise.length }}</a-tag>
</div>
<div class="right">
<a-button @click="scopeOpen = true">
{{ $t('topicSearchScope') }}
<span v-if="scopeCount" style="opacity: 0.75;">{{ scopeCount }}</span>
</a-button>
<a-input
v-model:value="query"
style="width: min(420px, 72vw);"
:placeholder="$t('topicSearchQueryPlaceholder')"
:disabled="qLoading"
@keydown.enter="runQuery"
allow-clear
/>
<a-button :loading="qLoading" @click="runQuery">{{ $t('search') }}</a-button>
<a-button v-if="qResult?.results?.length" @click="openQueryResult">{{ $t('topicSearchOpenResults') }}</a-button>
<span class="label">{{ $t('topicSearchThreshold') }}</span>
<a-input-number v-model:value="threshold" :min="0.5" :max="0.99" :step="0.01" />
<span class="label">{{ $t('topicSearchMinClusterSize') }}</span>
<a-input-number v-model:value="minClusterSize" :min="1" :max="50" :step="1" />
<a-button type="primary" ghost :loading="loading" :disabled="g.conf?.is_readonly" @click="refresh">{{ $t('refresh') }}</a-button>
</div>
</div>
<a-alert
v-if="g.conf?.is_readonly"
type="warning"
:message="$t('readonlyModeSettingPageDesc')"
style="margin: 12px 0;"
show-icon
/>
<a-alert
v-if="showRequirements"
type="info"
show-icon
closable
style="margin: 10px 0 0 0;"
:message="$t('topicSearchRequirementsTitle')"
@close="hideRequirements"
>
<template #description>
<div style="display: grid; gap: 6px;">
<div>
<span style="margin-right: 6px;">🔑</span>
<span>{{ $t('topicSearchRequirementsOpenai') }}</span>
</div>
<template v-if="isTauri">
<div>
<span style="margin-right: 6px;">🧩</span>
<span>{{ $t('topicSearchRequirementsDepsDesktop') }}</span>
</div>
</template>
<template v-else>
<div>
<span style="margin-right: 6px;">🐍</span>
<span>{{ $t('topicSearchRequirementsDepsPython') }}</span>
</div>
<div style="opacity: 0.85;">
<span style="margin-right: 6px;">💻</span>
<span>{{ $t('topicSearchRequirementsInstallCmd') }}</span>
</div>
</template>
</div>
</template>
</a-alert>
<a-alert
v-if="cacheInfo?.cache_hit && cacheInfo?.stale"
type="warning"
show-icon
style="margin: 10px 0 0 0;"
:message="$t('topicSearchCacheStale')"
>
<template #description>
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<span style="opacity: 0.85;">{{ $t('topicSearchCacheStaleDesc') }}</span>
<a-button size="small" :loading="loading || jobRunning" :disabled="g.conf?.is_readonly" @click="refresh">
{{ $t('topicSearchCacheUpdate') }}
</a-button>
</div>
</template>
</a-alert>
<div v-if="jobRunning" style="margin: 10px 0 0 0;">
<a-alert type="info" show-icon :message="jobStageText" :description="jobDesc" />
<a-progress :percent="jobPercent" size="small" style="margin-top: 8px;" />
</div>
<a-spin :spinning="loading">
<div v-if="qResult" style="margin-top: 10px;">
<a-alert
type="info"
:message="$t('topicSearchRecallMsg', [qResult.results.length, qResult.count, qResult.top_k])"
show-icon
/>
</div>
<div class="grid" v-if="clusters.length">
<div class="card" v-for="c in clusters" :key="c.id" @click="openCluster(c)">
<div class="card-top">
<div class="card-title line-clamp-1">{{ c.title }}</div>
<div class="card-count">{{ c.size }}</div>
</div>
<div class="card-desc line-clamp-2">{{ c.sample_prompt }}</div>
</div>
</div>
<div class="empty" v-else>
<a-alert
type="info"
show-icon
:message="$t('topicSearchGuideTitle')"
style="margin-bottom: 10px;"
/>
<div class="guide">
<div class="guide-row">
<span class="guide-icon">🗂</span>
<span class="guide-text">{{ $t('topicSearchGuideStep1') }}</span>
<a-button size="small" @click="scopeOpen = true">{{ $t('topicSearchScope') }}</a-button>
</div>
<div class="guide-row">
<span class="guide-icon">🧠</span>
<span class="guide-text">{{ $t('topicSearchGuideStep2') }}</span>
<a-button size="small" :loading="loading" :disabled="g.conf?.is_readonly" @click="refresh">{{ $t('refresh') }}</a-button>
</div>
<div class="guide-row">
<span class="guide-icon">🔎</span>
<span class="guide-text">{{ $t('topicSearchGuideStep3') }}</span>
</div>
<div class="guide-row">
<span class="guide-icon"></span>
<span class="guide-text">{{ $t('topicSearchGuideAdvantage1') }}</span>
</div>
<div class="guide-row">
<span class="guide-icon">🚀</span>
<span class="guide-text">{{ $t('topicSearchGuideAdvantage2') }}</span>
</div>
<div class="guide-hint">
<span class="guide-icon">💡</span>
<span class="guide-text" v-if="!scopeCount">{{ $t('topicSearchGuideEmptyReasonNoScope') }}</span>
<span class="guide-text" v-else>{{ $t('topicSearchGuideEmptyReasonNoTopics') }}</span>
</div>
</div>
</div>
</a-spin>
<a-modal
v-model:visible="scopeOpen"
:title="$t('topicSearchScopeModalTitle')"
:mask-closable="true"
@ok="
() => {
scopeOpen = false
void saveScopeToBackend()
}
"
>
<a-alert
type="info"
show-icon
:message="$t('topicSearchScopeTip')"
style="margin-bottom: 10px;"
/>
<a-alert
v-if="_saving"
type="info"
show-icon
:message="$t('topicSearchSavingToBackend')"
style="margin-bottom: 10px;"
/>
<a-select
v-model:value="selectedFolders"
mode="multiple"
style="width: 100%;"
:options="folderOptions"
:placeholder="$t('topicSearchScopePlaceholder')"
:max-tag-count="3"
:getPopupContainer="(trigger: HTMLElement) => trigger.parentElement || trigger"
allow-clear
/>
</a-modal>
</div>
</template>
<style scoped lang="scss">
.topic-search {
height: var(--pane-max-height);
overflow: auto;
padding: 12px;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
background: var(--zp-primary-background);
border-radius: 12px;
}
.left {
display: flex;
align-items: center;
gap: 8px;
}
.title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 700;
}
.right {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.label {
color: #888;
}
.guide {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
background: var(--zp-primary-background);
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
}
.guide-row {
display: flex;
align-items: center;
gap: 10px;
}
.guide-hint {
margin-top: 4px;
display: flex;
align-items: center;
gap: 10px;
opacity: 0.85;
}
.guide-icon {
width: 22px;
text-align: center;
flex: 0 0 22px;
}
.guide-text {
flex: 1 1 auto;
min-width: 0;
color: rgba(0, 0, 0, 0.75);
}
.grid {
margin-top: 12px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 10px;
}
.card {
background: var(--zp-primary-background);
border-radius: 12px;
padding: 10px 12px;
cursor: pointer;
border: 1px solid rgba(0, 0, 0, 0.06);
}
.card:hover {
border-color: rgba(24, 144, 255, 0.6);
}
.card-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.card-title {
font-weight: 700;
}
.card-count {
min-width: 28px;
text-align: right;
opacity: 0.75;
}
.card-desc {
margin-top: 6px;
color: #666;
font-size: 12px;
}
.empty {
height: calc(var(--pane-max-height) - 72px);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 12px;
}
.hint {
font-size: 16px;
opacity: 0.75;
}
</style>

View File

@ -33,6 +33,8 @@ import { prefix } from '@/util/const'
// @ts-ignore
import * as Pinyin from 'jian-pinyin'
import { Tag } from '@/api/db'
import { aiChat } from '@/api'
import { message } from 'ant-design-vue'
const global = useGlobalStore()
@ -58,9 +60,15 @@ const geninfoStructNoPrompts = computed(() => {
let p = parse(cleanImageGenInfo.value)
delete p.prompt
delete p.negativePrompt
delete p.extraJsonMetaInfo
return p
})
// extraJsonMetaInfo meta使 imageGenInfo HTML
const extraJsonMetaInfo = computed(() => {
const p = parse(imageGenInfo.value) // 使 imageGenInfo cleanImageGenInfo
return p.extraJsonMetaInfo as Record<string, any> | undefined
})
const emit = defineEmits<{
(type: 'contextMenuClick', e: MenuInfo, file: FileNodeInfo, idx: number): void
@ -305,6 +313,92 @@ const onTiktokViewClick = () => {
emit('contextMenuClick', { key: 'tiktokView' } as any, props.file, props.idx)
}
// AItag
const analyzingTags = ref(false)
const analyzeTagsWithAI = async () => {
if (!geninfoStruct.value.prompt) {
message.warning(t('aiAnalyzeTagsNoPrompt'))
return
}
if (!global.conf?.all_custom_tags?.length) {
message.warning(t('aiAnalyzeTagsNoCustomTags'))
return
}
analyzingTags.value = true
try {
const prompt = geninfoStruct.value.prompt
const availableTags = global.conf.all_custom_tags.map(tag => tag.name).join(', ')
const systemMessage = `You are a professional AI assistant responsible for analyzing Stable Diffusion prompts and categorizing them into appropriate tags.
Your task is:
1. Analyze the given prompt
2. Find all relevant tags from the provided tag list
3. Return only the matching tag names, separated by commas
4. If no tags match, return an empty string
5. Tag matching should be based on semantic similarity and thematic relevance
Available tags: ${availableTags}
Please return only tag names, do not include any other content.`
const response = await aiChat({
messages: [
{ role: 'system', content: systemMessage },
{ role: 'user', content: `Please analyze this prompt and return matching tags: ${prompt}` }
],
temperature: 0.3,
max_tokens: 200
})
const matchedTagsText = response.choices[0].message.content.trim()
if (!matchedTagsText) {
message.info(t('aiAnalyzeTagsNoMatchedTags'))
return
}
//
const matchedTagNames = matchedTagsText.split(',').map((name: string) => name.trim()).filter((name: string) => name)
// tag
const matchedTags = global.conf.all_custom_tags.filter((tag: Tag) =>
matchedTagNames.some((matchedName: string) =>
tag.name.toLowerCase() === matchedName.toLowerCase() ||
tag.name.toLowerCase().includes(matchedName.toLowerCase()) ||
matchedName.toLowerCase().includes(tag.name.toLowerCase())
)
)
//
const existingTagIds = new Set(selectedTag.value.map((t: Tag) => t.id))
const tagsToAdd = matchedTags.filter((tag: Tag) => !existingTagIds.has(tag.id))
if (tagsToAdd.length === 0) {
if (matchedTags.length > 0) {
message.info(t('aiAnalyzeTagsAllTagsAlreadyAdded'))
} else {
message.info(t('aiAnalyzeTagsNoValidTags'))
}
return
}
// tag
for (const tag of tagsToAdd) {
emit('contextMenuClick', { key: `toggle-tag-${tag.id}` } as any, props.file, props.idx)
}
message.success(t('aiAnalyzeTagsSuccess', [tagsToAdd.length.toString(), tagsToAdd.map(t => t.name).join(', ')]))
} catch (error) {
console.error('AI分析标签失败:', error)
message.error(t('aiAnalyzeTagsFailed'))
} finally {
analyzingTags.value = false
}
}
</script>
<template>
@ -384,6 +478,14 @@ const onTiktokViewClick = () => {
<a-button @click="copyPositivePrompt" v-if="imageGenInfo">{{
$t('copyPositivePrompt')
}}</a-button>
<a-button
@click="analyzeTagsWithAI"
type="primary"
:loading="analyzingTags"
v-if="imageGenInfo && global.conf?.all_custom_tags?.length"
>
{{ $t('aiAnalyzeTags') }}
</a-button>
<a-button
@click="onTiktokViewClick"
@touchstart.prevent="onTiktokViewClick"
@ -490,6 +592,17 @@ const onTiktokViewClick = () => {
</tr>
</table>
</template>
<template v-if="extraJsonMetaInfo && Object.keys(extraJsonMetaInfo).length"> <br />
<h3>Extra Meta Info</h3>
<table class="extra-meta-table">
<tr v-for="(val, key) in extraJsonMetaInfo" :key="key" class="gen-info-frag">
<td style="font-weight: 600;text-transform: capitalize;">{{ key }}</td>
<td style="cursor: pointer;" @dblclick="copy(val)">
<code class="extra-meta-value">{{ typeof val === 'string' ? val : JSON.stringify(val, null, 2) }}</code>
</td>
</tr>
</table>
</template>
</a-tab-pane>
<a-tab-pane key="sourceText" :tab="$t('sourceText')">
<code>{{ imageGenInfo }}</code>
@ -608,6 +721,21 @@ const onTiktokViewClick = () => {
tr td:first-child {
white-space: nowrap;
vertical-align: top;
}
}
table.extra-meta-table {
.extra-meta-value {
display: block;
max-height: 200px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.85em;
background: var(--zp-secondary-variant-background);
padding: 8px;
border-radius: 4px;
}
}

View File

@ -19,7 +19,6 @@ import { throttle, debounce } from 'lodash-es'
import { useLocalStorage } from '@vueuse/core'
import { prefix } from '@/util/const'
const globalStore = useGlobalStore()
const wsStore = useWorkspeaceSnapshot()
@ -71,7 +70,7 @@ const defaultInitinalPageOptions = computed(() => {
const shortCutsCountRec = computed(() => {
const rec = globalStore.shortcut
const res = {} as Dict<number>
Object.entries(rec).forEach(([_k, v]) => {
Object.values(rec).forEach((v) => {
res[v + ''] ??= 0
res[v + '']++
})
@ -96,6 +95,8 @@ const isShortcutConflict = (keyStr: string) => {
return keyStr && keyStr in shortCutsCountRec.value && shortCutsCountRec.value[keyStr] > 1
}
const disableMaximize = useLocalStorage(prefix+'disable_maximize', false)
// & TopicSearch
</script>
<template>
<div class="panel">
@ -172,6 +173,8 @@ const disableMaximize = useLocalStorage(prefix+'disable_maximize', false)
<a-switch v-model:checked="disableMaximize" />
<sub style="padding-left: 8px;color: #666;">{{ $t('takeEffectAfterReloadPage') }}</sub>
</a-form-item>
<h2>{{ t('shortcutKey') }}</h2>
<a-form-item :label="item.label" v-for="item in shortcutsList" :key="item.key">
<div class="col" :class="{ conflict: isShortcutConflict(globalStore.shortcut[item.key] + '') }"

View File

@ -23,7 +23,7 @@ interface TabPaneBase {
}
interface OtherTabPane extends TabPaneBase {
type: 'global-setting' | 'tag-search' | 'batch-download' | 'workspace-snapshot' | 'random-image'
type: 'global-setting' | 'tag-search' | 'batch-download' | 'workspace-snapshot' | 'random-image' | 'topic-search'
}
export interface EmptyStartTabPane extends TabPaneBase {
@ -90,6 +90,13 @@ interface TagSearchMatchedImageGridTabPane extends TabPaneBase {
selectedTagIds: MatchImageByTagsReq
id: string
}
interface TopicSearchMatchedImageGridTabPane extends TabPaneBase {
type: 'topic-search-matched-image-grid'
id: string
title: string
paths: string[]
}
export interface ImgSliTabPane extends TabPaneBase {
type: 'img-sli'
left: FileNodeInfo
@ -113,7 +120,16 @@ export interface FuzzySearchTabPane extends TabPaneBase {
searchScope?: string
}
export type TabPane = EmptyStartTabPane | FileTransferTabPane | OtherTabPane | TagSearchMatchedImageGridTabPane | ImgSliTabPane | TagSearchTabPane | FuzzySearchTabPane| GridViewTabPane
export type TabPane =
| EmptyStartTabPane
| FileTransferTabPane
| OtherTabPane
| TagSearchMatchedImageGridTabPane
| TopicSearchMatchedImageGridTabPane
| ImgSliTabPane
| TagSearchTabPane
| FuzzySearchTabPane
| GridViewTabPane
/**
* This interface represents a tab, which contains an array of panes, an ID, and a key

View File

@ -75,6 +75,19 @@ export function parse(parameters: string): ImageMeta {
const metadata: ImageMeta = {};
if (!parameters) return metadata;
// 提取 extraJsonMetaInfo 字段
const extraJsonMetaInfoMatch = parameters.match(/\nextraJsonMetaInfo:\s*(\{[\s\S]*\})\s*$/);
if (extraJsonMetaInfoMatch) {
try {
metadata.extraJsonMetaInfo = JSON.parse(extraJsonMetaInfoMatch[1]);
// 从原始参数中移除 extraJsonMetaInfo 部分
parameters = parameters.replace(/\nextraJsonMetaInfo:\s*\{[\s\S]*\}\s*$/, '');
} catch (e) {
// 解析失败,保留原始字符串
metadata.extraJsonMetaInfo = extraJsonMetaInfoMatch[1];
}
}
const metaLines = parameters.split('\n').filter((line) => {
return line.trim() !== '' && !stripKeys.some((key) => line.startsWith(key));
});
@ -117,10 +130,14 @@ export function parse(parameters: string): ImageMeta {
});
// Extract prompts
const [prompt, ...negativePrompt] = metaLines
let [prompt, ...negativePrompt] = metaLines
.join('\n')
.split('Negative prompt:')
.map((x) => x.trim());
// 确保 prompt 中不包含 extraJsonMetaInfo
prompt = prompt.replace(/\nextraJsonMetaInfo:\s*\{[\s\S]*\}\s*$/, '').trim();
metadata.prompt = prompt;
metadata.negativePrompt = negativePrompt.join(' ').trim();

View File

@ -40,7 +40,7 @@ export default defineConfig({
}
},
server: {
port: 3000,
port: 3002,
proxy: {
'/infinite_image_browsing/': {
target: 'http://127.0.0.1:7866/'