feat: experimental natural-language categorization & search (persistent scope, clustering, retrieval)
parent
2a1edbab1f
commit
d7e2b2f9fc
48
.env.example
48
.env.example
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -20,3 +20,4 @@ plugins/*
|
|||
!plugins/.gitkeep
|
||||
test_data/*
|
||||
.DS_Store
|
||||
iib_output
|
||||
79
README-zh.md
79
README-zh.md
|
|
@ -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`
|
||||
- 入参:`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` 传 `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 兜底**。只要服务端/模型返回异常或不符合约束,就会直接报错,避免产生“看似能跑但其实不可信”的结果。
|
||||
|
||||
|
||||
|
|
|
|||
79
README.md
79
README.md
|
|
@ -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).
|
||||
It’s 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`
|
||||
- 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`
|
||||
|
||||
#### 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.
|
||||
|
|
|
|||
|
|
@ -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-3d3aa3ce.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-db391c6a.js"></script>
|
||||
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-af514ea9.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ pillow-avif-plugin
|
|||
imageio
|
||||
av>=14,<15
|
||||
lxml
|
||||
filetype
|
||||
filetype
|
||||
requests
|
||||
|
|
@ -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)}")
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from contextlib import closing
|
|||
import os
|
||||
import threading
|
||||
import re
|
||||
import hashlib
|
||||
|
||||
|
||||
class FileInfoDict(TypedDict):
|
||||
|
|
@ -80,6 +81,8 @@ class DataBase:
|
|||
ExtraPath.create_table(conn)
|
||||
DirCoverCache.create_table(conn)
|
||||
GlobalSetting.create_table(conn)
|
||||
ImageEmbedding.create_table(conn)
|
||||
TopicTitleCache.create_table(conn)
|
||||
finally:
|
||||
conn.commit()
|
||||
clz.num += 1
|
||||
|
|
@ -301,6 +304,151 @@ 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 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 Tag:
|
||||
def __init__(self, name: str, score: int, type: str, count=0, color = ""):
|
||||
self.name = name
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,931 @@
|
|||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
from array import array
|
||||
from contextlib import closing
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import requests
|
||||
from fastapi import Depends, FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from scripts.iib.db.datamodel import DataBase, ImageEmbedding, TopicTitleCache
|
||||
from scripts.iib.tool import cwd
|
||||
|
||||
|
||||
def _normalize_base_url(base_url: str) -> str:
|
||||
return base_url[:-1] if base_url.endswith("/") else base_url
|
||||
|
||||
|
||||
_PROMPT_NORMALIZE_ENABLED = os.getenv("IIB_PROMPT_NORMALIZE", "1").strip().lower() not in ["0", "false", "no", "off"]
|
||||
# balanced: keep some discriminative style words (e.g. "科学插图/纪实/胶片") while dropping boilerplate quality/camera terms
|
||||
# theme_only: more aggressive removal, closer to "only subject/theme nouns"
|
||||
_PROMPT_NORMALIZE_MODE = (os.getenv("IIB_PROMPT_NORMALIZE_MODE", "balanced") or "balanced").strip().lower()
|
||||
|
||||
# Remove common SD boilerplate / quality descriptors.
|
||||
_DROP_PATTERNS_COMMON = [
|
||||
# SD / A1111 tags
|
||||
r"<lora:[^>]+>",
|
||||
r"<lyco:[^>]+>",
|
||||
# quality / resolution / generic
|
||||
r"\b(masterpiece|best\s*quality|high\s*quality|best\s*rating|(?:highly|ultra|hyper)[-\s\u2010\u2011\u2012\u2013\u2212]*detailed|absurdres|absurd\s*res|hires|hdr|uhd|8k|4k|2k|raw\s*photo|photorealistic|realistic|cinematic)\b",
|
||||
# photography / camera / lens
|
||||
r"\b(film\s+photography|photography|dslr|camera|canon|nikon|sony|sigma|leica|lens|bokeh|depth\s+of\s+field|dof|sharp\s+focus|wide\s+angle|fisheye)\b",
|
||||
r"\b(iso\s*\d{2,5}|f\/\d+(?:\.\d+)?|\d{2,4}mm)\b",
|
||||
]
|
||||
# chinese quality descriptors (common)
|
||||
_DROP_PATTERNS_ZH_COMMON = [
|
||||
r"(超高分辨率|高分辨率|高清|超清|8K|4K|2K|照片级|高质量|最佳质量|大师作品|杰作|超细节|细节丰富|极致细节|极致|完美)",
|
||||
]
|
||||
# chinese "style/photography" words: keep in balanced mode (discriminative), drop in theme_only mode
|
||||
_DROP_PATTERNS_ZH_STYLE = [
|
||||
r"(电影质感|写真|写实|真实感|摄影|摄影作品|摄影图像|摄影图|镜头|景深|胶片|光圈|光影|构图|色彩|渲染|纪实|插图|科学插图)",
|
||||
]
|
||||
|
||||
def _build_drop_re() -> re.Pattern:
|
||||
pats = list(_DROP_PATTERNS_COMMON) + list(_DROP_PATTERNS_ZH_COMMON)
|
||||
if _PROMPT_NORMALIZE_MODE in ["theme", "theme_only", "strict"]:
|
||||
pats += list(_DROP_PATTERNS_ZH_STYLE)
|
||||
return re.compile("|".join(f"(?:{p})" for p in pats), flags=re.IGNORECASE)
|
||||
|
||||
_DROP_RE = _build_drop_re()
|
||||
|
||||
|
||||
def _compute_prompt_normalize_version() -> str:
|
||||
"""
|
||||
IMPORTANT:
|
||||
- Do NOT allow users to override normalize-version via environment variables.
|
||||
- Version should be deterministic from the normalization rules themselves, so cache invalidation
|
||||
happens automatically when we change rules in code (or switch mode).
|
||||
"""
|
||||
payload = {
|
||||
"enabled": bool(_PROMPT_NORMALIZE_ENABLED),
|
||||
"mode": str(_PROMPT_NORMALIZE_MODE),
|
||||
"drop_common": list(_DROP_PATTERNS_COMMON),
|
||||
"drop_zh_common": list(_DROP_PATTERNS_ZH_COMMON),
|
||||
"drop_zh_style": list(_DROP_PATTERNS_ZH_STYLE),
|
||||
}
|
||||
s = json.dumps(payload, ensure_ascii=False, sort_keys=True)
|
||||
return "nv_" + hashlib.sha1(s.encode("utf-8")).hexdigest()[:12]
|
||||
|
||||
|
||||
# Derived normalize version fingerprint (for embedding/title cache invalidation)
|
||||
_PROMPT_NORMALIZE_VERSION = _compute_prompt_normalize_version()
|
||||
|
||||
|
||||
def _extract_prompt_text(raw_exif: str, max_chars: int = 4000) -> str:
|
||||
"""
|
||||
Extract the natural-language prompt part from stored exif text.
|
||||
Keep text before 'Negative prompt:' to preserve semantics.
|
||||
"""
|
||||
if not isinstance(raw_exif, str):
|
||||
return ""
|
||||
s = raw_exif.strip()
|
||||
if not s:
|
||||
return ""
|
||||
idx = s.lower().find("negative prompt:")
|
||||
if idx != -1:
|
||||
s = s[:idx].strip()
|
||||
if len(s) > max_chars:
|
||||
s = s[:max_chars]
|
||||
return s.strip()
|
||||
|
||||
|
||||
def _clean_prompt_for_semantic(text: str) -> str:
|
||||
"""
|
||||
Light, dependency-free prompt normalization:
|
||||
- remove lora tags / SD boilerplate / quality & photography descriptors
|
||||
- keep remaining text as 'theme' semantic signal for embeddings/clustering
|
||||
"""
|
||||
if not isinstance(text, str):
|
||||
return ""
|
||||
s = text
|
||||
# remove negative prompt tail early (safety if caller passes raw exif)
|
||||
s = re.sub(r"(negative prompt:).*", " ", s, flags=re.IGNORECASE | re.DOTALL)
|
||||
# remove weights like (foo:1.2)
|
||||
s = re.sub(r"\(([^()]{1,80}):\s*\d+(?:\.\d+)?\)", r"\1", s)
|
||||
# drop boilerplate patterns
|
||||
s = _DROP_RE.sub(" ", s)
|
||||
# normalize separators
|
||||
s = s.replace("**", " ")
|
||||
s = re.sub(r"[\[\]{}()]", " ", s)
|
||||
s = re.sub(r"\s+", " ", s).strip()
|
||||
# If it's a comma-tag style prompt, remove empty / tiny segments.
|
||||
parts = re.split(r"[,\n,;;]+", s)
|
||||
kept: List[str] = []
|
||||
for p in parts:
|
||||
t = p.strip()
|
||||
if not t:
|
||||
continue
|
||||
# drop segments that are basically leftover boilerplate (too short or all punctuation)
|
||||
if len(t) <= 2:
|
||||
continue
|
||||
kept.append(t)
|
||||
s2 = ",".join(kept) if kept else s
|
||||
return s2.strip()
|
||||
|
||||
|
||||
def _clean_for_title(text: str) -> str:
|
||||
if not isinstance(text, str):
|
||||
return ""
|
||||
s = text
|
||||
s = s.replace("**", " ")
|
||||
s = re.sub(r"<lora:[^>]+>", " ", s, flags=re.IGNORECASE)
|
||||
s = re.sub(r"<lyco:[^>]+>", " ", s, flags=re.IGNORECASE)
|
||||
s = re.sub(r"(negative prompt:).*", " ", s, flags=re.IGNORECASE | re.DOTALL)
|
||||
s = re.sub(r"(prompt:|提示词[::]|提示[::]|输出[::])", " ", s, flags=re.IGNORECASE)
|
||||
s = re.sub(r"\s+", " ", s).strip()
|
||||
return s
|
||||
|
||||
|
||||
def _title_from_representative_prompt(text: str, max_len: int = 18) -> str:
|
||||
"""
|
||||
Local fallback title: take the first sentence/clause and truncate.
|
||||
This is much more readable than token n-grams without Chinese word segmentation.
|
||||
"""
|
||||
# Use the same semantic cleaner as embeddings to avoid boilerplate titles like
|
||||
# "masterpiece, best quality" / "A highly detailed ..."
|
||||
base = _clean_for_title(text)
|
||||
s = _clean_prompt_for_semantic(base) if _PROMPT_NORMALIZE_ENABLED else base
|
||||
if not s:
|
||||
s = base
|
||||
if not s:
|
||||
return "主题"
|
||||
# Split by common sentence punctuations, keep the first segment.
|
||||
seg = re.split(r"[。!?!?\n\r;;]+", s)[0].strip()
|
||||
# Remove leading punctuation / separators
|
||||
seg = re.sub(r"^[,,;;::\s-]+", "", seg).strip()
|
||||
# Remove leading english articles for nicer titles
|
||||
seg = re.sub(r"^(a|an|the)\s+", "", seg, flags=re.IGNORECASE).strip()
|
||||
# Strip common boilerplate templates in titles while keeping discriminative words.
|
||||
# English: "highly detailed scientific rendering/illustration of ..."
|
||||
seg = re.sub(
|
||||
r"^(?:(?:highly|ultra|hyper)[-\s\u2010\u2011\u2012\u2013\u2212]*detailed\s+)?(?:scientific\s+)?(?:rendering|illustration|image|depiction|scene)\s+of\s+",
|
||||
"",
|
||||
seg,
|
||||
flags=re.IGNORECASE,
|
||||
).strip()
|
||||
# Chinese: "一张...图像/插图/照片..." template
|
||||
seg = re.sub(r"^一[张幅]\s*[^,,。]{0,20}(?:图像|插图|照片|摄影图像|摄影作品)\s*[,, ]*", "", seg).strip()
|
||||
# Remove trailing commas/colons
|
||||
seg = re.sub(r"[,:,:]\s*$", "", seg).strip()
|
||||
# If still too long, hard truncate.
|
||||
if len(seg) > max_len:
|
||||
seg = seg[:max_len].rstrip()
|
||||
return seg or "主题"
|
||||
|
||||
|
||||
def _vec_to_blob_f32(vec: List[float]) -> bytes:
|
||||
arr = array("f", vec)
|
||||
return arr.tobytes()
|
||||
|
||||
|
||||
def _blob_to_vec_f32(blob: bytes) -> array:
|
||||
arr = array("f")
|
||||
arr.frombytes(blob)
|
||||
return arr
|
||||
|
||||
|
||||
def _l2_norm_sq(vec: array) -> float:
|
||||
return sum((x * x for x in vec))
|
||||
|
||||
|
||||
def _dot(a: array, b: array) -> float:
|
||||
return sum((x * y for x, y in zip(a, b)))
|
||||
|
||||
|
||||
def _cos_sum(a_sum: array, a_norm_sq: float, b_sum: array, b_norm_sq: float) -> float:
|
||||
if a_norm_sq <= 0 or b_norm_sq <= 0:
|
||||
return 0.0
|
||||
dotv = sum((x * y for x, y in zip(a_sum, b_sum)))
|
||||
return dotv / (math.sqrt(a_norm_sq) * math.sqrt(b_norm_sq))
|
||||
|
||||
|
||||
def _call_embeddings(
|
||||
*,
|
||||
inputs: List[str],
|
||||
model: str,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
) -> List[List[float]]:
|
||||
if not api_key:
|
||||
raise HTTPException(status_code=500, detail="OpenAI API Key not configured")
|
||||
|
||||
url = f"{_normalize_base_url(base_url)}/embeddings"
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
payload = {"model": model, "input": inputs}
|
||||
try:
|
||||
resp = requests.post(url, json=payload, headers=headers, timeout=120)
|
||||
except requests.RequestException as e:
|
||||
raise HTTPException(status_code=502, detail=f"Embedding API request failed: {e}")
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(status_code=resp.status_code, detail=resp.text)
|
||||
data = resp.json()
|
||||
items = data.get("data") or []
|
||||
items.sort(key=lambda x: x.get("index", 0))
|
||||
embeddings = [x.get("embedding") for x in items]
|
||||
if any((not isinstance(v, list) for v in embeddings)):
|
||||
raise HTTPException(status_code=500, detail="Invalid embeddings response format")
|
||||
return embeddings
|
||||
|
||||
|
||||
def _call_chat_title(
|
||||
*,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
model: str,
|
||||
prompt_samples: List[str],
|
||||
output_lang: str,
|
||||
) -> Optional[Dict]:
|
||||
"""
|
||||
Ask LLM to generate a short topic title and a few keywords. Returns dict or None.
|
||||
"""
|
||||
if not api_key:
|
||||
raise HTTPException(status_code=500, detail="OpenAI API Key not configured")
|
||||
url = f"{_normalize_base_url(base_url)}/chat/completions"
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
|
||||
samples = [(_clean_prompt_for_semantic(_clean_for_title(s) or s) or s).strip() for s in prompt_samples if (s or "").strip()]
|
||||
samples = [s[:400] for s in samples][:6]
|
||||
if not samples:
|
||||
raise HTTPException(status_code=400, detail="No prompt samples for title generation")
|
||||
|
||||
json_example = '{"title":"...","keywords":["...","..."]}'
|
||||
sys = (
|
||||
"You are a topic naming assistant for image-generation prompts.\n"
|
||||
"Given several prompt snippets that belong to the SAME theme, output:\n"
|
||||
"- a short topic title\n"
|
||||
"- 3–6 keywords.\n"
|
||||
"\n"
|
||||
"Rules:\n"
|
||||
f"- Output language MUST be: {output_lang}\n"
|
||||
"- Prefer 4–12 characters for Chinese (Simplified/Traditional), otherwise 2–6 English/German words.\n"
|
||||
"- Avoid generic boilerplate like: masterpiece, best quality, highly detailed, cinematic, etc.\n"
|
||||
"- Keep distinctive terms if they help differentiate themes (e.g., Warhammer 40K, Lolita, scientific illustration).\n"
|
||||
"- Do NOT output explanations. Do NOT output markdown/code fences.\n"
|
||||
"- The output MUST start with '{' and end with '}' (no leading/trailing characters).\n"
|
||||
"\n"
|
||||
"Output STRICT JSON only:\n"
|
||||
+ json_example
|
||||
)
|
||||
user = "Prompt snippets:\n" + "\n".join([f"- {s}" for s in samples])
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [{"role": "system", "content": sys}, {"role": "user", "content": user}],
|
||||
# Prefer deterministic, JSON-only output
|
||||
"temperature": 0.0,
|
||||
"top_p": 1.0,
|
||||
# Give enough room for JSON across providers.
|
||||
"max_tokens": 2048,
|
||||
# Prefer tool/function call to force structured output across providers (e.g. Gemini).
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "set_topic",
|
||||
"description": "Return a concise topic title and 3-6 keywords.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"keywords": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
"required": ["title", "keywords"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
"tool_choice": {"type": "function", "function": {"name": "set_topic"}},
|
||||
}
|
||||
# Some OpenAI-compatible providers may use different token limit fields / casing.
|
||||
# Set them all (still a single request; no retry/fallback).
|
||||
payload["max_output_tokens"] = payload["max_tokens"]
|
||||
payload["max_completion_tokens"] = payload["max_tokens"]
|
||||
payload["maxOutputTokens"] = payload["max_tokens"]
|
||||
payload["maxCompletionTokens"] = payload["max_tokens"]
|
||||
|
||||
def _post_and_parse(payload_obj: Dict) -> Dict:
|
||||
try:
|
||||
resp = requests.post(url, json=payload_obj, headers=headers, timeout=60)
|
||||
except requests.RequestException as e:
|
||||
raise HTTPException(status_code=502, detail=f"Chat API request failed: {e}")
|
||||
if resp.status_code != 200:
|
||||
# keep response body for debugging (truncated)
|
||||
body = (resp.text or "")[:600]
|
||||
raise HTTPException(status_code=resp.status_code, detail=body)
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
txt = (resp.text or "")[:600]
|
||||
raise HTTPException(status_code=502, detail=f"Chat API response is not JSON: {e}; body={txt}")
|
||||
choice0 = (data.get("choices") or [{}])[0] if isinstance(data.get("choices"), list) else {}
|
||||
msg = (choice0 or {}).get("message") or {}
|
||||
|
||||
# OpenAI-compatible providers may return JSON in different places:
|
||||
# - message.tool_calls[].function.arguments (JSON string) <-- preferred when tools are used
|
||||
# - message.function_call.arguments (legacy)
|
||||
# - message.content (common)
|
||||
# - choice.text (legacy completions)
|
||||
raw = ""
|
||||
if not raw and isinstance(msg, dict):
|
||||
tcs = msg.get("tool_calls") or []
|
||||
if isinstance(tcs, list) and tcs:
|
||||
fn = ((tcs[0] or {}).get("function") or {}) if isinstance(tcs[0], dict) else {}
|
||||
args = (fn.get("arguments") or "") if isinstance(fn, dict) else ""
|
||||
if isinstance(args, str) and args.strip():
|
||||
raw = args.strip()
|
||||
if not raw and isinstance(msg, dict):
|
||||
fc = msg.get("function_call") or {}
|
||||
args = (fc.get("arguments") or "") if isinstance(fc, dict) else ""
|
||||
if isinstance(args, str) and args.strip():
|
||||
raw = args.strip()
|
||||
if not raw:
|
||||
content = (msg.get("content") or "") if isinstance(msg, dict) else ""
|
||||
if isinstance(content, str) and content.strip():
|
||||
raw = content.strip()
|
||||
if not raw:
|
||||
txt = (choice0.get("text") or "") if isinstance(choice0, dict) else ""
|
||||
if isinstance(txt, str) and txt.strip():
|
||||
raw = txt.strip()
|
||||
|
||||
m = re.search(r"\{[\s\S]*\}", raw)
|
||||
if not m:
|
||||
snippet = (raw or "")[:400].replace("\n", "\\n")
|
||||
choice_dump = json.dumps(choice0, ensure_ascii=False)[:600] if isinstance(choice0, dict) else str(choice0)[:600]
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=f"Chat API response has no JSON object; content_snippet={snippet}; choice0={choice_dump}",
|
||||
)
|
||||
try:
|
||||
obj = json.loads(m.group(0))
|
||||
except Exception as e:
|
||||
snippet = (m.group(0) or "")[:400].replace("\n", "\\n")
|
||||
raise HTTPException(status_code=502, detail=f"Chat API JSON parse failed: {e}; json_snippet={snippet}")
|
||||
if not isinstance(obj, dict):
|
||||
raise HTTPException(status_code=502, detail="Chat API response JSON is not an object")
|
||||
title = str(obj.get("title") or "").strip()
|
||||
keywords = obj.get("keywords") or []
|
||||
if not title:
|
||||
raise HTTPException(status_code=502, detail="Chat API response missing title")
|
||||
if not isinstance(keywords, list):
|
||||
keywords = []
|
||||
keywords = [str(x).strip() for x in keywords if str(x).strip()][:6]
|
||||
return {"title": title[:24], "keywords": keywords}
|
||||
|
||||
# No fallback / no retry: fail fast if provider/model doesn't support response_format or returns invalid output.
|
||||
return _post_and_parse(payload)
|
||||
|
||||
|
||||
def mount_topic_cluster_routes(
|
||||
app: FastAPI,
|
||||
db_api_base: str,
|
||||
verify_secret,
|
||||
write_permission_required,
|
||||
*,
|
||||
openai_base_url: str,
|
||||
openai_api_key: str,
|
||||
embedding_model: str,
|
||||
ai_model: str,
|
||||
):
|
||||
"""
|
||||
Mount embedding + topic clustering endpoints (MVP: manual, iib_output only).
|
||||
"""
|
||||
|
||||
class BuildIibOutputEmbeddingReq(BaseModel):
|
||||
folder: Optional[str] = None # default: {cwd}/iib_output
|
||||
model: Optional[str] = None
|
||||
force: Optional[bool] = False
|
||||
batch_size: Optional[int] = 64
|
||||
max_chars: Optional[int] = 4000
|
||||
|
||||
@app.post(
|
||||
f"{db_api_base}/build_iib_output_embeddings",
|
||||
dependencies=[Depends(verify_secret), Depends(write_permission_required)],
|
||||
)
|
||||
async def build_iib_output_embeddings(req: BuildIibOutputEmbeddingReq):
|
||||
if not openai_api_key:
|
||||
raise HTTPException(status_code=500, detail="OpenAI API Key not configured")
|
||||
if not openai_base_url:
|
||||
raise HTTPException(status_code=500, detail="OpenAI Base URL not configured")
|
||||
folder = req.folder or os.path.join(cwd, "iib_output")
|
||||
folder = os.path.normpath(folder)
|
||||
model = req.model or embedding_model
|
||||
batch_size = max(1, min(int(req.batch_size or 64), 256))
|
||||
max_chars = max(256, min(int(req.max_chars or 4000), 8000))
|
||||
force = bool(req.force)
|
||||
|
||||
if not os.path.exists(folder) or not os.path.isdir(folder):
|
||||
raise HTTPException(status_code=400, detail=f"Folder not found: {folder}")
|
||||
|
||||
conn = DataBase.get_conn()
|
||||
like_prefix = os.path.join(folder, "%")
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute("SELECT id, path, exif FROM image WHERE path LIKE ?", (like_prefix,))
|
||||
rows = cur.fetchall()
|
||||
|
||||
images = []
|
||||
for image_id, path, exif in rows:
|
||||
if not isinstance(path, str) or not os.path.exists(path):
|
||||
continue
|
||||
text_raw = _extract_prompt_text(exif, max_chars=max_chars)
|
||||
if _PROMPT_NORMALIZE_ENABLED:
|
||||
text = _clean_prompt_for_semantic(text_raw)
|
||||
if not text:
|
||||
text = text_raw
|
||||
else:
|
||||
text = text_raw
|
||||
if not text:
|
||||
continue
|
||||
images.append({"id": int(image_id), "path": path, "text": text})
|
||||
|
||||
if not images:
|
||||
return {"folder": folder, "count": 0, "updated": 0, "skipped": 0, "model": model}
|
||||
|
||||
id_list = [x["id"] for x in images]
|
||||
existing = ImageEmbedding.get_by_image_ids(conn, id_list)
|
||||
|
||||
to_embed = []
|
||||
skipped = 0
|
||||
for item in images:
|
||||
# include normalize version to force refresh when rules change
|
||||
text_hash = ImageEmbedding.compute_text_hash(f"{_PROMPT_NORMALIZE_VERSION}:{item['text']}")
|
||||
old = existing.get(item["id"])
|
||||
if (
|
||||
(not force)
|
||||
and old
|
||||
and old.get("model") == model
|
||||
and old.get("text_hash") == text_hash
|
||||
and old.get("vec")
|
||||
):
|
||||
skipped += 1
|
||||
continue
|
||||
to_embed.append({**item, "text_hash": text_hash})
|
||||
|
||||
updated = 0
|
||||
for i in range(0, len(to_embed), batch_size):
|
||||
batch = to_embed[i : i + batch_size]
|
||||
inputs = [x["text"] for x in batch]
|
||||
vectors = _call_embeddings(
|
||||
inputs=inputs,
|
||||
model=model,
|
||||
base_url=openai_base_url,
|
||||
api_key=openai_api_key,
|
||||
)
|
||||
if len(vectors) != len(batch):
|
||||
raise HTTPException(status_code=500, detail="Embeddings count mismatch")
|
||||
for item, vec in zip(batch, vectors):
|
||||
ImageEmbedding.upsert(
|
||||
conn=conn,
|
||||
image_id=item["id"],
|
||||
model=model,
|
||||
dim=len(vec),
|
||||
text_hash=item["text_hash"],
|
||||
vec_blob=_vec_to_blob_f32(vec),
|
||||
)
|
||||
updated += 1
|
||||
conn.commit()
|
||||
|
||||
return {"folder": folder, "count": len(images), "updated": updated, "skipped": skipped, "model": model}
|
||||
|
||||
class ClusterIibOutputReq(BaseModel):
|
||||
folder: Optional[str] = None
|
||||
folder_paths: Optional[List[str]] = None
|
||||
model: Optional[str] = None
|
||||
force_embed: Optional[bool] = False
|
||||
threshold: Optional[float] = 0.86
|
||||
batch_size: Optional[int] = 64
|
||||
max_chars: Optional[int] = 4000
|
||||
min_cluster_size: Optional[int] = 2
|
||||
# B: LLM title generation
|
||||
title_model: Optional[str] = None
|
||||
# Reduce noise by reassigning small-cluster members to best large cluster if similarity is high enough
|
||||
assign_noise_threshold: Optional[float] = None
|
||||
# Cache titles in sqlite to avoid repeated LLM calls
|
||||
use_title_cache: Optional[bool] = True
|
||||
force_title: Optional[bool] = False
|
||||
# Output language for titles/keywords (from frontend globalStore.lang)
|
||||
lang: Optional[str] = None
|
||||
|
||||
class PromptSearchReq(BaseModel):
|
||||
query: str
|
||||
folder: Optional[str] = None
|
||||
folder_paths: Optional[List[str]] = None
|
||||
model: Optional[str] = None
|
||||
top_k: Optional[int] = 50
|
||||
min_score: Optional[float] = 0.0
|
||||
# Ensure embeddings exist/updated before searching
|
||||
ensure_embed: Optional[bool] = True
|
||||
# Use the same normalization as clustering
|
||||
max_chars: Optional[int] = 4000
|
||||
|
||||
@app.post(
|
||||
f"{db_api_base}/search_iib_output_by_prompt",
|
||||
dependencies=[Depends(verify_secret), Depends(write_permission_required)],
|
||||
)
|
||||
async def search_iib_output_by_prompt(req: PromptSearchReq):
|
||||
if not openai_api_key:
|
||||
raise HTTPException(status_code=500, detail="OpenAI API Key not configured")
|
||||
if not openai_base_url:
|
||||
raise HTTPException(status_code=500, detail="OpenAI Base URL not configured")
|
||||
|
||||
q = (req.query or "").strip()
|
||||
if not q:
|
||||
raise HTTPException(status_code=400, detail="query is required")
|
||||
|
||||
folders: List[str] = []
|
||||
if req.folder_paths:
|
||||
for p in req.folder_paths:
|
||||
if isinstance(p, str) and p.strip():
|
||||
folders.append(os.path.normpath(p.strip()))
|
||||
if req.folder and isinstance(req.folder, str) and req.folder.strip():
|
||||
folders.append(os.path.normpath(req.folder.strip()))
|
||||
# 用户不会用默认 iib_output:未指定范围则直接报错
|
||||
if not folders:
|
||||
raise HTTPException(status_code=400, detail="folder_paths is required (select folders to search)")
|
||||
|
||||
# validate folders
|
||||
folders = list(dict.fromkeys(folders)) # de-dup keep order
|
||||
for f in folders:
|
||||
if not os.path.exists(f) or not os.path.isdir(f):
|
||||
raise HTTPException(status_code=400, detail=f"Folder not found: {f}")
|
||||
|
||||
folder = folders[0]
|
||||
model = req.model or embedding_model
|
||||
top_k = max(1, min(int(req.top_k or 50), 500))
|
||||
min_score = float(req.min_score or 0.0)
|
||||
min_score = max(-1.0, min(min_score, 1.0))
|
||||
max_chars = max(256, min(int(req.max_chars or 4000), 8000))
|
||||
|
||||
if bool(req.ensure_embed):
|
||||
for f in folders:
|
||||
await build_iib_output_embeddings(
|
||||
BuildIibOutputEmbeddingReq(folder=f, model=model, force=False, batch_size=64, max_chars=max_chars)
|
||||
)
|
||||
|
||||
# Build query embedding
|
||||
q_text = _extract_prompt_text(q, max_chars=max_chars)
|
||||
if _PROMPT_NORMALIZE_ENABLED:
|
||||
q_text2 = _clean_prompt_for_semantic(q_text)
|
||||
if q_text2:
|
||||
q_text = q_text2
|
||||
vecs = _call_embeddings(inputs=[q_text], model=model, base_url=openai_base_url, api_key=openai_api_key)
|
||||
if not vecs or not isinstance(vecs[0], list) or not vecs[0]:
|
||||
raise HTTPException(status_code=502, detail="Embedding API returned empty vector")
|
||||
qv = array("f", [float(x) for x in vecs[0]])
|
||||
qn2 = _l2_norm_sq(qv)
|
||||
if qn2 <= 0:
|
||||
raise HTTPException(status_code=502, detail="Query embedding has zero norm")
|
||||
qinv = 1.0 / math.sqrt(qn2)
|
||||
for i in range(len(qv)):
|
||||
qv[i] *= qinv
|
||||
|
||||
conn = DataBase.get_conn()
|
||||
like_prefixes = [os.path.join(f, "%") for f in folders]
|
||||
with closing(conn.cursor()) as cur:
|
||||
where = " OR ".join(["image.path LIKE ?"] * len(like_prefixes))
|
||||
cur.execute(
|
||||
f"""SELECT image.id, image.path, image.exif, image_embedding.vec
|
||||
FROM image
|
||||
INNER JOIN image_embedding ON image_embedding.image_id = image.id
|
||||
WHERE ({where}) AND image_embedding.model = ?""",
|
||||
(*like_prefixes, model),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
# TopK by cosine similarity (brute force; MVP only)
|
||||
import heapq
|
||||
|
||||
heap: List[Tuple[float, Dict]] = []
|
||||
total = 0
|
||||
for image_id, path, exif, vec_blob in rows:
|
||||
if not isinstance(path, str) or not os.path.exists(path):
|
||||
continue
|
||||
if not vec_blob:
|
||||
continue
|
||||
v = _blob_to_vec_f32(vec_blob)
|
||||
n2 = _l2_norm_sq(v)
|
||||
if n2 <= 0:
|
||||
continue
|
||||
inv = 1.0 / math.sqrt(n2)
|
||||
for i in range(len(v)):
|
||||
v[i] *= inv
|
||||
score = _dot(qv, v)
|
||||
total += 1
|
||||
if score < min_score:
|
||||
continue
|
||||
item = {
|
||||
"id": int(image_id),
|
||||
"path": path,
|
||||
"score": float(score),
|
||||
"sample_prompt": _clean_for_title(_extract_prompt_text(exif, max_chars=max_chars))[:200],
|
||||
}
|
||||
if len(heap) < top_k:
|
||||
heapq.heappush(heap, (score, item))
|
||||
else:
|
||||
if score > heap[0][0]:
|
||||
heapq.heapreplace(heap, (score, item))
|
||||
|
||||
heap.sort(key=lambda x: x[0], reverse=True)
|
||||
results = [x[1] for x in heap]
|
||||
return {
|
||||
"query": q,
|
||||
"folder": folder,
|
||||
"folders": folders,
|
||||
"model": model,
|
||||
"count": total,
|
||||
"top_k": top_k,
|
||||
"results": results,
|
||||
}
|
||||
|
||||
def _cluster_sig(
|
||||
*,
|
||||
member_ids: List[int],
|
||||
model: str,
|
||||
threshold: float,
|
||||
min_cluster_size: int,
|
||||
title_model: str,
|
||||
lang: str,
|
||||
) -> str:
|
||||
h = hashlib.sha1()
|
||||
h.update(f"m={model}|t={threshold:.6f}|min={min_cluster_size}|tm={title_model}|lang={lang}|nv={_PROMPT_NORMALIZE_VERSION}|nm={_PROMPT_NORMALIZE_MODE}".encode("utf-8"))
|
||||
for iid in sorted(member_ids):
|
||||
h.update(b"|")
|
||||
h.update(str(int(iid)).encode("utf-8"))
|
||||
return h.hexdigest()
|
||||
|
||||
def _normalize_output_lang(lang: Optional[str]) -> str:
|
||||
"""
|
||||
Map frontend language keys to a human-readable instruction for LLM output language.
|
||||
Frontend uses: en / zhHans / zhHant / de
|
||||
"""
|
||||
if not lang:
|
||||
return "English"
|
||||
l = str(lang).strip()
|
||||
ll = l.lower()
|
||||
if ll in ["zh", "zhhans", "zh-hans", "zh_cn", "zh-cn", "cn", "zh-hans-cns", "zhs"]:
|
||||
return "Chinese (Simplified)"
|
||||
if ll in ["zhhant", "zh-hant", "zh_tw", "zh-tw", "zh_hk", "zh-hk", "tw", "hk", "zht"]:
|
||||
return "Chinese (Traditional)"
|
||||
if ll.startswith("de"):
|
||||
return "German"
|
||||
if ll.startswith("en"):
|
||||
return "English"
|
||||
# fallback
|
||||
return "English"
|
||||
|
||||
@app.post(
|
||||
f"{db_api_base}/cluster_iib_output",
|
||||
dependencies=[Depends(verify_secret), Depends(write_permission_required)],
|
||||
)
|
||||
async def cluster_iib_output(req: ClusterIibOutputReq):
|
||||
if not openai_api_key:
|
||||
raise HTTPException(status_code=500, detail="OpenAI API Key not configured")
|
||||
if not openai_base_url:
|
||||
raise HTTPException(status_code=500, detail="OpenAI Base URL not configured")
|
||||
folders: List[str] = []
|
||||
if req.folder_paths:
|
||||
for p in req.folder_paths:
|
||||
if isinstance(p, str) and p.strip():
|
||||
folders.append(os.path.normpath(p.strip()))
|
||||
if req.folder and isinstance(req.folder, str) and req.folder.strip():
|
||||
folders.append(os.path.normpath(req.folder.strip()))
|
||||
# 用户不会用默认 iib_output:未指定范围则直接报错
|
||||
if not folders:
|
||||
raise HTTPException(status_code=400, detail="folder_paths is required (select folders to cluster)")
|
||||
|
||||
folders = list(dict.fromkeys(folders))
|
||||
for f in folders:
|
||||
if not os.path.exists(f) or not os.path.isdir(f):
|
||||
raise HTTPException(status_code=400, detail=f"Folder not found: {f}")
|
||||
|
||||
# Ensure embeddings exist (incremental per folder)
|
||||
for f in folders:
|
||||
await build_iib_output_embeddings(
|
||||
BuildIibOutputEmbeddingReq(
|
||||
folder=f,
|
||||
model=req.model,
|
||||
force=req.force_embed,
|
||||
batch_size=req.batch_size,
|
||||
max_chars=req.max_chars,
|
||||
)
|
||||
)
|
||||
|
||||
folder = folders[0]
|
||||
model = req.model or embedding_model
|
||||
threshold = float(req.threshold or 0.86)
|
||||
threshold = max(0.0, min(threshold, 0.999))
|
||||
min_cluster_size = max(1, int(req.min_cluster_size or 2))
|
||||
title_model = req.title_model or os.getenv("TOPIC_TITLE_MODEL") or ai_model
|
||||
output_lang = _normalize_output_lang(req.lang)
|
||||
assign_noise_threshold = req.assign_noise_threshold
|
||||
if assign_noise_threshold is None:
|
||||
# conservative: only reassign if very likely belongs to a large topic
|
||||
assign_noise_threshold = max(0.72, min(threshold - 0.035, 0.93))
|
||||
else:
|
||||
assign_noise_threshold = max(0.0, min(float(assign_noise_threshold), 0.999))
|
||||
use_title_cache = bool(True if req.use_title_cache is None else req.use_title_cache)
|
||||
force_title = bool(req.force_title)
|
||||
|
||||
conn = DataBase.get_conn()
|
||||
like_prefixes = [os.path.join(f, "%") for f in folders]
|
||||
with closing(conn.cursor()) as cur:
|
||||
where = " OR ".join(["image.path LIKE ?"] * len(like_prefixes))
|
||||
cur.execute(
|
||||
f"""SELECT image.id, image.path, image.exif, image_embedding.vec
|
||||
FROM image
|
||||
INNER JOIN image_embedding ON image_embedding.image_id = image.id
|
||||
WHERE ({where}) AND image_embedding.model = ?""",
|
||||
(*like_prefixes, model),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
items = []
|
||||
for image_id, path, exif, vec_blob in rows:
|
||||
if not isinstance(path, str) or not os.path.exists(path):
|
||||
continue
|
||||
if not vec_blob:
|
||||
continue
|
||||
vec = _blob_to_vec_f32(vec_blob)
|
||||
n2 = _l2_norm_sq(vec)
|
||||
if n2 <= 0:
|
||||
continue
|
||||
inv = 1.0 / math.sqrt(n2)
|
||||
for i in range(len(vec)):
|
||||
vec[i] *= inv
|
||||
text_raw = _extract_prompt_text(exif, max_chars=int(req.max_chars or 4000))
|
||||
if _PROMPT_NORMALIZE_ENABLED:
|
||||
text = _clean_prompt_for_semantic(text_raw)
|
||||
if not text:
|
||||
text = text_raw
|
||||
else:
|
||||
text = text_raw
|
||||
items.append({"id": int(image_id), "path": path, "text": text, "vec": vec})
|
||||
|
||||
if not items:
|
||||
return {"folder": folder, "folders": folders, "model": model, "threshold": threshold, "clusters": [], "noise": []}
|
||||
|
||||
# Incremental clustering by centroid-direction (sum vector)
|
||||
clusters = [] # {sum, norm_sq, members:[idx], sample_text}
|
||||
for idx, it in enumerate(items):
|
||||
v = it["vec"]
|
||||
best_ci = -1
|
||||
best_sim = -1.0
|
||||
best_dot = 0.0
|
||||
for ci, c in enumerate(clusters):
|
||||
dotv = _dot(v, c["sum"])
|
||||
denom = math.sqrt(c["norm_sq"]) if c["norm_sq"] > 0 else 1.0
|
||||
sim = dotv / denom
|
||||
if sim > best_sim:
|
||||
best_sim = sim
|
||||
best_ci = ci
|
||||
best_dot = dotv
|
||||
if best_ci != -1 and best_sim >= threshold:
|
||||
c = clusters[best_ci]
|
||||
for i in range(len(v)):
|
||||
c["sum"][i] += v[i]
|
||||
c["norm_sq"] = c["norm_sq"] + 2.0 * best_dot + 1.0
|
||||
c["members"].append(idx)
|
||||
else:
|
||||
clusters.append({"sum": array("f", v), "norm_sq": 1.0, "members": [idx], "sample_text": it.get("text") or ""})
|
||||
|
||||
# Merge highly similar clusters (fix: same theme split into multiple clusters)
|
||||
merge_threshold = min(0.995, max(threshold + 0.04, 0.90))
|
||||
merged = True
|
||||
while merged and len(clusters) > 1:
|
||||
merged = False
|
||||
best_i = best_j = -1
|
||||
best_sim = merge_threshold
|
||||
for i in range(len(clusters)):
|
||||
ci = clusters[i]
|
||||
for j in range(i + 1, len(clusters)):
|
||||
cj = clusters[j]
|
||||
sim = _cos_sum(ci["sum"], ci["norm_sq"], cj["sum"], cj["norm_sq"])
|
||||
if sim >= best_sim:
|
||||
best_sim = sim
|
||||
best_i, best_j = i, j
|
||||
if best_i != -1:
|
||||
a = clusters[best_i]
|
||||
b = clusters[best_j]
|
||||
for k in range(len(a["sum"])):
|
||||
a["sum"][k] += b["sum"][k]
|
||||
a["norm_sq"] = _l2_norm_sq(a["sum"])
|
||||
a["members"].extend(b["members"])
|
||||
if not a.get("sample_text"):
|
||||
a["sample_text"] = b.get("sample_text", "")
|
||||
clusters.pop(best_j)
|
||||
merged = True
|
||||
|
||||
# Reassign members from small clusters into best large cluster to reduce noise
|
||||
if min_cluster_size > 1 and assign_noise_threshold > 0 and clusters:
|
||||
large = [c for c in clusters if len(c["members"]) >= min_cluster_size]
|
||||
if large:
|
||||
new_large = []
|
||||
# copy large clusters first
|
||||
for c in clusters:
|
||||
if len(c["members"]) >= min_cluster_size:
|
||||
new_large.append(c)
|
||||
# reassign items from small clusters
|
||||
for c in clusters:
|
||||
if len(c["members"]) >= min_cluster_size:
|
||||
continue
|
||||
for mi in c["members"]:
|
||||
v = items[mi]["vec"]
|
||||
best_ci = -1
|
||||
best_sim = -1.0
|
||||
best_dot = 0.0
|
||||
for ci, bigc in enumerate(new_large):
|
||||
dotv = _dot(v, bigc["sum"])
|
||||
denom = math.sqrt(bigc["norm_sq"]) if bigc["norm_sq"] > 0 else 1.0
|
||||
sim = dotv / denom
|
||||
if sim > best_sim:
|
||||
best_sim = sim
|
||||
best_ci = ci
|
||||
best_dot = dotv
|
||||
if best_ci != -1 and best_sim >= assign_noise_threshold:
|
||||
bigc = new_large[best_ci]
|
||||
for k in range(len(v)):
|
||||
bigc["sum"][k] += v[k]
|
||||
bigc["norm_sq"] = bigc["norm_sq"] + 2.0 * best_dot + 1.0
|
||||
bigc["members"].append(mi)
|
||||
# else: keep in small cluster -> will become noise below
|
||||
clusters = new_large
|
||||
|
||||
# Split small clusters to noise, generate titles
|
||||
out_clusters = []
|
||||
noise = []
|
||||
for cidx, c in enumerate(clusters):
|
||||
if len(c["members"]) < min_cluster_size:
|
||||
for mi in c["members"]:
|
||||
noise.append(items[mi]["path"])
|
||||
continue
|
||||
|
||||
member_items = [items[mi] for mi in c["members"]]
|
||||
paths = [x["path"] for x in member_items]
|
||||
texts = [x.get("text") or "" for x in member_items]
|
||||
member_ids = [x["id"] for x in member_items]
|
||||
|
||||
# Representative prompt for LLM title generation
|
||||
rep = (c.get("sample_text") or (texts[0] if texts else "")).strip()
|
||||
|
||||
cached = None
|
||||
cluster_hash = _cluster_sig(
|
||||
member_ids=member_ids,
|
||||
model=model,
|
||||
threshold=threshold,
|
||||
min_cluster_size=min_cluster_size,
|
||||
title_model=title_model,
|
||||
lang=output_lang,
|
||||
)
|
||||
if use_title_cache and (not force_title):
|
||||
cached = TopicTitleCache.get(conn, cluster_hash)
|
||||
if cached and isinstance(cached, dict) and cached.get("title"):
|
||||
title = str(cached.get("title"))
|
||||
keywords = cached.get("keywords") or []
|
||||
else:
|
||||
llm = _call_chat_title(
|
||||
base_url=openai_base_url,
|
||||
api_key=openai_api_key,
|
||||
model=title_model,
|
||||
prompt_samples=[rep] + texts[:5],
|
||||
output_lang=output_lang,
|
||||
)
|
||||
title = (llm or {}).get("title") if isinstance(llm, dict) else None
|
||||
keywords = (llm or {}).get("keywords", []) if isinstance(llm, dict) else []
|
||||
if not title:
|
||||
raise HTTPException(status_code=502, detail="Chat API returned empty title")
|
||||
if use_title_cache and title:
|
||||
try:
|
||||
TopicTitleCache.upsert(conn, cluster_hash, str(title), list(keywords or []), str(title_model))
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
out_clusters.append(
|
||||
{
|
||||
"id": f"topic_{cidx}",
|
||||
"title": title,
|
||||
"keywords": keywords,
|
||||
"size": len(paths),
|
||||
"paths": paths,
|
||||
"sample_prompt": _clean_for_title(rep)[:200],
|
||||
}
|
||||
)
|
||||
|
||||
out_clusters.sort(key=lambda x: x["size"], reverse=True)
|
||||
return {
|
||||
"folder": folder,
|
||||
"folders": folders,
|
||||
"model": model,
|
||||
"threshold": threshold,
|
||||
"min_cluster_size": min_cluster_size,
|
||||
"clusters": out_clusters,
|
||||
"noise": noise,
|
||||
"count": len(items),
|
||||
"title_model": title_model,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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-3d3aa3ce.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-db391c6a.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
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 +0,0 @@
|
|||
import{d as a,U as t,V as s,c as n,cD as _,a0 as o}from"./index-3d3aa3ce.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};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{d as a,U as t,V as s,c as n,cI as _,a0 as c}from"./index-db391c6a.js";const o={class:"img-sli-container"},i=a({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(l){return(e,r)=>(t(),s("div",o,[n(_,{left:e.left,right:e.right},null,8,["left","right"])]))}});const p=c(i,[["__scopeId","data-v-ec71de83"]]);export{p as default};
|
||||
|
|
@ -0,0 +1 @@
|
|||
.img-sli-container[data-v-ec71de83]{position:relative;overflow-y:auto;height:calc(100vh - 40px)}
|
||||
|
|
@ -1 +0,0 @@
|
|||
.img-sli-container[data-v-ae3fb9a8]{position:relative;overflow-y:auto;height:calc(100vh - 40px)}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
.container[data-v-6e837a6f]{background:var(--zp-secondary-background);position:relative}.container .action-bar[data-v-6e837a6f]{display:flex;align-items:center;user-select:none;gap:4px;padding:4px}.container .action-bar>*[data-v-6e837a6f]{flex-wrap:wrap}.container .file-list[data-v-6e837a6f]{list-style:none;padding:8px;overflow:auto;height:calc(var(--pane-max-height) - 40px);width:100%}.container .no-res-hint[data-v-6e837a6f]{height:var(--pane-max-height);display:flex;align-items:center;flex-direction:column;justify-content:center}.container .no-res-hint .hint[data-v-6e837a6f]{font-size:1.6em;margin-bottom:2em;text-align:center}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.container[data-v-795e5ef4]{background:var(--zp-secondary-background);position:relative;height:var(--pane-max-height)}.action-bar[data-v-795e5ef4]{display:flex;align-items:center;user-select:none;gap:6px;padding:6px 8px}.title[data-v-795e5ef4]{font-weight:700;max-width:40vw}.file-list[data-v-795e5ef4]{list-style:none;padding:8px;overflow:auto;height:calc(var(--pane-max-height) - 44px);width:100%}.no-res-hint[data-v-795e5ef4]{height:calc(var(--pane-max-height) - 44px);display:flex;align-items:center;flex-direction:column;justify-content:center}.no-res-hint .hint[data-v-795e5ef4]{font-size:1.2em;opacity:.7}.preview-switch[data-v-795e5ef4]{position:fixed;bottom:24px;right:24px;display:flex;gap:8px;font-size:36px;user-select:none}.disable[data-v-795e5ef4]{opacity:.3;pointer-events:none}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
.container[data-v-4815fec6]{background:var(--zp-secondary-background);position:relative}.container .action-bar[data-v-4815fec6]{display:flex;align-items:center;user-select:none;gap:4px;padding:4px}.container .action-bar>*[data-v-4815fec6]{flex-wrap:wrap}.container .file-list[data-v-4815fec6]{list-style:none;padding:8px;overflow:auto;height:calc(var(--pane-max-height) - 40px);width:100%}.container .no-res-hint[data-v-4815fec6]{height:var(--pane-max-height);display:flex;align-items:center;flex-direction:column;justify-content:center}.container .no-res-hint .hint[data-v-4815fec6]{font-size:1.6em;margin-bottom:2em;text-align:center}
|
||||
|
|
@ -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)}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.full-screen-menu[data-v-4fda442c]{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-4fda442c]{margin:4px 0}.full-screen-menu .tags-container .tag[data-v-4fda442c]{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-4fda442c]{background:var(--tag-color);color:#fff}.full-screen-menu .container[data-v-4fda442c]{height:100%;display:flex;overflow:hidden;flex-direction:column}.full-screen-menu .gen-info[data-v-4fda442c]{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-4fda442c]{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-4fda442c] .natural-text{margin:.5em 0;line-height:1.6em;text-align:justify;color:var(--zp-primary)}.full-screen-menu .gen-info code[data-v-4fda442c] .short-tag{word-break:break-all;white-space:nowrap}.full-screen-menu .gen-info code[data-v-4fda442c] 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-4fda442c] .has-parentheses.tag{background:rgba(255,100,100,.14)}.full-screen-menu .gen-info code[data-v-4fda442c] span.tag:hover{background:rgba(120,0,0,.15)}.full-screen-menu .gen-info table[data-v-4fda442c]{font-size:1em;border-radius:4px;border-collapse:separate;margin-bottom:3em}.full-screen-menu .gen-info table tr td[data-v-4fda442c]:first-child{white-space:nowrap;vertical-align:top}.full-screen-menu .gen-info table.extra-meta-table .extra-meta-value[data-v-4fda442c]{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-4fda442c]{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-4fda442c]{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-4fda442c]{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-4fda442c]{padding:4px}.full-screen-menu.unset-size[data-v-4fda442c]{width:unset!important;height:unset!important}.full-screen-menu .mouse-sensor[data-v-4fda442c]{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-4fda442c]{font-size:18px;padding:4px}.full-screen-menu .action-bar[data-v-4fda442c]{display:flex;align-items:center;user-select:none;gap:4px}.full-screen-menu .action-bar .icon[data-v-4fda442c]{font-size:1.5em;padding:2px 4px;border-radius:4px}.full-screen-menu .action-bar .icon[data-v-4fda442c]:hover{background:var(--zp-secondary-variant-background)}.full-screen-menu .action-bar>*[data-v-4fda442c]{flex-wrap:wrap}.full-screen-menu.lr[data-v-4fda442c]{top:var(--6e46b464)!important;right:0!important;bottom:0!important;left:100vw!important;height:unset!important;width:var(--02641c57)!important;transition:left ease .3s}.full-screen-menu.lr.always-on[data-v-4fda442c],.full-screen-menu.lr.mouse-in[data-v-4fda442c]{left:var(--c403b44a)!important}.tag-alpha-item[data-v-4fda442c]{display:flex;margin-top:4px}.tag-alpha-item h4[data-v-4fda442c]{width:32px;flex-shrink:0}.sort-tag-switch[data-v-4fda442c]{display:inline-block;padding-right:16px;padding-left:8px;cursor:pointer;user-select:none}.sort-tag-switch span[data-v-4fda442c]{transition:all ease .3s;transform:scale(1.2)}.sort-tag-switch:hover span[data-v-4fda442c]{transform:scale(1.3)}.lr-layout-control[data-v-4fda442c]{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-4fda442c]{display:flex;align-items:center;gap:4px;flex-wrap:nowrap}.select-actions[data-v-b6f9a67c]>:not(:last-child){margin-right:4px}.float-panel[data-v-b6f9a67c]{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
|
|
@ -1 +0,0 @@
|
|||
[data-v-e1bd92bd] .float-panel{position:fixed}.regex-icon[data-v-e1bd92bd]{user-select:none;padding:4px;margin:0 4px;cursor:pointer;border:1px solid var(--zp-border);border-radius:4px}.regex-icon img[data-v-e1bd92bd]{height:1.5em}.regex-icon[data-v-e1bd92bd]:hover{background:var(--zp-border)}.regex-icon.selected[data-v-e1bd92bd]{background:var(--primary-color-1);border:1px solid var(--primary-color)}.search-bar[data-v-e1bd92bd]{padding:8px 8px 0;display:flex}.search-bar.last[data-v-e1bd92bd]{padding-bottom:8px}.search-bar .form-name[data-v-e1bd92bd]{flex-shrink:0;padding:4px 8px}.search-bar .actions>*[data-v-e1bd92bd]{margin-right:4px}.container[data-v-e1bd92bd]{background:var(--zp-secondary-background);position:relative}.container .file-list[data-v-e1bd92bd]{list-style:none;padding:8px;height:100%;overflow:auto;height:var(--pane-max-height);width:100%}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
[data-v-2a7f2e56] .float-panel{position:fixed}.regex-icon[data-v-2a7f2e56]{user-select:none;padding:4px;margin:0 4px;cursor:pointer;border:1px solid var(--zp-border);border-radius:4px}.regex-icon img[data-v-2a7f2e56]{height:1.5em}.regex-icon[data-v-2a7f2e56]:hover{background:var(--zp-border)}.regex-icon.selected[data-v-2a7f2e56]{background:var(--primary-color-1);border:1px solid var(--primary-color)}.search-bar[data-v-2a7f2e56]{padding:8px 8px 0;display:flex}.search-bar.last[data-v-2a7f2e56]{padding-bottom:8px}.search-bar .form-name[data-v-2a7f2e56]{flex-shrink:0;padding:4px 8px}.search-bar .actions>*[data-v-2a7f2e56]{margin-right:4px}.container[data-v-2a7f2e56]{background:var(--zp-secondary-background);position:relative}.container .file-list[data-v-2a7f2e56]{list-style:none;padding:8px;height:100%;overflow:auto;height:var(--pane-max-height);width:100%}
|
||||
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
|
|
@ -0,0 +1 @@
|
|||
.topic-search[data-v-39216928]{height:var(--pane-max-height);overflow:auto;padding:12px}.toolbar[data-v-39216928]{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;background:var(--zp-primary-background);border-radius:12px}.left[data-v-39216928]{display:flex;align-items:center;gap:8px}.title[data-v-39216928]{display:flex;align-items:center;gap:8px;font-weight:700}.right[data-v-39216928]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.label[data-v-39216928]{color:#888}.guide[data-v-39216928]{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,.06)}.guide-row[data-v-39216928]{display:flex;align-items:center;gap:10px}.guide-hint[data-v-39216928]{margin-top:4px;display:flex;align-items:center;gap:10px;opacity:.85}.guide-icon[data-v-39216928]{width:22px;text-align:center;flex:0 0 22px}.guide-text[data-v-39216928]{flex:1 1 auto;min-width:0;color:#000000bf}.grid[data-v-39216928]{margin-top:12px;display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}.card[data-v-39216928]{background:var(--zp-primary-background);border-radius:12px;padding:10px 12px;cursor:pointer;border:1px solid rgba(0,0,0,.06)}.card[data-v-39216928]:hover{border-color:#1890ff99}.card-top[data-v-39216928]{display:flex;align-items:center;justify-content:space-between;gap:8px}.card-title[data-v-39216928]{font-weight:700}.card-count[data-v-39216928]{min-width:28px;text-align:right;opacity:.75}.card-desc[data-v-39216928]{margin-top:6px;color:#666;font-size:12px}.empty[data-v-39216928]{height:calc(var(--pane-max-height) - 72px);display:flex;align-items:center;justify-content:center;flex-direction:column;gap:12px}.hint[data-v-39216928]{font-size:16px;opacity:.75}
|
||||
|
|
@ -1 +1 @@
|
|||
import{bU as i,b1 as t,e0 as f,bL as n}from"./index-3d3aa3ce.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,e0 as f,bM as n}from"./index-db391c6a.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};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{d as F,a1 as B,cJ 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,cK as V,cL as y,z as x,B as E,ak as T,a0 as U}from"./index-db391c6a.js";import{_ as L}from"./index-30c22d1a.js";import{u as N,a as H,f as O,F as W,d as j}from"./FileItem-9be5bb5d.js";import"./numInput.vue_vue_type_style_index_0_scoped_bd954eda_lang-28fed536.js";/* empty css */import"./index-4a1bd1ce.js";import"./_isIterateeCall-6ab5736a.js";import"./index-ae90fb7e.js";import"./index-83d83387.js";const q={class:"actions-panel actions"},G={class:"item"},J={key:0,class:"file-list"},K={class:"hint"},P=F({__name:"batchDownload",props:{tabIdx:{},paneIdx:{},id:{}},setup(Q){const{stackViewEl:D}=N().toRefs(),{itemSize:h,gridItems:b,cellWidth:g}=H(),i=B(),m=O(),{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=L;return _(),w("div",{class:"container",ref_key:"stackViewEl",ref:D,onDrop:v},[f("div",q,[l(o,{onClick:t[0]||(t[0]=n=>s(m).selectdFiles=[])},{default:d(()=>[p(c(e.$t("clear")),1)]),_:1}),f("div",G,[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(j),{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(W,{idx:k,file:n,"cell-width":s(g),"enable-close-icon":"",onCloseIconClick:X=>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",J,[f("p",K,c(e.$t("batchDownloaDDragAndDropHint")),1)]))],544)}}});const le=U(P,[["__scopeId","data-v-3d7e6f2d"]]);export{le as default};
|
||||
|
|
@ -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-3d3aa3ce.js";import{_ as N}from"./index-0343c437.js";import{u as L,a as G,f as H,F as O,d as W}from"./FileItem-19dda24f.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-6330384b.js";/* empty css */import"./_isIterateeCall-295c4a47.js";import"./index-f644ead6.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};
|
||||
|
|
@ -1 +0,0 @@
|
|||
.container[data-v-a2642a17]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-a2642a17]{padding:8px;background-color:var(--zp-primary-background)}.container .actions-panel.actions[data-v-a2642a17]{display:flex;align-items:center;gap:16px;z-index:333}.container .file-list[data-v-a2642a17]{flex:1;z-index:222;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-a2642a17]{text-align:center;font-size:2em;padding:30vh 128px 0}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.container[data-v-3d7e6f2d]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-3d7e6f2d]{padding:8px;background-color:var(--zp-primary-background)}.container .actions-panel.actions[data-v-3d7e6f2d]{display:flex;align-items:center;gap:16px;z-index:333}.container .file-list[data-v-3d7e6f2d]{flex:1;z-index:222;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-3d7e6f2d]{text-align:center;font-size:2em;padding:30vh 128px 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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
import{u as w,a as y,F as k,d as x}from"./FileItem-19dda24f.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-3d3aa3ce.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-6330384b.js";/* empty css */import"./_isIterateeCall-295c4a47.js";import"./index-f644ead6.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};
|
||||
|
|
@ -0,0 +1 @@
|
|||
.container[data-v-0c31f6b2]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-0c31f6b2]{padding:8px;background-color:var(--zp-primary-background)}.container .file-list[data-v-0c31f6b2]{flex:1;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-0c31f6b2]{text-align:center;font-size:2em;padding:30vh 128px 0}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{u as w,a as y,F as b,d as k}from"./FileItem-9be5bb5d.js";import{d as x,a1 as h,c9 as F,r as D,bh as I,bl as C,U as V,V as E,c as n,a3 as z,a4 as e,af as S,cK as B,cM as K,a0 as R}from"./index-db391c6a.js";import"./numInput.vue_vue_type_style_index_0_scoped_bd954eda_lang-28fed536.js";/* empty css */import"./index-4a1bd1ce.js";import"./_isIterateeCall-6ab5736a.js";import"./index-ae90fb7e.js";import"./index-83d83387.js";const A=x({__name:"gridView",props:{tabIdx:{},paneIdx:{},id:{},removable:{type:Boolean},allowDragAndDrop:{type:Boolean},files:{},paneKey:{}},setup(p){const o=p,m=h(),{stackViewEl:d}=w().toRefs(),{itemSize:i,gridItems:u,cellWidth:f}=y(),g=F(),a=D(o.files??[]),_=async s=>{const l=B(s);o.allowDragAndDrop&&l&&(a.value=K([...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:_},[n(e(k),{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 c;return[n(b,{idx:r,file:t,"cell-width":e(f),"enable-close-icon":o.removable,onCloseIconClick:M=>v(r),"full-screen-preview-image-url":e(S)(t),"extra-tags":(c=t==null?void 0:t.tags)==null?void 0:c.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 j=R(A,[["__scopeId","data-v-0c31f6b2"]]);export{j as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
.container[data-v-f35f4802]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-f35f4802]{padding:8px;background-color:var(--zp-primary-background)}.container .file-list[data-v-f35f4802]{flex:1;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-f35f4802]{text-align:center;font-size:2em;padding:30vh 128px 0}
|
||||
|
|
@ -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-3d3aa3ce.js";import{u as L,a as Q,b as j,e as H}from"./FileItem-19dda24f.js";import{a as T,b as U,c as W}from"./MultiSelectKeep-5efc7de6.js";import{u as B}from"./useGenInfoDiff-f0c279cf.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-db391c6a.js";import{u as L,a as Q,b as j,e as H}from"./FileItem-9be5bb5d.js";import{a as T,b as U,c as W}from"./MultiSelectKeep-cd70772d.js";import{u as B}from"./useGenInfoDiff-9f58ef19.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};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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-3d3aa3ce.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 _};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{cw as j,ay as z,d as K,j as U,dp 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 _,dq as W,P as c,dr as x}from"./index-db391c6a.js";var q=z("small","default"),R=function(){return{id:String,prefixCls:String,size:c.oneOf(q),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:R(),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),u=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(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,P.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=j(J);export{X as _};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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}
|
||||
|
|
@ -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-3d3aa3ce.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,cE as j,az as k,n as V,cF as B,cG as y,e as $,c as a,_ as T,h as r,a as P,cH as F,P as b}from"./index-db391c6a.js";var G=["class","style"],H=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 M(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(H(),{size:"default",spinning:!0,wrapperClassName:""}),setup:function(){return{originalUpdateSpinning:null,configProvider:A("configProvider",j)}},data:function(){var n=this.spinning,e=this.delay,i=M(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=T(l,G),S=this.configProvider,U=S.getPrefixCls,E=S.direction,s=U("spin",x),u=this.sSpinning,z=(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"),E==="rtl"),r(i,v,!!v),i),m=a("div",P(P({},C),{},{style:_,class:z}),[this.renderIndicator(s),d?a("div",{class:"".concat(s,"-text")},[d]):null]),g=F(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 +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-3d3aa3ce.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,cN 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 N,z as _,B as v,aj as V,ak as W,cO as D,a0 as R}from"./index-db391c6a.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=N(e.tabs)},k=b(async e=>{await D(`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=V,l=W;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-e55e3025"]]);export{E as default};
|
||||
|
|
@ -0,0 +1 @@
|
|||
.container[data-v-e55e3025]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column;padding:16px}.container .actions[data-v-e55e3025]{margin-bottom:16px}.container .actions *[data-v-e55e3025]{margin-right:10px}.snapshot[data-v-e55e3025]{list-style:none;padding:0;margin:0;width:512px}.snapshot li[data-v-e55e3025]{display:flex;justify-content:space-between;align-items:center;padding:10px;background-color:var(--zp-secondary-variant-background);border-radius:4px;margin-bottom:10px;transition:all .3s ease;border-bottom:2px solid var(--zp-luminous-deep)}.snapshot li[data-v-e55e3025]:hover{border-bottom:2px solid var(--zp-luminous)}.snapshot li div[data-v-e55e3025]:first-child{flex-grow:1;font-weight:700}.snapshot li div[data-v-e55e3025]:last-child{display:flex;gap:10px}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{d as F,u as S,G as k,an as j,h as d,c as s,aq as M,e1 as U,r as W,bh as G,Z as V,dq as Z,P as N,cb as z}from"./index-db391c6a.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(M.join("|"),")(-inverse)?$")),L=new RegExp("^(".concat(U.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=W(!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),q=I?s(V,null,[I,s("span",null,[T])]):T,A="onClick"in g,$=s("span",{class:E.value,style:O},[q,B()]);return A?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 _};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
.container[data-v-2c44013c]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column;padding:16px}.container .actions[data-v-2c44013c]{margin-bottom:16px}.container .actions *[data-v-2c44013c]{margin-right:10px}.snapshot[data-v-2c44013c]{list-style:none;padding:0;margin:0;width:512px}.snapshot li[data-v-2c44013c]{display:flex;justify-content:space-between;align-items:center;padding:10px;background-color:var(--zp-secondary-variant-background);border-radius:4px;margin-bottom:10px;transition:all .3s ease;border-bottom:2px solid var(--zp-luminous-deep)}.snapshot li[data-v-2c44013c]:hover{border-bottom:2px solid var(--zp-luminous)}.snapshot li div[data-v-2c44013c]:first-child{flex-grow:1;font-weight:700}.snapshot li div[data-v-2c44013c]:last-child{display:flex;gap:10px}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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-3d3aa3ce.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-db391c6a.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 +0,0 @@
|
|||
import{ck as e,cl as i,cm as r,cn as a,b1 as n}from"./index-3d3aa3ce.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};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{cl as e,cm as i,cn as r,co as a,b1 as n}from"./index-db391c6a.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};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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-bd954eda]{display:flex}.num-input .slide[data-v-bd954eda]{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
1
vue/dist/assets/numInput.vue_vue_type_style_index_0_scoped_bd954eda_lang-28fed536.js
vendored
Normal file
1
vue/dist/assets/numInput.vue_vue_type_style_index_0_scoped_bd954eda_lang-28fed536.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 195 B After Width: | Height: | Size: 195 B |
|
|
@ -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,cP as ce,cQ as de,ak as ue,ai as me,T as fe,a0 as pe}from"./index-db391c6a.js";import{u as ve,c as ge,a as ke,F as we,d as he}from"./FileItem-9be5bb5d.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-cd70772d.js";import"./numInput.vue_vue_type_style_index_0_scoped_bd954eda_lang-28fed536.js";/* empty css */import"./index-4a1bd1ce.js";import"./_isIterateeCall-6ab5736a.js";import"./index-ae90fb7e.js";import"./index-83d83387.js";import"./shortcut-ace377a3.js";import"./Checkbox-be055c11.js";import"./index-30c22d1a.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:K,multiSelectedIdxs:p,stack:L,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:Q,onContextMenuClick:q,onFileItemClick:H}=Se({openNext:de}),{previewIdx:o,previewing:y,onPreviewVisibleChange:J,previewImgMove:x,canPreview:b}=_e(),V=async(s,t,d)=>{L.value=[{curr:"",files:l.value}],await q(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:K},[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(J),"is-selected-mutil-files":e(p).length>1,selected:e(p).includes(T),onFileItemClick:e(H),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-e1531e89"]]);export{We as default};
|
||||
|
|
@ -1 +1 @@
|
|||
.container[data-v-49082269]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-49082269]{padding:8px;background-color:var(--zp-primary-background)}.container .refresh-button[data-v-49082269]{position:absolute;top:90%;left:50%;transform:translate(-50%,-50%);z-index:99;background:white;border-radius:9999px;box-shadow:0 0 20px var(--zp-secondary);padding:4px;display:flex;align-items:center;gap:8px}.container .file-list[data-v-49082269]{flex:1;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-49082269]{text-align:center;font-size:2em;padding:30vh 128px 0}
|
||||
.container[data-v-e1531e89]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-e1531e89]{padding:8px;background-color:var(--zp-primary-background)}.container .refresh-button[data-v-e1531e89]{position:absolute;top:90%;left:50%;transform:translate(-50%,-50%);z-index:99;background:white;border-radius:9999px;box-shadow:0 0 20px var(--zp-secondary);padding:4px;display:flex;align-items:center;gap:8px}.container .file-list[data-v-e1531e89]{flex:1;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-e1531e89]{text-align:center;font-size:2em;padding:30vh 128px 0}
|
||||
|
|
@ -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-3d3aa3ce.js";import{u as ve,c as ge,a as ke,F as we,d as he}from"./FileItem-19dda24f.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-5efc7de6.js";import"./numInput.vue_vue_type_style_index_0_scoped_55978858_lang-6330384b.js";/* empty css */import"./_isIterateeCall-295c4a47.js";import"./index-f644ead6.js";import"./shortcut-7f0d6830.js";import"./Checkbox-20185ccb.js";import"./index-0343c437.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};
|
||||
|
|
@ -0,0 +1 @@
|
|||
[data-v-fff181dd] .ant-row .ant-col:nth-child(1){font-weight:700}.record-container[data-v-fff181dd]{list-style:none;padding:8px;margin:16px;max-height:50vh;overflow:auto}.record[data-v-fff181dd]{display:flex;flex-direction:row;justify-content:space-between;align-items:center;padding:10px;border-bottom:1px solid var(--zp-tertiary);position:relative;flex-wrap:nowrap;transition:all .3s ease}.record[data-v-fff181dd]:hover{background:var(--zp-secondary-background)}.record .rec-actions[data-v-fff181dd]{user-select:none;display:flex;gap:8px}.record .pin[data-v-fff181dd]{cursor:pointer;padding:4px 8px;border-radius:4px;transition:all .3s ease}.record .pin[data-v-fff181dd]:hover{background:var(--zp-primary-background)}
|
||||
|
|
@ -1 +0,0 @@
|
|||
[data-v-834a248f] .ant-row .ant-col:nth-child(1){font-weight:700}.record-container[data-v-834a248f]{list-style:none;padding:8px;margin:16px;max-height:50vh;overflow:auto}.record[data-v-834a248f]{display:flex;flex-direction:row;justify-content:space-between;align-items:center;padding:10px;border-bottom:1px solid var(--zp-tertiary);position:relative;flex-wrap:nowrap;transition:all .3s ease}.record[data-v-834a248f]:hover{background:var(--zp-secondary-background)}.record .rec-actions[data-v-834a248f]{user-select:none;display:flex;gap:8px}.record .pin[data-v-834a248f]{cursor:pointer;padding:4px 8px;border-radius:4px;transition:all .3s ease}.record .pin[data-v-834a248f]:hover{background:var(--zp-primary-background)}
|
||||
|
|
@ -1 +1 @@
|
|||
import{R as y,C as v}from"./index-ec2847a4.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-3d3aa3ce.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-e3af27b3.js";import{cw as f,c as d,A as w,d as P,U as a,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-db391c6a.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 a(),c("div",null,[r("ul",F,[(a(!0),c(S,null,V(e.records.getRecords(),i=>(a(),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-fff181dd"]]);class o{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 o,{serializer:{read:t=>{const e=JSON.parse(t);return new o(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}}),M=_(`${m}tag-search-HistoryRecord`,new o,{serializer:{read:t=>{const e=JSON.parse(t);return new o(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 +1 @@
|
|||
.ant-breadcrumb{box-sizing:border-box;margin:0;padding:0;color:#000000d9;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";color:#00000073;font-size:14px}.ant-breadcrumb .anticon{font-size:14px}.ant-breadcrumb a{color:#00000073;transition:color .3s}.ant-breadcrumb a:hover{color:#de632f}.ant-breadcrumb>span:last-child{color:#000000d9}.ant-breadcrumb>span:last-child a{color:#000000d9}.ant-breadcrumb>span:last-child .ant-breadcrumb-separator{display:none}.ant-breadcrumb-separator{margin:0 8px;color:#00000073}.ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-link>.anticon+a{margin-left:4px}.ant-breadcrumb-overlay-link>.anticon{margin-left:4px}.ant-breadcrumb-rtl{direction:rtl}.ant-breadcrumb-rtl:before{display:table;content:""}.ant-breadcrumb-rtl:after{display:table;clear:both;content:""}.ant-breadcrumb-rtl>span{float:right}.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+a{margin-right:4px;margin-left:0}.ant-breadcrumb-rtl .ant-breadcrumb-overlay-link>.anticon{margin-right:4px;margin-left:0}.nprogress{pointer-events:none}.nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}.nprogress .peg{display:block;position:absolute;right:0px;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translate(0px,-4px);-ms-transform:rotate(3deg) translate(0px,-4px);transform:rotate(3deg) translateY(-4px)}.nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}.nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent .nprogress .spinner,.nprogress-custom-parent .nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.base-info[data-v-afd25667]{position:absolute;padding:4px;font-size:.8em;background:var(--zp-primary-background);color:var(--zp-primary);left:0;bottom:0;border-top-right-radius:4px}.location-act[data-v-317d328e]{margin-left:8px}.location-act .copy[data-v-317d328e]{margin-right:4px}@media (max-width: 768px){.location-act[data-v-317d328e]{display:flex;flex-direction:column}.location-act>*[data-v-317d328e],.location-act .copy[data-v-317d328e]{margin:2px}}.breadcrumb[data-v-317d328e]{display:flex;align-items:center}.breadcrumb>*[data-v-317d328e]{margin-right:4px}@media (max-width: 768px){.breadcrumb[data-v-317d328e]{width:100%}.breadcrumb .ant-breadcrumb>*[data-v-317d328e]{display:inline-block}}.container[data-v-317d328e]{background:var(--zp-secondary-background);height:var(--pane-max-height)}.location-bar[data-v-317d328e]{padding:4px 16px;background:var(--zp-primary-background);border-bottom:1px solid var(--zp-border);display:flex;align-items:center;justify-content:space-between}@media (max-width: 768px){.location-bar[data-v-317d328e]{flex-direction:column}.location-bar[data-v-317d328e] ::-webkit-scrollbar{height:2px;background-color:var(--zp-secondary-variant-background)}.location-bar .actions[data-v-317d328e]{padding:4px 0;width:100%;overflow:auto;display:flex;align-items:center}.location-bar .actions>*[data-v-317d328e]{flex-shrink:0}}.location-bar .actions[data-v-317d328e]{display:flex;align-items:center;flex-shrink:0}.location-bar a.opt[data-v-317d328e]{margin-left:8px}.view[data-v-317d328e]{padding:8px;height:calc(100vh - 48px)}.view .file-list[data-v-317d328e]{list-style:none;padding:8px;height:100%;overflow:auto}.hint[data-v-317d328e]{padding:4px;border:4px;background:var(--zp-secondary-background);border:1px solid var(--zp-border)}
|
||||
.ant-breadcrumb{box-sizing:border-box;margin:0;padding:0;color:#000000d9;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";color:#00000073;font-size:14px}.ant-breadcrumb .anticon{font-size:14px}.ant-breadcrumb a{color:#00000073;transition:color .3s}.ant-breadcrumb a:hover{color:#de632f}.ant-breadcrumb>span:last-child{color:#000000d9}.ant-breadcrumb>span:last-child a{color:#000000d9}.ant-breadcrumb>span:last-child .ant-breadcrumb-separator{display:none}.ant-breadcrumb-separator{margin:0 8px;color:#00000073}.ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-link>.anticon+a{margin-left:4px}.ant-breadcrumb-overlay-link>.anticon{margin-left:4px}.ant-breadcrumb-rtl{direction:rtl}.ant-breadcrumb-rtl:before{display:table;content:""}.ant-breadcrumb-rtl:after{display:table;clear:both;content:""}.ant-breadcrumb-rtl>span{float:right}.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+a{margin-right:4px;margin-left:0}.ant-breadcrumb-rtl .ant-breadcrumb-overlay-link>.anticon{margin-right:4px;margin-left:0}.nprogress{pointer-events:none}.nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}.nprogress .peg{display:block;position:absolute;right:0px;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translate(0px,-4px);-ms-transform:rotate(3deg) translate(0px,-4px);transform:rotate(3deg) translateY(-4px)}.nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}.nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent .nprogress .spinner,.nprogress-custom-parent .nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.base-info[data-v-7f9c0b15]{position:absolute;padding:4px;font-size:.8em;background:var(--zp-primary-background);color:var(--zp-primary);left:0;bottom:0;border-top-right-radius:4px}.location-act[data-v-8d4bd42a]{margin-left:8px}.location-act .copy[data-v-8d4bd42a]{margin-right:4px}@media (max-width: 768px){.location-act[data-v-8d4bd42a]{display:flex;flex-direction:column}.location-act>*[data-v-8d4bd42a],.location-act .copy[data-v-8d4bd42a]{margin:2px}}.breadcrumb[data-v-8d4bd42a]{display:flex;align-items:center}.breadcrumb>*[data-v-8d4bd42a]{margin-right:4px}@media (max-width: 768px){.breadcrumb[data-v-8d4bd42a]{width:100%}.breadcrumb .ant-breadcrumb>*[data-v-8d4bd42a]{display:inline-block}}.container[data-v-8d4bd42a]{background:var(--zp-secondary-background);height:var(--pane-max-height)}.location-bar[data-v-8d4bd42a]{padding:4px 16px;background:var(--zp-primary-background);border-bottom:1px solid var(--zp-border);display:flex;align-items:center;justify-content:space-between}@media (max-width: 768px){.location-bar[data-v-8d4bd42a]{flex-direction:column}.location-bar[data-v-8d4bd42a] ::-webkit-scrollbar{height:2px;background-color:var(--zp-secondary-variant-background)}.location-bar .actions[data-v-8d4bd42a]{padding:4px 0;width:100%;overflow:auto;display:flex;align-items:center}.location-bar .actions>*[data-v-8d4bd42a]{flex-shrink:0}}.location-bar .actions[data-v-8d4bd42a]{display:flex;align-items:center;flex-shrink:0}.location-bar a.opt[data-v-8d4bd42a]{margin-left:8px}.view[data-v-8d4bd42a]{padding:8px;height:calc(100vh - 48px)}.view .file-list[data-v-8d4bd42a]{list-style:none;padding:8px;height:100%;overflow:auto}.hint[data-v-8d4bd42a]{padding:4px;border:4px;background:var(--zp-secondary-background);border:1px solid var(--zp-border)}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{u as G,g as d}from"./FileItem-9be5bb5d.js";import{r as b,t as j,ct as m,cu as y,cv as D}from"./index-db391c6a.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};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as G,g as d}from"./FileItem-19dda24f.js";import{r as b,t as j,cs as m,ct as y,cu as D}from"./index-3d3aa3ce.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};
|
||||
|
|
@ -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-3d3aa3ce.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-db391c6a.js"></script>
|
||||
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-af514ea9.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -161,4 +161,94 @@ 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[]
|
||||
}
|
||||
|
||||
export const clusterIibOutput = async (req: ClusterIibOutputReq) => {
|
||||
const resp = await axiosInst.value.post('/db/cluster_iib_output', req, { timeout: Infinity })
|
||||
return resp.data as ClusterIibOutputResp
|
||||
}
|
||||
|
||||
// ===== 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
|
||||
}
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -118,6 +118,15 @@ export const getGlobalSetting = async () => {
|
|||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取后端原始 global_setting(包含 app_fe_setting),不受 isSync() 影响。
|
||||
* 仅在确实需要使用后端 KV(GlobalSetting 表)做持久化时使用。
|
||||
*/
|
||||
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 }
|
||||
|
|
@ -171,6 +180,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 } })
|
||||
|
|
@ -178,4 +195,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
|
||||
}
|
||||
|
|
@ -20,6 +20,32 @@ 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})',
|
||||
|
||||
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',
|
||||
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).',
|
||||
'auto.refreshed': 'Automatische Aktualisierung erfolgreich durchgeführt!',
|
||||
copied: 'In die Zwischenablage kopiert!',
|
||||
'index.expired': 'Index abgelaufen, automatische Aktualisierung wird durchgeführt',
|
||||
|
|
@ -81,6 +107,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',
|
||||
|
|
|
|||
|
|
@ -20,6 +20,32 @@ 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})',
|
||||
|
||||
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',
|
||||
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).',
|
||||
success: 'Success',
|
||||
setCurrFrameAsVideoPoster: 'Set Current Frame as Video Cover',
|
||||
sync: 'Sync',
|
||||
|
|
@ -269,6 +295,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 +411,6 @@ 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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,32 @@ 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})',
|
||||
|
||||
topicSearchGuideTitle: '快速上手(实验性)',
|
||||
topicSearchGuideStep1: '选择要分析的文件夹范围(可多选)',
|
||||
topicSearchGuideStep2: '点击刷新,生成主题卡片(会增量向量化)',
|
||||
topicSearchGuideStep3: '输入一句话搜索,会召回相似图片并打开结果页',
|
||||
topicSearchGuideEmptyReasonNoScope: '当前为空:未选择范围(已默认关闭),请先点“范围”选择文件夹',
|
||||
topicSearchGuideEmptyReasonNoTopics: '当前为空:该范围内还未生成主题(可点刷新,或调低最小组/阈值)',
|
||||
success: '成功',
|
||||
setCurrFrameAsVideoPoster: '设置当前帧为视频封面',
|
||||
sync: '同步',
|
||||
|
|
@ -128,6 +154,7 @@ export const zhHans = {
|
|||
sendImageFailed: '发送图像失败,请携带console的错误消息找开发者',
|
||||
confirmDelete: '确认删除?',
|
||||
deleteSuccess: '删除成功',
|
||||
moveToTrashSuccess: '已移动到回收站',
|
||||
doubleClickToCopy: '双击复制',
|
||||
root: '根',
|
||||
drive: '盘',
|
||||
|
|
@ -362,5 +389,6 @@ 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分析标签'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,32 @@ 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})',
|
||||
|
||||
topicSearchGuideTitle: '快速上手(實驗性)',
|
||||
topicSearchGuideStep1: '選擇要分析的資料夾範圍(可多選)',
|
||||
topicSearchGuideStep2: '點擊刷新,生成主題卡片(會增量向量化)',
|
||||
topicSearchGuideStep3: '輸入一句話搜尋,召回相似圖片並打開結果頁',
|
||||
topicSearchGuideEmptyReasonNoScope: '目前為空:尚未選擇範圍(預設關閉),請先點「範圍」選擇資料夾',
|
||||
topicSearchGuideEmptyReasonNoTopics: '目前為空:此範圍尚未生成主題(可點刷新,或調低最小組/閾值)',
|
||||
clearCacheIfNotTakeEffect: '如果更改沒有生效,請嘗試清理頁面緩存',
|
||||
success: '成功',
|
||||
setCurrFrameAsVideoPoster: '設置當前幀為視頻封面',
|
||||
|
|
@ -134,6 +160,7 @@ export const zhHant: Partial<IIBI18nMap> = {
|
|||
sendImageFailed: '發送圖像失敗,請攜帶console的錯誤訊息找開發者',
|
||||
confirmDelete: '確認刪除?',
|
||||
deleteSuccess: '刪除成功',
|
||||
moveToTrashSuccess: '已移動到回收站',
|
||||
doubleClickToCopy: '雙擊複製',
|
||||
promptcompare: 'Compare Prompts',
|
||||
root: '根',
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,472 @@
|
|||
<script setup lang="ts">
|
||||
import { getGlobalSettingRaw, setAppFeSettingForce } from '@/api'
|
||||
import { clusterIibOutput, searchIibOutputByPrompt, type ClusterIibOutputResp, type PromptSearchResp } from '@/api/db'
|
||||
import { t } from '@/i18n'
|
||||
import { useGlobalStore } from '@/store/useGlobalStore'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { uniqueId } from 'lodash-es'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const props = defineProps<{ tabIdx: number; paneIdx: number }>()
|
||||
const g = useGlobalStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const threshold = ref(0.86)
|
||||
const minClusterSize = ref(2)
|
||||
const result = ref<ClusterIibOutputResp | null>(null)
|
||||
|
||||
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 ?? []).slice(0, 12))
|
||||
|
||||
const loadScopeFromBackend = async () => {
|
||||
if (_scopeInitDone.value) return
|
||||
try {
|
||||
const conf = await getGlobalSettingRaw()
|
||||
// 同步到全局 store,避免页面间不一致
|
||||
g.conf = conf as any
|
||||
} catch (e) {
|
||||
// ignore(axios 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 refresh = async () => {
|
||||
if (g.conf?.is_readonly) return
|
||||
if (!scopeCount.value) {
|
||||
message.warning(t('topicSearchNeedScope'))
|
||||
scopeOpen.value = true
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
result.value = await clusterIibOutput({
|
||||
threshold: threshold.value,
|
||||
min_cluster_size: minClusterSize.value,
|
||||
lang: g.lang,
|
||||
folder_paths: scopeFolders.value
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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 refresh()
|
||||
}
|
||||
})()
|
||||
})
|
||||
|
||||
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-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-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>
|
||||
|
||||
|
||||
|
|
@ -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,80 @@ const onTiktokViewClick = () => {
|
|||
emit('contextMenuClick', { key: 'tiktokView' } as any, props.file, props.idx)
|
||||
}
|
||||
|
||||
// AI分析tag功能
|
||||
const analyzeTagsWithAI = async () => {
|
||||
if (!geninfoStruct.value.prompt) {
|
||||
message.warning('没有找到提示词')
|
||||
return
|
||||
}
|
||||
|
||||
if (!global.conf?.all_custom_tags?.length) {
|
||||
message.warning('没有自定义标签')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const prompt = geninfoStruct.value.prompt
|
||||
const availableTags = global.conf.all_custom_tags.map(tag => tag.name).join(', ')
|
||||
|
||||
const systemMessage = `你是一个专业的AI助手,负责分析Stable Diffusion提示词并将其分类到相应的标签中。
|
||||
|
||||
你的任务是:
|
||||
1. 分析给定的提示词
|
||||
2. 从提供的标签列表中找出所有相关的标签
|
||||
3. 只返回匹配的标签名称,用逗号分隔
|
||||
4. 如果没有匹配的标签,返回空字符串
|
||||
5. 标签匹配应该基于语义相似性和主题相关性
|
||||
|
||||
可用的标签:${availableTags}
|
||||
|
||||
请只返回标签名称,不要包含其他内容。`
|
||||
|
||||
const response = await aiChat({
|
||||
messages: [
|
||||
{ role: 'system', content: systemMessage },
|
||||
{ role: 'user', content: `请分析这个提示词并返回匹配的标签:${prompt}` }
|
||||
],
|
||||
temperature: 0.3,
|
||||
max_tokens: 200
|
||||
})
|
||||
|
||||
const matchedTagsText = response.choices[0].message.content.trim()
|
||||
if (!matchedTagsText) {
|
||||
message.info('AI没有找到匹配的标签')
|
||||
return
|
||||
}
|
||||
|
||||
// 解析返回的标签
|
||||
const matchedTagNames = matchedTagsText.split(',').map((name: string) => name.trim()).filter((name: string) => name)
|
||||
|
||||
// 找到对应的tag对象
|
||||
const tagsToAdd = 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())
|
||||
)
|
||||
)
|
||||
|
||||
if (tagsToAdd.length === 0) {
|
||||
message.info('没有找到有效的匹配标签')
|
||||
return
|
||||
}
|
||||
|
||||
// 为每个匹配的tag发送添加请求
|
||||
for (const tag of tagsToAdd) {
|
||||
emit('contextMenuClick', { key: `toggle-tag-${tag.id}` } as any, props.file, props.idx)
|
||||
}
|
||||
|
||||
message.success(`已添加 ${tagsToAdd.length} 个标签:${tagsToAdd.map(t => t.name).join(', ')}`)
|
||||
|
||||
} catch (error) {
|
||||
console.error('AI分析标签失败:', error)
|
||||
message.error('AI分析标签失败,请检查配置')
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -384,6 +466,13 @@ const onTiktokViewClick = () => {
|
|||
<a-button @click="copyPositivePrompt" v-if="imageGenInfo">{{
|
||||
$t('copyPositivePrompt')
|
||||
}}</a-button>
|
||||
<a-button
|
||||
@click="analyzeTagsWithAI"
|
||||
type="primary"
|
||||
v-if="imageGenInfo && global.conf?.all_custom_tags?.length"
|
||||
>
|
||||
{{ $t('aiAnalyzeTags') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
@click="onTiktokViewClick"
|
||||
@touchstart.prevent="onTiktokViewClick"
|
||||
|
|
@ -490,6 +579,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 +708,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue