mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-07-24 15:24:06 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 568f0f1838 | |||
| cebc3dc5c2 | |||
| 443afab7bd | |||
| e635ec6908 | |||
| ef7a218ba6 | |||
| bd0ad0aebf | |||
| 8cbe00e348 | |||
| 6300f5fb26 | |||
| 73090da95a |
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "infinite-canvas-local",
|
||||
"interface": {
|
||||
"displayName": "Infinite Canvas"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "infinite-canvas",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/infinite-canvas"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
NEXT_PUBLIC_DOC_URL=https://docs.canvas.best
|
||||
@@ -32,15 +32,16 @@
|
||||
|
||||
## 前端规范
|
||||
|
||||
- 前端使用 Next.js App Router、React、TypeScript、Ant Design、Tailwind、Zustand。
|
||||
- 前端使用 Vite、React、React Router、TypeScript、Ant Design、Tailwind、Zustand。
|
||||
- 编写 Ant Design 相关代码时,参考 https://ant.design/llms-full.txt 理解组件 API、示例和设计规范,并优先结合项目当前 antd 版本与既有写法。
|
||||
- API 请求统一放在 `web/src/services/api/`。
|
||||
- 全局或跨页面状态优先放在 `web/src/stores/`。
|
||||
- 已经放在全局 store 或全局 hook 中的状态/动作,组件需要时直接使用对应 store/hook,不要为了“纯组件”层层透传 props;避免一个组件传递过多参数。
|
||||
- 全局组件、全局常量、全局配置等全局性质的内容不要作为 props 或参数层层传递;哪里需要就在哪里直接从对应全局入口获取。
|
||||
- 多个页面重复出现的 UI 副作用动作,例如复制文本并提示、下载并提示、统一确认弹窗,优先抽成 `web/src/hooks/` 下的全局 hook;不要放进 store,除非它确实是需要共享/订阅的状态。
|
||||
- 画布相关状态和组件放在 `web/src/app/(user)/canvas/` 内部。
|
||||
- 页面里只有一个主业务组件时直接写在 `page.tsx`,不要单独拆 `Manager` 组件再传一堆 props。
|
||||
- 路由页面放在 `web/src/pages/`,页面布局放在 `web/src/layouts/`,路由配置放在 `web/src/router.tsx`。
|
||||
- 画布页面放在 `web/src/pages/canvas/`,画布组件放在 `web/src/components/canvas/`,画布状态放在 `web/src/stores/canvas/`,画布工具函数放在 `web/src/lib/canvas/`。
|
||||
- 页面按目录组织,例如 `web/src/pages/image/index.tsx`;页面里只有一个主业务组件时直接写在对应页面入口中,不要单独拆 `Manager` 组件再传一堆 props。
|
||||
- 不要新增只做简单转发的组件,例如只 `return <X>{children}</X>` 或只换个名字透传 props;直接在使用处使用真实组件或把逻辑写进当前文件。
|
||||
- 页面私有 hook 放在对应页面目录下,例如 `admin/assets/use-admin-assets.ts`;只有多个页面真实复用的 hook 才放到外层 `hooks/`。
|
||||
- 管理后台页面私有组件放到各自页面目录的 `components/` 下,例如 `admin/assets/components/`、`admin/prompts/components/`;不要为了单页面使用放到 `admin/components/` 共享目录。
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
+ [新增] 新增Codex App插件支持。
|
||||
+ [修复] 修复前端 TypeScript 构建报错。
|
||||
+ [调整] Docker 运行镜像改为 nginx 静态托管。
|
||||
|
||||
## v0.5.0 - 2026-07-05
|
||||
|
||||
+ [新增] 渠道兼容Gemini格式。
|
||||
+ [调整] 前端从 Next.js 迁移到 Vite,项目改为静态前端构建。
|
||||
+ [调整] 移除已 404 的 EvoLinkAI 提示词来源。
|
||||
|
||||
## v0.4.0 - 2026-06-16
|
||||
|
||||
+ [新增] 新增网页版Agent Loop模式。
|
||||
|
||||
+6
-15
@@ -1,27 +1,18 @@
|
||||
# 构建 Next.js 前端产物。
|
||||
# 构建 Vite 前端产物。
|
||||
FROM oven/bun:1.3.13 AS web-build
|
||||
|
||||
WORKDIR /app/web
|
||||
COPY web/package.json web/bun.lock ./
|
||||
RUN --mount=type=cache,target=/root/.bun/install/cache bun install --frozen-lockfile --cache-dir=/root/.bun/install/cache
|
||||
RUN --mount=type=cache,target=/root/.bun/install/cache bun install --cache-dir=/root/.bun/install/cache
|
||||
COPY VERSION /app/VERSION
|
||||
COPY CHANGELOG.md /app/CHANGELOG.md
|
||||
COPY web ./
|
||||
RUN bun run build
|
||||
|
||||
# 运行镜像:只启动 Next.js,AI 请求由浏览器前台直连用户自己的接口。
|
||||
FROM node:22-bookworm-slim
|
||||
# 运行镜像:只启动静态前端,AI 请求由浏览器前台直连用户自己的接口。
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY VERSION /app/VERSION
|
||||
COPY CHANGELOG.md /app/CHANGELOG.md
|
||||
COPY --from=web-build /app/web/public /app/web/public
|
||||
COPY --from=web-build /app/web/.next/standalone /app/web
|
||||
COPY --from=web-build /app/web/.next/static /app/web/.next/static
|
||||
ENV NODE_ENV=production
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
ENV PORT=3000
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=web-build /app/web/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["sh", "-c", "cd /app/web && PORT=3000 node server.js"]
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<a href="https://github.com/basketikun/infinite-canvas"><img src="https://img.shields.io/github/stars/basketikun/infinite-canvas?style=flat-square&logo=github" alt="GitHub stars"></a>
|
||||
<a href="VERSION"><img src="https://img.shields.io/badge/version-v0.2.0-2563eb?style=flat-square" alt="Version"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-f97316?style=flat-square" alt="License"></a>
|
||||
<a href="https://vercel.com/"><img src="https://img.shields.io/badge/Vercel-ready-000000?style=flat-square&logo=vercel" alt="Vercel ready"></a>
|
||||
<a href="https://nextjs.org/"><img src="https://img.shields.io/badge/Next.js-16.2-000000?style=flat-square&logo=nextdotjs" alt="Next.js"></a>
|
||||
<a href="https://vite.dev/"><img src="https://img.shields.io/badge/Vite-7-646cff?style=flat-square&logo=vite&logoColor=white" alt="Vite"></a>
|
||||
<a href="https://reactrouter.com/"><img src="https://img.shields.io/badge/React_Router-7-ca4245?style=flat-square&logo=reactrouter&logoColor=white" alt="React Router"></a>
|
||||
</p>
|
||||
|
||||
无限画布是一款面向图片创作的开源工作台。它把画布编排、AI 图片生成、参考图编辑、对话助手、提示词库和素材沉淀放在同一个界面里,适合用来探索视觉方案并连续迭代图片结果。
|
||||
@@ -26,8 +26,9 @@
|
||||
- 无限画布:多画布项目、节点拖拽缩放、连线、小地图、撤销重做、导入导出。
|
||||
- AI 创作:浏览器前台直连你配置的 OpenAI 兼容接口,支持文生图、图生图、参考图编辑、文本问答、音频和视频生成;Seedance 2.0 可通过火山方舟 Agent Plan 接入。
|
||||
- 画布助手:围绕选中节点和上游节点对话、生图,并把结果插回画布。
|
||||
- 本地 Agent:通过本机 Canvas Agent 连接 Codex / Claude Code,让 Agent 通过 MCP 操作当前画布。
|
||||
- 提示词库:Next.js route 抓取多个 GitHub 开源项目,并缓存在运行实例内存中。
|
||||
- 本地 Agent:通过本机 Canvas Agent 连接 Codex / Claude Code,让 Agent 通过 MCP 操作当前画布;
|
||||
- Codex App 插件:提供 Codex app 插件,安装后会自动注册 MCP 并尝试拉起本地 Agent。
|
||||
- 提示词库:浏览器前端直连多个 GitHub 开源项目,并缓存到 IndexedDB。
|
||||
|
||||
完整功能说明见 [功能介绍](docs/content/docs/overview/features.mdx)。
|
||||
|
||||
@@ -35,13 +36,13 @@
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 前端:Next.js、React、TypeScript、Tailwind CSS、Ant Design、Zustand、TanStack Query。
|
||||
- 少量 Next.js Route:第三方提示词内存缓存、WebDAV 可选代理。
|
||||
- 部署:Vercel 或 Docker。
|
||||
- 前端:Vite、React、React Router、TypeScript、Tailwind CSS、Ant Design、Zustand、TanStack Query。
|
||||
- 前端:提示词库、WebDAV 和 AI 接口都由浏览器前端直连。
|
||||
- 部署:静态站点托管或 Docker。
|
||||
|
||||
## 快速开始
|
||||
|
||||
推荐直接导入仓库到 Vercel,根目录已提供 `vercel.json`,会构建 `web/`。AI API Key、Base URL、画布、素材和生成记录默认保存在浏览器本地。
|
||||
AI API Key、Base URL、画布、素材和生成记录默认保存在浏览器本地。
|
||||
|
||||
```bash
|
||||
git clone git@github.com:basketikun/infinite-canvas.git
|
||||
@@ -106,6 +107,7 @@ https://canvas.best?apiKey={key}&baseUrl={address}
|
||||
- [漏洞提交](SECURITY.md)
|
||||
- [待办事项](docs/content/docs/progress/todo.mdx)
|
||||
- [本地 Canvas Agent](canvas-agent/README.md)
|
||||
- [Codex app 插件](plugins/infinite-canvas)
|
||||
|
||||
## 赞助支持
|
||||
|
||||
|
||||
+23
-1
@@ -1,6 +1,6 @@
|
||||
# Infinite Canvas Agent
|
||||
|
||||
本地 Canvas Agent 用来连接线上画布网页和用户电脑上的 Codex / Claude Code。
|
||||
本地 Canvas Agent 用来连接画布网页和用户电脑上的 Codex / Claude Code。本地开发时优先连接 `http://localhost:3000`,不需要先使用线上站点。
|
||||
|
||||
## 启动
|
||||
|
||||
@@ -26,6 +26,8 @@ Connect token: xxxxxx
|
||||
|
||||
在画布右上角点击 `Agent`,填入地址和 token 后连接。
|
||||
|
||||
Codex app 插件会读取启动输出里的 Local URL 和 Connect token,并直接打开画布网页地址;Canvas Agent 不负责生成画布打开 URL。
|
||||
|
||||
Canvas Agent 默认只监听 `127.0.0.1`。网页第一次带正确 token 连接后,Canvas Agent 会记录该网页 Origin;之后其他 Origin 不能复用这个本地 Agent,除非用户清理 `~/.infinite-canvas/canvas-agent.json` 里的 `origins`。
|
||||
|
||||
## 发布
|
||||
@@ -38,6 +40,26 @@ Canvas Agent 默认只监听 `127.0.0.1`。网页第一次带正确 token 连接
|
||||
|
||||
如果希望 Codex 终端能直接操作画布,需要先把 Canvas Agent 注册成 Codex MCP。
|
||||
|
||||
### Codex app 插件
|
||||
|
||||
仓库内提供了 Codex app 插件:`plugins/infinite-canvas`。在 Codex app 中添加本仓库的 marketplace 后,可以安装 `Infinite Canvas` 插件;插件会注册同一个 `infinite-canvas` MCP,并带上画布操作说明。
|
||||
|
||||
添加本地 marketplace 时建议使用仓库绝对路径,避免 Codex 从其他工作目录解析失败:
|
||||
|
||||
```bash
|
||||
cd /path/to/infinite-canvas
|
||||
codex plugin marketplace add "$(pwd)"
|
||||
codex plugin add infinite-canvas@infinite-canvas-local
|
||||
```
|
||||
|
||||
插件默认通过 npm 启动 MCP:
|
||||
|
||||
```bash
|
||||
npx -y @basketikun/canvas-agent mcp
|
||||
```
|
||||
|
||||
使用时可以直接在 Codex 里说“打开 Infinite Canvas”,插件会优先启动本地画布和本地 Agent,读取 Local URL 和 Connect token,然后直接打开画布网页地址新建并连接画布。如果自动连接失败,再检查本地画布服务和 Canvas Agent 是否都已启动。
|
||||
|
||||
Canvas Agent 启动后,给 Codex 添加 MCP:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -23,7 +23,7 @@ http://localhost:3000
|
||||
|
||||
## 2. 配置模型
|
||||
|
||||
打开右上角配置弹窗,填写自己的 `Base URL`、`API Key` 和模型名。第三方提示词由 Next.js route 拉取并缓存在运行实例内存中;WebDAV 可选择前端直连或 Next.js 转发。
|
||||
打开右上角配置弹窗,填写自己的 `Base URL`、`API Key` 和模型名。第三方提示词和 WebDAV 都由浏览器前端直连。
|
||||
|
||||
## 3. 启动文档站
|
||||
|
||||
@@ -36,5 +36,5 @@ bun run dev
|
||||
## 常见场景
|
||||
|
||||
- 改画布、页面和交互:主要看 `web/`
|
||||
- 改提示词缓存或 WebDAV 代理:主要看 `web/src/app/api/` 和 `web/src/app/webdav-proxy/`
|
||||
- 改提示词缓存:主要看 `web/src/services/api/prompts.ts`
|
||||
- 改文档站内容:主要看 `docs/content/docs/`
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: Codex App 插件
|
||||
description: 在 Codex App 中安装并使用 Infinite Canvas 插件
|
||||
---
|
||||
|
||||
# Codex App 插件
|
||||
|
||||
Infinite Canvas 提供了一个 Codex App 插件,用来把 Codex 和本地 Canvas Agent 连接起来。安装后,你可以直接在 Codex 中说“打开 Infinite Canvas”,让 Codex 打开画布、读取节点、创建节点并触发生成流程。
|
||||
|
||||
插件源码位于仓库的 `plugins/infinite-canvas/` 目录。
|
||||
|
||||
## 安装方式
|
||||
|
||||
### 让 Codex 自动安装
|
||||
|
||||
把下面这段发给 Codex:
|
||||
|
||||
```text
|
||||
请从 https://github.com/basketikun/infinite-canvas.git 安装 Infinite Canvas Codex 插件。
|
||||
请 clone 仓库到 ~/plugins/infinite-canvas,确认 plugins/infinite-canvas/.codex-plugin/plugin.json 存在,
|
||||
把 plugins/infinite-canvas 加入 personal marketplace,先运行 codex plugin marketplace add ~,
|
||||
再运行 codex plugin add infinite-canvas@personal。
|
||||
安装后请校验插件,并告诉我是否需要开启一个新对话来加载新技能和 MCP 工具。
|
||||
```
|
||||
|
||||
这种方式适合不想手动改 marketplace 配置的用户。Codex 会完成 clone、写入 marketplace、安装插件和基础校验。
|
||||
|
||||
### 手动安装
|
||||
|
||||
推荐把仓库 clone 到 Codex personal marketplace 默认会引用的位置:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/plugins
|
||||
git clone https://github.com/basketikun/infinite-canvas.git ~/plugins/infinite-canvas
|
||||
```
|
||||
|
||||
确保 `~/.agents/plugins/marketplace.json` 中有 Infinite Canvas 条目。这里的 `path` 要指向仓库里的插件子目录:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "personal",
|
||||
"interface": {
|
||||
"displayName": "Personal"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "infinite-canvas",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/infinite-canvas/plugins/infinite-canvas"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
然后注册 personal marketplace 并安装插件:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace add ~
|
||||
codex plugin add infinite-canvas@personal
|
||||
```
|
||||
|
||||
安装完成后,建议新建一个 Codex 对话,让新的 skill 和 MCP 工具完整加载。
|
||||
|
||||
### 本仓库开发调试安装
|
||||
|
||||
如果你正在 Infinite Canvas 仓库中开发插件,可以直接使用仓库自带的 marketplace:
|
||||
|
||||
```bash
|
||||
cd /path/to/infinite-canvas
|
||||
codex plugin marketplace add "$(pwd)"
|
||||
codex plugin add infinite-canvas@infinite-canvas-local
|
||||
```
|
||||
|
||||
仓库内的 `.agents/plugins/marketplace.json` 已经指向 `./plugins/infinite-canvas`,适合开发者本地调试。
|
||||
|
||||
## 使用插件
|
||||
|
||||
新建 Codex 对话后,可以直接说:
|
||||
|
||||
```text
|
||||
打开 Infinite Canvas 新建画布 并连接好Codex
|
||||
```
|
||||
|
||||
插件会优先确认当前仓库的本地画布服务是否已经运行。如果端口被占用,它会检查监听进程是否属于当前项目,避免误连到其他项目。确认或启动画布后,插件会启动本地 Canvas Agent,并打开带连接参数的新建画布 URL。
|
||||
|
||||
画布打开后可以继续让 Codex 操作画布:
|
||||
|
||||
```text
|
||||
读取当前画布并总结节点结构
|
||||
根据选中节点创建一组生图提示词
|
||||
把这些节点整理成一个生成流程
|
||||
```
|
||||
|
||||
## 更新插件
|
||||
|
||||
普通业务代码变更通常不需要更新插件。只有改了下面这些内容时,才建议重新安装或刷新插件:
|
||||
|
||||
- `plugins/infinite-canvas/.codex-plugin/plugin.json`
|
||||
- `plugins/infinite-canvas/.mcp.json`
|
||||
- `plugins/infinite-canvas/skills/` 下的技能说明
|
||||
- 插件暴露的 MCP、app 或入口脚本
|
||||
|
||||
重新安装后建议开启新对话,避免旧对话继续使用缓存中的技能和 MCP 工具。
|
||||
|
||||
## 手动排查
|
||||
|
||||
插件默认通过下面的 MCP 命令启动 Canvas Agent:
|
||||
|
||||
```bash
|
||||
npx -y @basketikun/canvas-agent mcp
|
||||
```
|
||||
|
||||
如果需要手动排查,先启动画布:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
然后启动本地 Agent。端口不是 `3000` 时,把 `CANVAS_URL` 换成真实本地画布地址:
|
||||
|
||||
```bash
|
||||
CANVAS_URL=http://localhost:3000 npx -y @basketikun/canvas-agent
|
||||
```
|
||||
|
||||
从 Agent 输出或 `http://127.0.0.1:17371/config` 读取本地地址和 token 后,打开:
|
||||
|
||||
```text
|
||||
<画布网页地址>/canvas?mode=new&agentUrl=<Local URL>&agentToken=<Connect token>
|
||||
```
|
||||
|
||||
`mode=new` 会让网页自动创建具体画布并连接本地 Agent,不需要手动点击“新建画布”。
|
||||
@@ -49,4 +49,4 @@ docker compose -f docker-compose.local.yml up -d --build
|
||||
|
||||
## 数据说明
|
||||
|
||||
当前主应用镜像只启动 Next.js。画布、我的素材、生成记录和 AI API Key 默认保存在浏览器本地;第三方提示词由 Next.js route 拉取后缓存在运行实例内存里,不需要额外挂载数据目录。
|
||||
当前主应用镜像只启动 Next.js。画布、我的素材、生成记录和 AI API Key 默认保存在浏览器本地;第三方提示词由浏览器前端直连拉取,不需要额外挂载数据目录。
|
||||
|
||||
@@ -125,7 +125,7 @@ Base URL 如果已经以 `/v1`、`/api/v3` 或 `/api/plan/v3` 结尾,系统不
|
||||
- 查看远程提示词源。
|
||||
- 触发读取内置远程提示词源。
|
||||
|
||||
当前内置远程源包括多个 GPT Image / GPT-4o / Nano Banana Pro 相关提示词仓库,由 Next.js route 拉取并缓存在当前运行实例内存中。
|
||||
当前内置远程源包括多个 GPT Image / GPT-4o / Nano Banana Pro 相关提示词仓库,由浏览器前端直连拉取并缓存到 IndexedDB。
|
||||
|
||||
## 素材
|
||||
|
||||
@@ -146,7 +146,7 @@ Base URL 如果已经以 `/v1`、`/api/v3` 或 `/api/plan/v3` 结尾,系统不
|
||||
|
||||
- 当前版本不需要账号登录,也不再提供后台管理页面。
|
||||
- 配置与用户偏好弹窗支持多个 OpenAI 兼容渠道、默认模型、生成偏好和 WebDAV 同步设置。
|
||||
- 第三方提示词和 WebDAV 可使用少量 Next.js route,AI 接口不经过项目后端代理。
|
||||
- 第三方提示词、WebDAV 和 AI 接口都由浏览器前端直连,不经过项目后端代理。
|
||||
|
||||
## 当前限制
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"defaultOpen": true,
|
||||
"pages": [
|
||||
"quick-start",
|
||||
"codex-app-plugin",
|
||||
"features",
|
||||
"render",
|
||||
"docker",
|
||||
|
||||
@@ -39,7 +39,7 @@ docker run --rm -p 3000:3000 infinite-canvas
|
||||
## 首次使用建议
|
||||
|
||||
- 先打开右上角配置弹窗,填入自己的 `Base URL`、`API Key` 和模型名。
|
||||
- 如果需要提示词仓库内容,打开 `/prompts` 会通过 Next.js route 拉取并缓存在内存中。
|
||||
- 如果需要提示词仓库内容,打开 `/prompts` 会由浏览器前端直连拉取并缓存到 IndexedDB。
|
||||
- 如果需要跨设备同步画布、素材和生成记录,可在配置弹窗中填写 WebDAV。
|
||||
|
||||
## 说明
|
||||
|
||||
@@ -22,7 +22,7 @@ description: 使用 Render 部署无限画布
|
||||
默认使用 Render 免费 Web Service:
|
||||
|
||||
- 空闲约 15 分钟后会休眠,下次访问会自动唤醒。
|
||||
- 当前主应用数据默认保存在浏览器本地;第三方提示词缓存会随 Render 实例重启而清空,下次访问会重新拉取。
|
||||
- 当前主应用数据默认保存在浏览器本地;第三方提示词由浏览器前端直连拉取。
|
||||
- 适合体验和演示,不适合长期保存正式数据。
|
||||
|
||||
长期使用建议优先部署到 Vercel,或自行配置 WebDAV 同步浏览器本地数据。
|
||||
|
||||
@@ -5,3 +5,15 @@ description: 当前版本已实现但仍需人工验证的变更项
|
||||
|
||||
# 待测试
|
||||
|
||||
- Codex App 插件文档:新增插件安装教程,区分 AI 自动安装、手动安装和本仓库开发调试安装;同步优化插件 README 与技能说明。
|
||||
- 配置与用户偏好:每个模型渠道新增 OpenAI / Gemini 调用格式选择,默认 OpenAI,切换默认格式时同步更新默认 Base URL;渠道 Tab 增加紧凑醒目的提醒,说明模型是否可选需要到“模型”Tab 选择。
|
||||
- Gemini 调用格式:支持拉取 Gemini 模型列表,并用于文本问答、在线 Agent 工具调用、基础生图和图生图请求。
|
||||
- 音频和视频生成:选择 Gemini 调用格式时先给出不支持提示,仍需使用 OpenAI 兼容渠道。
|
||||
- WebDAV 同步:移除 Next.js 转发代理和连接方式选择,改为浏览器前端直接连接 WebDAV 服务。
|
||||
- 提示词库:移除 Next.js route 抓取和服务端内存缓存,改为浏览器前端直连 GitHub 原始文件并缓存到 IndexedDB。
|
||||
- 提示词库:移除已 404 的 `EvoLinkAI/awesome-gpt-image-2-API-and-Prompts` 来源。
|
||||
- 前端构建:移除 Next.js,改为 Vite + React Router,并将前端目录调整为 `pages` / `components` / `stores` / `lib` / `layouts` / `styles`,需验证首页、图片/视频工作台、素材、提示词库和画布路由跳转。
|
||||
- 前端构建:修复 TypeScript 严格检查下的构建报错。
|
||||
- Vercel 部署:新增 `web` 和 `docs` 两个子项目的 Vercel 配置,需分别验证静态前端和文档站部署。
|
||||
- Docker 部署:运行镜像从 Node serve 改为 nginx 静态托管,需验证前端路由刷新回退。
|
||||
- 环境变量:移除过期的后端 `.env.example` 示例文件。
|
||||
|
||||
@@ -25,11 +25,10 @@ description: 安全漏洞提交和负责任披露说明
|
||||
- 清晰的复现步骤。
|
||||
- 漏洞影响、攻击场景和触发条件。
|
||||
- 已脱敏的日志、截图或 PoC。
|
||||
- 是否涉及浏览器本地存储、API Key、WebDAV 同步、AI 渠道配置或代理接口。
|
||||
- 是否涉及浏览器本地存储、API Key、WebDAV 同步、AI 渠道配置或接口请求。
|
||||
|
||||
## 范围说明
|
||||
|
||||
项目会优先处理由本仓库代码或默认配置导致的 XSS、敏感信息泄露、文件处理、导入导出、WebDAV 代理、权限控制和供应链风险。
|
||||
项目会优先处理由本仓库代码或默认配置导致的 XSS、敏感信息泄露、文件处理、导入导出、WebDAV 同步、权限控制和供应链风险。
|
||||
|
||||
第三方模型服务、托管平台、浏览器插件、用户自行泄露的 API Key、没有实际利用路径的安全头建议、纯社工钓鱼或账号找回问题通常不属于本项目漏洞范围。
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"framework": "nextjs",
|
||||
"installCommand": "bun install",
|
||||
"buildCommand": "bun run build"
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
server {
|
||||
listen 3000;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "infinite-canvas",
|
||||
"version": "0.1.0",
|
||||
"description": "Use Codex to read and operate Infinite Canvas through the local Canvas Agent MCP server.",
|
||||
"author": {
|
||||
"name": "Infinite Canvas"
|
||||
},
|
||||
"repository": "https://github.com/basketikun/infinite-canvas",
|
||||
"license": "AGPL-3.0",
|
||||
"keywords": ["infinite-canvas", "canvas", "mcp", "agent"],
|
||||
"skills": "./skills/",
|
||||
"mcpServers": "./.mcp.json",
|
||||
"interface": {
|
||||
"displayName": "Infinite Canvas",
|
||||
"shortDescription": "Use Codex to operate the current Infinite Canvas board.",
|
||||
"longDescription": "Connects Codex to the local Canvas Agent MCP server so Codex can inspect the current Infinite Canvas board, create nodes, connect flows, and trigger generation through the web canvas confirmation flow.",
|
||||
"developerName": "Infinite Canvas",
|
||||
"category": "Productivity",
|
||||
"capabilities": ["Canvas", "MCP", "Creative Workflow"],
|
||||
"defaultPrompt": [
|
||||
"打开 Infinite Canvas",
|
||||
"读取当前画布并总结节点结构",
|
||||
"根据选中节点创建一组生图提示词"
|
||||
],
|
||||
"brandColor": "#2563EB",
|
||||
"composerIcon": "./assets/icon.png",
|
||||
"logo": "./assets/icon.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"infinite-canvas": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@basketikun/canvas-agent", "mcp"],
|
||||
"startup_timeout_sec": 20,
|
||||
"tool_timeout_sec": 90
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
# Infinite Canvas Codex Plugin
|
||||
|
||||
这个插件把 Infinite Canvas 的本地 Canvas Agent MCP 打包给 Codex app 使用,让 Codex 能打开本地画布、读取当前节点、创建内容并触发生成流程。
|
||||
|
||||
## 安装
|
||||
|
||||
### AI 自动安装
|
||||
|
||||
把下面这段发给 Codex:
|
||||
|
||||
```text
|
||||
请从 https://github.com/basketikun/infinite-canvas.git 安装 Infinite Canvas Codex 插件。
|
||||
请 clone 仓库到 ~/plugins/infinite-canvas,确认 plugins/infinite-canvas/.codex-plugin/plugin.json 存在,
|
||||
把 plugins/infinite-canvas 加入 personal marketplace,先运行 codex plugin marketplace add ~,
|
||||
再运行 codex plugin add infinite-canvas@personal。
|
||||
安装后请校验插件,并告诉我是否需要开启一个新对话来加载新技能和 MCP 工具。
|
||||
```
|
||||
|
||||
### 手动安装
|
||||
|
||||
推荐把仓库 clone 到 Codex personal marketplace 默认会引用的位置:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/plugins
|
||||
git clone https://github.com/basketikun/infinite-canvas.git ~/plugins/infinite-canvas
|
||||
```
|
||||
|
||||
确保 `~/.agents/plugins/marketplace.json` 中有 Infinite Canvas 条目,注意 `path` 指向仓库里的插件子目录:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "personal",
|
||||
"interface": {
|
||||
"displayName": "Personal"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "infinite-canvas",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/infinite-canvas/plugins/infinite-canvas"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
然后注册 personal marketplace 并安装插件:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace add ~
|
||||
codex plugin add infinite-canvas@personal
|
||||
```
|
||||
|
||||
安装后建议开启一个新的 Codex 对话,让新的 skill 和 MCP 工具完整加载。
|
||||
|
||||
### 本仓库开发调试
|
||||
|
||||
如果你就在 Infinite Canvas 仓库中调试插件,可以直接添加仓库自带 marketplace。建议使用仓库绝对路径,避免 Codex 从其他工作目录解析失败:
|
||||
|
||||
```bash
|
||||
cd /path/to/infinite-canvas
|
||||
codex plugin marketplace add "$(pwd)"
|
||||
codex plugin add infinite-canvas@infinite-canvas-local
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
1. 新建 Codex 线程后说“打开 Infinite Canvas”。
|
||||
2. 插件会确认当前仓库的本地画布服务是否已运行;端口被占用时会检查进程归属,不会把其他项目的 `3000` 当作 Infinite Canvas。
|
||||
3. 确认或启动后,插件会直接打开新建画布 URL,并自动尝试连接本地 Agent。
|
||||
4. 画布打开后,让 Codex 读取或操作当前画布。
|
||||
|
||||
常用提示:
|
||||
|
||||
```text
|
||||
打开 Infinite Canvas
|
||||
读取当前画布并总结节点结构
|
||||
根据选中节点创建一组生图提示词
|
||||
```
|
||||
|
||||
## 工作机制
|
||||
|
||||
插件默认通过以下命令启动 MCP,并会在 MCP 启动时自动尝试拉起本地 Agent:
|
||||
|
||||
```bash
|
||||
npx -y @basketikun/canvas-agent mcp
|
||||
```
|
||||
|
||||
## 手动排查
|
||||
|
||||
优先本地启动画布:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
然后启动本地 Agent。端口不是 `3000` 时,把 `CANVAS_URL` 换成真实本地画布地址:
|
||||
|
||||
```bash
|
||||
CANVAS_URL=http://localhost:3000 npx -y @basketikun/canvas-agent
|
||||
```
|
||||
|
||||
手动排查时先从 Agent 输出或 `http://127.0.0.1:17371/config` 读取本地地址和 token,然后直接打开 `<画布网页地址>/canvas?mode=new&agentUrl=<Local URL>&agentToken=<Connect token>`。不要通过页面点击来新建画布;`mode=new` 会让网页自动创建具体画布并连接本地 Agent。
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: canvas
|
||||
description: 操作 Infinite Canvas 当前网页画布,读取节点、选区、创建文本节点、创建生成流程、连接节点或触发生成。
|
||||
---
|
||||
|
||||
# Infinite Canvas
|
||||
|
||||
你正在帮助用户操作 Infinite Canvas 网页画布。需要理解或改动画布时,优先使用已配置的 `infinite-canvas` MCP 工具;不要让用户手动复制 JSON、URL 或 token。
|
||||
|
||||
## 工作流
|
||||
|
||||
- 如果用户还没有打开或连接网页画布,使用 `open-canvas` 技能打开 Infinite Canvas,不要要求用户手动复制 URL 或 token。
|
||||
- 操作前先用 `canvas_get_state` 读取当前画布;如果用户明确提到选中内容、当前节点或“这个”,先用 `canvas_get_selection`。
|
||||
- 创建单个文本内容优先用 `canvas_create_text_node`。
|
||||
- 创建生成内容优先用 `canvas_generate_text`、`canvas_generate_image`、`canvas_generate_video`、`canvas_generate_audio`。
|
||||
- 需要把提示词、配置和生成节点串成流程时,使用 `canvas_create_generation_flow` 或项目已有的流程工具。
|
||||
- 需要批量增删改、移动、连接节点或设置视口时,使用 `canvas_apply_ops`。
|
||||
- 不要模拟鼠标点击,不要要求用户手动复制 JSON。
|
||||
- 写入画布的操作会由网页侧边栏做二次确认,按当前工具结果继续推进即可。
|
||||
|
||||
## 风格
|
||||
|
||||
- 页面文案和画布节点内容默认使用中文。
|
||||
- 生成节点、配置节点和提示词节点要保持结构清晰,方便用户继续编辑。
|
||||
- 批量创建节点时注意给节点留出间距,不要堆叠在同一个位置。
|
||||
- 图片、视频、音频等媒体节点默认保留原始比例;只有用户明确要求自由变形时才改变比例。
|
||||
- 生成流程尽量少而清楚,优先让用户一眼能看懂节点关系。
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: open-canvas
|
||||
description: 打开 Infinite Canvas 网页画布并自动连接本地 Canvas Agent。用户要求打开、启动、进入、使用 Infinite Canvas 或画布时使用。
|
||||
---
|
||||
|
||||
# Open Infinite Canvas
|
||||
|
||||
当用户要求打开、启动、进入或使用 Infinite Canvas 时,不要把 URL 交给用户手动复制,不要通过浏览器点击“新建画布”。优先快速拉起本地画布和本地 Canvas Agent,然后直接打开带 `mode`、`agentUrl`、`agentToken` 的 URL,让网页自动创建或选择画布并连接 Agent。
|
||||
|
||||
## 默认打开方式
|
||||
|
||||
- 新建画布:`<画布网页地址>/canvas?mode=new&agentUrl=<Local URL>&agentToken=<Connect token>`
|
||||
- 最近画布:`<画布网页地址>/canvas?mode=recent&agentUrl=<Local URL>&agentToken=<Connect token>`
|
||||
- 自己选择:`<画布网页地址>/canvas?mode=choose&agentUrl=<Local URL>&agentToken=<Connect token>`
|
||||
|
||||
默认打开新建本地画布;只有用户明确要求线上地址、最近画布或自己选择时,才改用对应模式。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 如果当前仓库是 Infinite Canvas 项目,优先使用当前仓库的 `web/` 前端。
|
||||
2. 先检查本地端口归属:如果 `3000`、`3001` 等端口已被占用,必须用 `lsof`/`ps` 或服务输出确认监听进程的工作目录属于当前仓库的 `web/`,不能只因为端口存在就当成本地画布。
|
||||
3. 如果已有当前仓库的 Next dev 服务,复用它并记录真实画布地址,例如 `http://localhost:3001`。
|
||||
4. 如果没有当前仓库的服务,启动本地画布开发服务,默认在 `web/` 下运行 `bun run dev`;若默认端口被其他项目占用,改用空闲端口启动,例如 `bunx next dev --webpack -H 0.0.0.0 -p <空闲端口>`。不要执行构建或测试。
|
||||
5. 启动本地 Canvas Agent,必须带上第 3/4 步得到的真实画布地址:`CANVAS_URL=<真实画布地址> npx -y @basketikun/canvas-agent`。如果 Agent 已经在运行,则读取 `~/.infinite-canvas/canvas-agent.json` 或 `/config` 获取 `Local URL` 和 token。
|
||||
6. 读取 Agent 输出或配置中的 `Local URL` 和 `Connect token`,不要让用户手动复制。
|
||||
7. 不走本地 Agent 的 `/open` 跳转;直接构造并打开最终 URL:`<真实画布地址>/canvas?mode=new&agentUrl=<Local URL>&agentToken=<Connect token>`。
|
||||
8. 画布网页会自动新建具体画布、打开本机 Agent 面板并连接本地 Agent;不要用浏览器点击新建画布。
|
||||
9. 打开后再使用 `canvas_get_state` 检查画布是否已经连接;如果尚未连接,等待片刻再检查,不要改用线上站点,除非用户明确要求。
|
||||
|
||||
## 用户只安装插件时
|
||||
|
||||
- 如果当前工作区不是 Infinite Canvas 源码仓库,优先提示用户先打开或启动 Infinite Canvas 网页,再连接本地 Agent。
|
||||
- 可以使用线上画布地址或用户给出的本地地址作为 `<画布网页地址>`,但仍要通过本地 Canvas Agent 获取 token 后再打开最终 URL。
|
||||
- 不要假设用户已经安装本仓库依赖;插件的 MCP 会通过 `npx -y @basketikun/canvas-agent mcp` 使用已发布的 Canvas Agent。
|
||||
|
||||
不要要求用户手动填写 URL、token 或复制 JSON。
|
||||
+156
-96
@@ -6,7 +6,6 @@
|
||||
"name": "infinite-canvas",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.1",
|
||||
"@ant-design/nextjs-registry": "^1.3.0",
|
||||
"@ant-design/pro-components": "3.0.0-beta.3",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@tanstack/react-query": "^5.100.9",
|
||||
@@ -23,10 +22,11 @@
|
||||
"lucide-react": "^1.16.0",
|
||||
"motion": "^12.38.0",
|
||||
"nanoid": "^5.1.11",
|
||||
"next": "16.2.3",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.5",
|
||||
"react-dom": "19.2.5",
|
||||
"react-router": "^7.12.0",
|
||||
"react-router-dom": "^7.12.0",
|
||||
"shadcn": "^4.7.0",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4",
|
||||
@@ -39,8 +39,10 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "^5",
|
||||
"vite": "^7.3.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -49,7 +51,7 @@
|
||||
|
||||
"@ant-design/colors": ["@ant-design/colors@8.0.1", "https://registry.npmmirror.com/@ant-design/colors/-/colors-8.0.1.tgz", { "dependencies": { "@ant-design/fast-color": "^3.0.0" } }, "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ=="],
|
||||
|
||||
"@ant-design/cssinjs": ["@ant-design/cssinjs@2.1.2", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ=="],
|
||||
"@ant-design/cssinjs": ["@ant-design/cssinjs@1.24.0", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg=="],
|
||||
|
||||
"@ant-design/cssinjs-utils": ["@ant-design/cssinjs-utils@2.1.2", "https://registry.npmmirror.com/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@babel/runtime": "^7.23.2", "@rc-component/util": "^1.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA=="],
|
||||
|
||||
@@ -59,8 +61,6 @@
|
||||
|
||||
"@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="],
|
||||
|
||||
"@ant-design/nextjs-registry": ["@ant-design/nextjs-registry@1.3.0", "", { "peerDependencies": { "@ant-design/cssinjs": ">=1.0.0", "antd": ">=5.0.0", "next": ">=14.0.0", "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-lHlg5bnj3LPYHUD+iZsO1k11NhIrx1mLWh2DXQ0m3Ly3OSaYfgeHX0WoVXxdVjEGjjYnPTe+OUY7YvU/p1aPZw=="],
|
||||
|
||||
"@ant-design/pro-components": ["@ant-design/pro-components@3.0.0-beta.3", "https://registry.npmmirror.com/@ant-design/pro-components/-/pro-components-3.0.0-beta.3.tgz", { "dependencies": { "@ant-design/cssinjs": "^1.24.0", "@ant-design/icons": "^5.6.1", "@babel/runtime": "^7.27.6", "@ctrl/tinycolor": "^4.1.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.2", "@emotion/cache": "^11.14.0", "@emotion/css": "^11.13.5", "@emotion/serialize": "^1.3.3", "@rc-component/util": "^1.2.2", "@umijs/route-utils": "^4.0.1", "@umijs/use-params": "^1.0.9", "classnames": "^2.5.1", "dayjs": "^1.11.13", "lodash-es": "^4.17.21", "path-to-regexp": "^6.3.0", "rc-field-form": "^2.7.0", "rc-footer": "^0.6.8", "rc-resize-observer": "^1.4.3", "rc-steps": "^6.0.1", "rc-table": "^7.51.1", "react-draggable": "^4.5.0", "react-layout-kit": "^1.9.2", "react-lazy-load": "^4.0.1", "react-markdown": "^8.0.7", "react-syntax-highlighter": "^15.6.1", "safe-stable-stringify": "^2.5.0", "shiki-es": "^0.2.0", "swr": "^2.3.4" }, "peerDependencies": { "antd": "^5.11.2", "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-ehdIGlDcy4J5kv6E3fZ8e4YLQY4+7S3ZsIZnZ//vtfd+s0Qkv13hQ65CepoMiC/x6gNBlDnHozKANNi7JOtJAw=="],
|
||||
|
||||
"@ant-design/react-slick": ["@ant-design/react-slick@2.0.0", "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-2.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.4", "clsx": "^2.1.1", "json2mq": "^0.2.0", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg=="],
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
|
||||
|
||||
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="],
|
||||
|
||||
@@ -111,6 +111,10 @@
|
||||
|
||||
"@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
|
||||
|
||||
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="],
|
||||
|
||||
"@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "https://registry.npmmirror.com/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="],
|
||||
@@ -157,8 +161,6 @@
|
||||
|
||||
"@ecies/ciphers": ["@ecies/ciphers@0.2.6", "https://registry.npmmirror.com/@ecies/ciphers/-/ciphers-0.2.6.tgz", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
|
||||
"@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "https://registry.npmmirror.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="],
|
||||
|
||||
"@emotion/cache": ["@emotion/cache@11.14.0", "https://registry.npmmirror.com/@emotion/cache/-/cache-11.14.0.tgz", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="],
|
||||
@@ -179,6 +181,58 @@
|
||||
|
||||
"@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "https://registry.npmmirror.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.5", "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
|
||||
@@ -189,56 +243,6 @@
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.14", "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.14.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||
|
||||
"@inquirer/ansi": ["@inquirer/ansi@2.0.5", "https://registry.npmmirror.com/@inquirer/ansi/-/ansi-2.0.5.tgz", {}, "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw=="],
|
||||
|
||||
"@inquirer/confirm": ["@inquirer/confirm@6.0.13", "https://registry.npmmirror.com/@inquirer/confirm/-/confirm-6.0.13.tgz", { "dependencies": { "@inquirer/core": "^11.1.10", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw=="],
|
||||
@@ -273,24 +277,6 @@
|
||||
|
||||
"@mswjs/interceptors": ["@mswjs/interceptors@0.41.9", "https://registry.npmmirror.com/@mswjs/interceptors/-/interceptors-0.41.9.tgz", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w=="],
|
||||
|
||||
"@next/env": ["@next/env@16.2.3", "", {}, "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA=="],
|
||||
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg=="],
|
||||
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ=="],
|
||||
|
||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q=="],
|
||||
|
||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw=="],
|
||||
|
||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ=="],
|
||||
|
||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw=="],
|
||||
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw=="],
|
||||
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@1.3.0", "https://registry.npmmirror.com/@noble/ciphers/-/ciphers-1.3.0.tgz", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
|
||||
|
||||
"@noble/curves": ["@noble/curves@1.9.7", "https://registry.npmmirror.com/@noble/curves/-/curves-1.9.7.tgz", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="],
|
||||
@@ -513,12 +499,62 @@
|
||||
|
||||
"@rc-component/virtual-list": ["@rc-component/virtual-list@1.0.2", "https://registry.npmmirror.com/@rc-component/virtual-list/-/virtual-list-1.0.2.tgz", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="],
|
||||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="],
|
||||
@@ -555,8 +591,18 @@
|
||||
|
||||
"@ts-morph/common": ["@ts-morph/common@0.27.0", "https://registry.npmmirror.com/@ts-morph/common/-/common-0.27.0.tgz", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.13", "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/file-saver": ["@types/file-saver@2.0.7", "", {}, "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A=="],
|
||||
|
||||
"@types/hast": ["@types/hast@2.3.10", "https://registry.npmmirror.com/@types/hast/-/hast-2.3.10.tgz", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="],
|
||||
@@ -591,6 +637,8 @@
|
||||
|
||||
"@umijs/use-params": ["@umijs/use-params@1.0.9", "https://registry.npmmirror.com/@umijs/use-params/-/use-params-1.0.9.tgz", { "peerDependencies": { "react": "*" } }, "sha512-QlN0RJSBVQBwLRNxbxjQ5qzqYIGn+K7USppMoIOVlf7fxXHsnQZ2bEsa6Pm74bt6DVQxpUE8HqvdStn6Y9FV1w=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
@@ -661,8 +709,6 @@
|
||||
|
||||
"cli-width": ["cli-width@4.1.0", "https://registry.npmmirror.com/cli-width/-/cli-width-4.1.0.tgz", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
|
||||
|
||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
@@ -765,6 +811,8 @@
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
@@ -835,6 +883,8 @@
|
||||
|
||||
"fs-extra": ["fs-extra@11.3.5", "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.5.tgz", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"fuzzysort": ["fuzzysort@3.1.0", "https://registry.npmmirror.com/fuzzysort/-/fuzzysort-3.1.0.tgz", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
|
||||
@@ -1115,8 +1165,6 @@
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"next": ["next@16.2.3", "", { "dependencies": { "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.3", "@next/swc-darwin-x64": "16.2.3", "@next/swc-linux-arm64-gnu": "16.2.3", "@next/swc-linux-arm64-musl": "16.2.3", "@next/swc-linux-x64-gnu": "16.2.3", "@next/swc-linux-x64-musl": "16.2.3", "@next/swc-win32-arm64-msvc": "16.2.3", "@next/swc-win32-x64-msvc": "16.2.3", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@3.3.2", "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
@@ -1229,10 +1277,16 @@
|
||||
|
||||
"react-markdown": ["react-markdown@8.0.7", "https://registry.npmmirror.com/react-markdown/-/react-markdown-8.0.7.tgz", { "dependencies": { "@types/hast": "^2.0.0", "@types/prop-types": "^15.0.0", "@types/unist": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^2.0.0", "prop-types": "^15.0.0", "property-information": "^6.0.0", "react-is": "^18.0.0", "remark-parse": "^10.0.0", "remark-rehype": "^10.0.0", "space-separated-tokens": "^2.0.0", "style-to-object": "^0.4.0", "unified": "^10.0.0", "unist-util-visit": "^4.0.0", "vfile": "^5.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.2", "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
|
||||
|
||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||
|
||||
"react-router": ["react-router@7.18.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ=="],
|
||||
|
||||
"react-router-dom": ["react-router-dom@7.18.0", "", { "dependencies": { "react-router": "7.18.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA=="],
|
||||
|
||||
"react-style-singleton": ["react-style-singleton@2.2.3", "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||
|
||||
"react-syntax-highlighter": ["react-syntax-highlighter@15.6.6", "https://registry.npmmirror.com/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", { "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^3.6.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw=="],
|
||||
@@ -1261,6 +1315,8 @@
|
||||
|
||||
"reusify": ["reusify@1.1.0", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="],
|
||||
|
||||
"router": ["router@2.2.0", "https://registry.npmmirror.com/router/-/router-2.2.0.tgz", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"run-applescript": ["run-applescript@7.1.0", "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
||||
@@ -1277,20 +1333,18 @@
|
||||
|
||||
"scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
|
||||
|
||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
"semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"send": ["send@1.2.1", "https://registry.npmmirror.com/send/-/send-1.2.1.tgz", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.1.tgz", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@3.1.0", "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
|
||||
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"shadcn": ["shadcn@4.7.0", "https://registry.npmmirror.com/shadcn/-/shadcn-4.7.0.tgz", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-70fwnesNrY1GgeD7Kdzn+3SsYeyfibm8immsA5L68+OusoPTvYF01oWExl8/latKpMpvVXcbgdbbE6VFBJQ38w=="],
|
||||
|
||||
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
@@ -1337,8 +1391,6 @@
|
||||
|
||||
"style-to-object": ["style-to-object@0.4.4", "https://registry.npmmirror.com/style-to-object/-/style-to-object-0.4.4.tgz", { "dependencies": { "inline-style-parser": "0.1.1" } }, "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg=="],
|
||||
|
||||
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||
|
||||
"stylis": ["stylis@4.4.0", "https://registry.npmmirror.com/stylis/-/stylis-4.4.0.tgz", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
@@ -1357,6 +1409,8 @@
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"tldts": ["tldts@7.0.30", "https://registry.npmmirror.com/tldts/-/tldts-7.0.30.tgz", { "dependencies": { "tldts-core": "^7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw=="],
|
||||
|
||||
"tldts-core": ["tldts-core@7.0.30", "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.0.30.tgz", {}, "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q=="],
|
||||
@@ -1429,6 +1483,8 @@
|
||||
|
||||
"vfile-message": ["vfile-message@3.1.4", "https://registry.npmmirror.com/vfile-message/-/vfile-message-3.1.4.tgz", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^3.0.0" } }, "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw=="],
|
||||
|
||||
"vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
|
||||
|
||||
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
@@ -1463,15 +1519,19 @@
|
||||
|
||||
"zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="],
|
||||
|
||||
"@ant-design/pro-components/@ant-design/cssinjs": ["@ant-design/cssinjs@1.24.0", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg=="],
|
||||
"@ant-design/cssinjs-utils/@ant-design/cssinjs": ["@ant-design/cssinjs@2.1.2", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ=="],
|
||||
|
||||
"@ant-design/pro-components/@ant-design/icons": ["@ant-design/icons@5.6.1", "https://registry.npmmirror.com/@ant-design/icons/-/icons-5.6.1.tgz", { "dependencies": { "@ant-design/colors": "^7.0.0", "@ant-design/icons-svg": "^4.4.0", "@babel/runtime": "^7.24.8", "classnames": "^2.2.6", "rc-util": "^5.31.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
"@babel/plugin-syntax-jsx/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
"@babel/plugin-syntax-typescript/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
"@babel/plugin-transform-modules-commonjs/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||
|
||||
"@babel/plugin-transform-typescript/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||
|
||||
"@babel/preset-typescript/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||
|
||||
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "https://registry.npmmirror.com/commander/-/commander-11.1.0.tgz", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
@@ -1517,6 +1577,8 @@
|
||||
|
||||
"accepts/mime-types": ["mime-types@3.0.2", "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"antd/@ant-design/cssinjs": ["@ant-design/cssinjs@2.1.2", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ=="],
|
||||
|
||||
"antd/@ant-design/icons": ["@ant-design/icons@6.2.3", "https://registry.npmmirror.com/@ant-design/icons/-/icons-6.2.3.tgz", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.4.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg=="],
|
||||
|
||||
"babel-plugin-macros/cosmiconfig": ["cosmiconfig@7.1.0", "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
|
||||
@@ -1539,12 +1601,12 @@
|
||||
|
||||
"hastscript/space-separated-tokens": ["space-separated-tokens@1.1.5", "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", {}, "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="],
|
||||
|
||||
"headers-polyfill/set-cookie-parser": ["set-cookie-parser@3.1.0", "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
|
||||
|
||||
"log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.2", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
@@ -1597,8 +1659,6 @@
|
||||
|
||||
"express/mime-types/mime-db": ["mime-db@1.54.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"send/mime-types/mime-db": ["mime-db@1.54.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"type-is/mime-types/mime-db": ["mime-db@1.54.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" class="font-sans">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="一个无限画布创作工具" />
|
||||
<link rel="icon" href="/logo.svg" />
|
||||
<title>无限画布</title>
|
||||
<script>
|
||||
try {
|
||||
var s = JSON.parse(localStorage.getItem("infinite-canvas:theme_store") || "{}");
|
||||
var t = s.state && s.state.theme === "light" ? "light" : "dark";
|
||||
document.documentElement.classList.toggle("dark", t === "dark");
|
||||
document.documentElement.style.colorScheme = t;
|
||||
} catch (e) {}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background text-foreground antialiased">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
import { PHASE_DEVELOPMENT_SERVER } from "next/constants";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { parseChangelog } from "@/lib/release";
|
||||
|
||||
const webDir = dirname(fileURLToPath(import.meta.url));
|
||||
const localVersion = readFileSync(resolve(webDir, "../VERSION"), "utf8").trim() || "dev";
|
||||
const localChangelog = readFileSync(resolve(webDir, "../CHANGELOG.md"), "utf8");
|
||||
|
||||
export default function nextConfig(phase: string): NextConfig {
|
||||
const isDev = phase === PHASE_DEVELOPMENT_SERVER;
|
||||
const releases = parseChangelog(localChangelog);
|
||||
|
||||
return {
|
||||
output: "standalone",
|
||||
allowedDevOrigins: isDev ? ["*.*.*.*"] : [],
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
env: {
|
||||
NEXT_PUBLIC_APP_VERSION: localVersion,
|
||||
NEXT_PUBLIC_APP_RELEASES: JSON.stringify(releases),
|
||||
},
|
||||
};
|
||||
}
|
||||
+8
-6
@@ -4,15 +4,14 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack -H 0.0.0.0 -p 3000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"dev": "vite --host 0.0.0.0 --port 3000",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"start": "vite preview --host 0.0.0.0 --port 3000",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.1",
|
||||
"@ant-design/nextjs-registry": "^1.3.0",
|
||||
"@ant-design/pro-components": "3.0.0-beta.3",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@tanstack/react-query": "^5.100.9",
|
||||
@@ -29,10 +28,11 @@
|
||||
"lucide-react": "^1.16.0",
|
||||
"motion": "^12.38.0",
|
||||
"nanoid": "^5.1.11",
|
||||
"next": "16.2.3",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.5",
|
||||
"react-dom": "19.2.5",
|
||||
"react-router": "^7.12.0",
|
||||
"react-router-dom": "^7.12.0",
|
||||
"shadcn": "^4.7.0",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4",
|
||||
@@ -41,11 +41,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5",
|
||||
"vite": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import CanvasClientPage from "./canvas-client-page";
|
||||
|
||||
export default function CanvasPage() {
|
||||
return <CanvasClientPage />;
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type Prompt = {
|
||||
id: string;
|
||||
title: string;
|
||||
coverUrl: string;
|
||||
prompt: string;
|
||||
tags: string[];
|
||||
category: string;
|
||||
githubUrl: string;
|
||||
preview: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type PromptCategory = {
|
||||
category: string;
|
||||
githubUrl: string;
|
||||
build: () => Promise<Omit<Prompt, "category" | "githubUrl">[]>;
|
||||
};
|
||||
|
||||
const gptImage2RawBase = "https://raw.githubusercontent.com/EvoLinkAI/awesome-gpt-image-2-API-and-Prompts/main";
|
||||
const awesomeGptImageRawBase = "https://raw.githubusercontent.com/ZeroLu/awesome-gpt-image/main";
|
||||
const awesomeGpt4oImagePromptsBase = "https://raw.githubusercontent.com/ImgEdify/Awesome-GPT4o-Image-Prompts/main";
|
||||
const youMindGptImage2RawBase = "https://raw.githubusercontent.com/YouMind-OpenLab/awesome-gpt-image-2/main";
|
||||
const youMindNanoBananaProRawBase = "https://raw.githubusercontent.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts/main";
|
||||
const davidWuGptImage2RawBase = "https://raw.githubusercontent.com/davidwuw0811-boop/awesome-gpt-image2-prompts/main";
|
||||
const gptImage2CaseFiles = ["README.md", "cases/ad-creative.md", "cases/character.md", "cases/comparison.md", "cases/ecommerce.md", "cases/portrait.md", "cases/poster.md", "cases/ui.md"];
|
||||
const cacheTtlMs = 1000 * 60 * 60;
|
||||
|
||||
const categories: PromptCategory[] = [
|
||||
{ category: "gpt-image-2-prompts", githubUrl: "https://github.com/EvoLinkAI/awesome-gpt-image-2-API-and-Prompts", build: buildGptImage2Prompts },
|
||||
{ category: "awesome-gpt-image", githubUrl: "https://github.com/ZeroLu/awesome-gpt-image", build: buildAwesomeGptImagePrompts },
|
||||
{ category: "awesome-gpt4o-image-prompts", githubUrl: "https://github.com/ImgEdify/Awesome-GPT4o-Image-Prompts", build: buildAwesomeGpt4oImagePrompts },
|
||||
{ category: "youmind-gpt-image-2", githubUrl: "https://github.com/YouMind-OpenLab/awesome-gpt-image-2", build: () => buildYouMindPrompts(youMindGptImage2RawBase, "youmind-gpt-image-2", "gpt-image-2") },
|
||||
{ category: "youmind-nano-banana-pro", githubUrl: "https://github.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts", build: () => buildYouMindPrompts(youMindNanoBananaProRawBase, "youmind-nano-banana-pro", "nano-banana-pro") },
|
||||
{ category: "davidwu-gpt-image2-prompts", githubUrl: "https://github.com/davidwuw0811-boop/awesome-gpt-image2-prompts", build: buildDavidWuGptImage2Prompts },
|
||||
];
|
||||
|
||||
let memoryCache: { items: Prompt[]; fetchedAt: number } | null = null;
|
||||
let loadingPrompts: Promise<Prompt[]> | null = null;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const params = request.nextUrl.searchParams;
|
||||
const keyword = (params.get("keyword") || "").trim().toLowerCase();
|
||||
const tags = params.getAll("tag").filter(Boolean);
|
||||
const category = params.get("category") || "";
|
||||
const page = Math.max(1, Number(params.get("page")) || 1);
|
||||
const pageSize = Math.max(1, Math.min(100, Number(params.get("pageSize")) || 20));
|
||||
const items = await getPrompts();
|
||||
const withoutTagFilter = filterPrompts(items, { keyword, category, tags: [] });
|
||||
const filtered = filterPrompts(items, { keyword, category, tags });
|
||||
|
||||
return Response.json({
|
||||
items: filtered.slice((page - 1) * pageSize, page * pageSize),
|
||||
tags: collectTags(withoutTagFilter),
|
||||
categories: categories.map((item) => item.category),
|
||||
total: filtered.length,
|
||||
});
|
||||
}
|
||||
|
||||
async function getPrompts() {
|
||||
if (memoryCache && Date.now() - memoryCache.fetchedAt < cacheTtlMs) return memoryCache.items;
|
||||
if (loadingPrompts) return loadingPrompts;
|
||||
loadingPrompts = loadPrompts().finally(() => {
|
||||
loadingPrompts = null;
|
||||
});
|
||||
return loadingPrompts;
|
||||
}
|
||||
|
||||
async function loadPrompts() {
|
||||
const settled = await Promise.all(
|
||||
categories.map(async (category) => {
|
||||
try {
|
||||
const items = await category.build();
|
||||
return items.map((item) => ({ ...item, category: category.category, githubUrl: category.githubUrl }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
);
|
||||
const items = settled.flat();
|
||||
memoryCache = { items, fetchedAt: Date.now() };
|
||||
return items;
|
||||
}
|
||||
|
||||
function filterPrompts(items: Prompt[], options: { keyword: string; category: string; tags: string[] }) {
|
||||
return items.filter((item) => {
|
||||
if (isActiveOption(options.category) && item.category !== options.category) return false;
|
||||
if (options.tags.length && !options.tags.some((tag) => item.tags.includes(tag))) return false;
|
||||
if (!options.keyword) return true;
|
||||
return [item.title, item.prompt, item.category, ...item.tags].join(" ").toLowerCase().includes(options.keyword);
|
||||
});
|
||||
}
|
||||
|
||||
async function buildGptImage2Prompts() {
|
||||
const data = (await fetchJson<{ records?: Array<{ title?: string; tweet_url?: string; image_dir?: string; category?: string; added_at?: string }> }>(gptImage2RawBase, "data/ingested_tweets.json")).records || [];
|
||||
const cases = new Map<string, string>();
|
||||
const markdowns = await Promise.all(gptImage2CaseFiles.map((file) => fetchText(gptImage2RawBase, file)));
|
||||
markdowns.forEach((markdown) => collectGptImage2Cases(cases, markdown));
|
||||
const items: Omit<Prompt, "category" | "githubUrl">[] = [];
|
||||
data.forEach((item) => {
|
||||
const prompt = cases.get(item.tweet_url || "");
|
||||
if (!item.title || !prompt || !item.image_dir) return;
|
||||
const image = `${gptImage2RawBase}/${item.image_dir}/output.jpg`;
|
||||
items.push({ id: `gpt-image-2-prompts-${leftPad(items.length + 1)}`, title: item.title, coverUrl: image, prompt, tags: tagsFromCategory(item.category || ""), preview: markdownPreview([image]), createdAt: item.added_at || "", updatedAt: item.added_at || "" });
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
function collectGptImage2Cases(cases: Map<string, string>, markdown: string) {
|
||||
for (const match of markdown.matchAll(/### Case \d+: \[[^\]]+]\(([^)]+)\).*?\*\*Prompt:\*\*\s*\r?\n\s*```[\w-]*\r?\n(.*?)\r?\n```/gs)) {
|
||||
cases.set(match[1], match[2].trim());
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAwesomeGptImagePrompts() {
|
||||
const markdown = await fetchText(awesomeGptImageRawBase, "README.zh-CN.md");
|
||||
const items: Omit<Prompt, "category" | "githubUrl">[] = [];
|
||||
for (const section of splitBeforeHeading(markdown, "## ")) {
|
||||
const tags = tagsFromHeading(firstMatch(section, /^##\s+(.+)$/m));
|
||||
for (const block of splitBeforeHeading(section, "### ")) {
|
||||
const title = firstMatch(block, /^###\s+(.+)$/m).replace(/\[([^\]]+)]\([^)]+\)/g, "$1").trim();
|
||||
const prompt = firstMatch(block, /\*\*提示词:\*\*\s*\r?\n\s*```[\w-]*\r?\n(.*?)\r?\n```/s).trim();
|
||||
if (!title || !prompt) continue;
|
||||
const images = extractMarkdownImages(awesomeGptImageRawBase, block);
|
||||
items.push(defaultPrompt(`awesome-gpt-image-${leftPad(items.length + 1)}`, title, prompt, images[0] || "", tags, markdownPreview(images)));
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async function buildAwesomeGpt4oImagePrompts() {
|
||||
const markdown = await fetchText(awesomeGpt4oImagePromptsBase, "README.zh-CN.md");
|
||||
const items: Omit<Prompt, "category" | "githubUrl">[] = [];
|
||||
for (const block of splitBeforeHeading(markdown, "### ")) {
|
||||
const title = firstMatch(block, /^###\s+(.+)$/m).trim();
|
||||
const prompt = firstMatch(block, /- \*\*提示词文本:\*\*\s*`(.*?)`/s).trim();
|
||||
if (!title || !prompt) continue;
|
||||
const images = extractMarkdownImages(awesomeGpt4oImagePromptsBase, block);
|
||||
items.push(defaultPrompt(`awesome-gpt4o-image-prompts-${leftPad(items.length + 1)}`, title, prompt, images[0] || "", ["gpt4o"], markdownPreview(images)));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async function buildYouMindPrompts(baseUrl: string, idPrefix: string, modelTag: string) {
|
||||
const markdown = await fetchText(baseUrl, "README_zh.md");
|
||||
const items: Omit<Prompt, "category" | "githubUrl">[] = [];
|
||||
for (const block of splitBeforeHeading(markdown, "### ")) {
|
||||
const title = firstMatch(block, /^###\s+No\.\s*\d+:\s*(.+)$/m).trim();
|
||||
const prompt = firstMatch(block, /#### .*?提示词\s*\r?\n\s*```[\w-]*\r?\n(.*?)\r?\n```/s).trim();
|
||||
if (!title || !prompt) continue;
|
||||
const images = extractMarkdownImages(baseUrl, block);
|
||||
items.push(defaultPrompt(`${idPrefix}-${leftPad(items.length + 1)}`, title, prompt, images[0] || "", youMindTags(title, modelTag), markdownPreview(images)));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async function buildDavidWuGptImage2Prompts() {
|
||||
const data = await fetchJson<Array<{ id?: number; title_en?: string; title_cn?: string; category?: string; category_cn?: string; prompt?: string; note?: string; author?: string; source?: string; needs_ref?: boolean; image?: string }>>(davidWuGptImage2RawBase, "prompts.json");
|
||||
return data
|
||||
.map((item, index) => {
|
||||
const title = (item.title_cn || item.title_en || "").trim();
|
||||
const prompt = (item.prompt || "").trim();
|
||||
if (!title || !prompt) return null;
|
||||
const image = absoluteImage(davidWuGptImage2RawBase, item.image || "");
|
||||
const preview = [item.title_en, item.note, image ? `` : ""].filter(Boolean).join("\n\n");
|
||||
return defaultPrompt(`davidwu-gpt-image2-prompts-${leftPad(item.id || index + 1)}`, title, prompt, image, davidWuTags(item), preview);
|
||||
})
|
||||
.filter((item): item is Omit<Prompt, "category" | "githubUrl"> => Boolean(item));
|
||||
}
|
||||
|
||||
function defaultPrompt(id: string, title: string, prompt: string, coverUrl: string, tags: string[], preview: string): Omit<Prompt, "category" | "githubUrl"> {
|
||||
return { id, title, coverUrl, prompt, tags, preview, createdAt: "", updatedAt: "" };
|
||||
}
|
||||
|
||||
async function fetchText(baseUrl: string, file: string) {
|
||||
const response = await fetch(`${baseUrl}/${file}`, { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`${file} 拉取失败`);
|
||||
return response.text();
|
||||
}
|
||||
|
||||
async function fetchJson<T>(baseUrl: string, file: string) {
|
||||
return JSON.parse(await fetchText(baseUrl, file)) as T;
|
||||
}
|
||||
|
||||
function splitBeforeHeading(markdown: string, prefix: string) {
|
||||
const blocks: string[] = [];
|
||||
let current: string[] = [];
|
||||
for (const line of markdown.split("\n")) {
|
||||
if (line.startsWith(prefix) && current.length) {
|
||||
blocks.push(current.join("\n"));
|
||||
current = [];
|
||||
}
|
||||
current.push(line);
|
||||
}
|
||||
blocks.push(current.join("\n"));
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function firstMatch(value: string, pattern: RegExp) {
|
||||
return pattern.exec(value)?.[1] || "";
|
||||
}
|
||||
|
||||
function extractMarkdownImages(baseUrl: string, markdown: string) {
|
||||
return Array.from(markdown.matchAll(/!\[[^\]]*]\(([^)]+)\)/g), (match) => absoluteImage(baseUrl, match[1])).filter(Boolean);
|
||||
}
|
||||
|
||||
function absoluteImage(baseUrl: string, image: string) {
|
||||
if (!image) return "";
|
||||
if (/^https?:\/\//i.test(image)) return image;
|
||||
return `${baseUrl}/${image.replace(/^\.?\//, "")}`;
|
||||
}
|
||||
|
||||
function tagsFromCategory(category: string) {
|
||||
return splitTags(category.replace(/\s+Cases$/i, ""), /\s*(?:&|and)\s*/);
|
||||
}
|
||||
|
||||
function tagsFromHeading(heading: string) {
|
||||
return splitTags(heading.replace(/[^\p{L}\p{N}/&、与 ]/gu, ""), /\s*(?:\/|&|、|与)\s*/);
|
||||
}
|
||||
|
||||
function youMindTags(title: string, modelTag: string) {
|
||||
const [, prefix] = title.match(/^(.+?) - /) || [];
|
||||
return [modelTag, ...tagsFromHeading(prefix || "")];
|
||||
}
|
||||
|
||||
function davidWuTags(item: { category_cn?: string; category?: string; author?: string; source?: string; needs_ref?: boolean }) {
|
||||
const tags = splitTags([item.category_cn, item.category, item.author, item.source].filter(Boolean).join("/"), /\//);
|
||||
if (item.needs_ref) tags.push("需要参考图");
|
||||
return tags;
|
||||
}
|
||||
|
||||
function splitTags(value: string, pattern: RegExp) {
|
||||
return value
|
||||
.split(pattern)
|
||||
.map((tag) => tag.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function markdownPreview(images: string[]) {
|
||||
return images.filter(Boolean).map((image) => ``).join("\n\n");
|
||||
}
|
||||
|
||||
function collectTags(items: Prompt[]) {
|
||||
return Array.from(new Set(items.flatMap((item) => item.tags).filter(Boolean)));
|
||||
}
|
||||
|
||||
function leftPad(value: number) {
|
||||
return String(value).padStart(4, "0");
|
||||
}
|
||||
|
||||
function isActiveOption(value: string) {
|
||||
return value && value !== "全部" && value !== "all";
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
@@ -1,40 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import Script from "next/script";
|
||||
import { AntdRegistry } from "@ant-design/nextjs-registry";
|
||||
import { AppProviders } from "@/components/layout/app-providers";
|
||||
import "antd/dist/reset.css";
|
||||
import "./globals.css";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "无限画布",
|
||||
description: "一个无限画布创作工具",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN" suppressHydrationWarning className="font-sans">
|
||||
<body
|
||||
className="bg-background text-foreground antialiased"
|
||||
style={{
|
||||
fontFamily: '"SF Pro Display","SF Pro Text","PingFang SC","Microsoft YaHei","Helvetica Neue",sans-serif',
|
||||
}}
|
||||
>
|
||||
<Script
|
||||
id="theme-script"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `try{var s=JSON.parse(localStorage.getItem("infinite-canvas:theme_store")||"{}");var t=s.state&&s.state.theme==="light"?"light":"dark";document.documentElement.classList.toggle("dark",t==="dark");document.documentElement.style.colorScheme=t}catch(e){}`,
|
||||
}}
|
||||
/>
|
||||
<AntdRegistry>
|
||||
<AppProviders>{children}</AppProviders>
|
||||
</AntdRegistry>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const WEBDAV_PROXY_TIMEOUT_MS = 120000;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const target = request.headers.get("x-webdav-target") || "";
|
||||
const method = (request.headers.get("x-webdav-method") || "GET").toUpperCase();
|
||||
if (!target) return new Response("Missing x-webdav-target", { status: 400 });
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(target);
|
||||
} catch {
|
||||
return new Response("Invalid x-webdav-target", { status: 400 });
|
||||
}
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") return new Response("Unsupported WebDAV target", { status: 400 });
|
||||
|
||||
const headers = new Headers();
|
||||
copyHeader(request, headers, "x-webdav-authorization", "Authorization");
|
||||
copyHeader(request, headers, "x-webdav-depth", "Depth");
|
||||
copyHeader(request, headers, "x-webdav-destination", "Destination");
|
||||
copyHeader(request, headers, "x-webdav-overwrite", "Overwrite");
|
||||
copyHeader(request, headers, "x-webdav-content-type", "Content-Type");
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), WEBDAV_PROXY_TIMEOUT_MS);
|
||||
try {
|
||||
const body = method === "GET" || method === "HEAD" ? undefined : await request.arrayBuffer();
|
||||
console.log(`[webdav-proxy] ${method} ${url.href} ${body?.byteLength || 0}B`);
|
||||
const response = await fetch(url, { method, headers, body: body?.byteLength ? body : undefined, signal: controller.signal });
|
||||
console.log(`[webdav-proxy] ${method} ${url.href} -> ${response.status}`);
|
||||
return new Response(method === "HEAD" ? null : response.body, {
|
||||
status: response.status,
|
||||
headers: responseHeaders(response.headers),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === "AbortError") return new Response("WebDAV proxy timeout", { status: 504 });
|
||||
return new Response(error instanceof Error ? error.message : "WebDAV proxy error", { status: 502 });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function copyHeader(request: NextRequest, headers: Headers, from: string, to: string) {
|
||||
const value = request.headers.get(from);
|
||||
if (value) headers.set(to, value);
|
||||
}
|
||||
|
||||
function responseHeaders(headers: Headers) {
|
||||
const result = new Headers();
|
||||
["content-type", "etag", "last-modified", "dav"].forEach((key) => {
|
||||
const value = headers.get(key);
|
||||
if (value) result.set(key, value);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactNode } from "react";
|
||||
|
||||
import { ImageSettingsTheme } from "@/components/image-settings-panel";
|
||||
|
||||
+1
-2
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Empty, Input, Modal, Pagination, Tag } from "antd";
|
||||
import { Search } from "lucide-react";
|
||||
@@ -11,6 +9,7 @@ export type InsertAssetPayload = { kind: "text"; content: string; title: string
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
defaultTab?: string;
|
||||
onInsert: (payload: InsertAssetPayload) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { Button, Tooltip } from "antd";
|
||||
import { ArrowUp, CheckCircle2, CircleAlert, ImagePlus, LoaderCircle, UserRound, Wrench, X, XCircle } from "lucide-react";
|
||||
+10
-10
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import copyToClipboard from "copy-to-clipboard";
|
||||
import { Bot, Copy, Cpu, History, PanelRightClose, Plus, Settings2, Trash2, X } from "lucide-react";
|
||||
@@ -20,10 +18,10 @@ import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/u
|
||||
import { CanvasPromptLibrary } from "./canvas-prompt-library";
|
||||
import { AgentChatComposer, AgentChatMessage, AgentModeSwitch, AgentPanelTabs, AgentWorkingMessage, type CanvasAgentChatMessage, type CanvasAgentMode } from "./canvas-agent-chat-ui";
|
||||
import { CanvasLocalAgentPanel } from "./canvas-local-agent-panel";
|
||||
import { NODE_DEFAULT_SIZE } from "../constants";
|
||||
import { CanvasNodeType, type CanvasAssistantMessage, type CanvasAssistantReference, type CanvasAssistantSession, type CanvasNodeData } from "../types";
|
||||
import { useCanvasAgentStore } from "../stores/use-canvas-agent-store";
|
||||
import { summarizeCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "../utils/canvas-agent-ops";
|
||||
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { CanvasNodeType, type CanvasAssistantMessage, type CanvasAssistantReference, type CanvasAssistantSession, type CanvasNodeData } from "@/types/canvas";
|
||||
import { useCanvasAgentStore } from "@/stores/canvas/use-canvas-agent-store";
|
||||
import { summarizeCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
||||
|
||||
export const CANVAS_AGENT_PANEL_MOTION_MS = 500;
|
||||
const PANEL_MOTION_SECONDS = CANVAS_AGENT_PANEL_MOTION_MS / 1000;
|
||||
@@ -137,11 +135,12 @@ type CanvasAssistantPanelProps = {
|
||||
onPasteImage: (file: File) => void;
|
||||
agentMode: CanvasAgentMode;
|
||||
onAgentModeChange: (mode: CanvasAgentMode) => void;
|
||||
autoConnectLocal?: boolean;
|
||||
closing: boolean;
|
||||
onCollapse: () => void;
|
||||
};
|
||||
|
||||
export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, sessions, activeSessionId, onSelectNodeIds, onSessionsChange, onApplyOps, canUndoOps, onUndoOps, onPasteImage, agentMode, onAgentModeChange, closing, onCollapse }: CanvasAssistantPanelProps) {
|
||||
export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, sessions, activeSessionId, onSelectNodeIds, onSessionsChange, onApplyOps, canUndoOps, onUndoOps, onPasteImage, agentMode, onAgentModeChange, autoConnectLocal, closing, onCollapse }: CanvasAssistantPanelProps) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const user = useUserStore((state) => state.user);
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
@@ -660,6 +659,7 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, session
|
||||
canUndoOps={canUndoOps}
|
||||
onApplyOps={onApplyOps}
|
||||
onUndoOps={onUndoOps}
|
||||
autoConnect={autoConnectLocal}
|
||||
/>
|
||||
) : (
|
||||
onlineContent
|
||||
@@ -805,7 +805,7 @@ function OnlineAgentLogView({ logs, theme, context, onClear }: { logs: OnlineAge
|
||||
const content = mode === "text" ? formatOnlineLogText(logs, context) : formatOnlineLogJson(logs, context);
|
||||
const lastError = [...logs].reverse().find((item) => /错误|失败|error/i.test(`${item.title}\n${stringifyLog(item.data)}`));
|
||||
const copy = async (value = content) => {
|
||||
if (copyToClipboard(value)) return;
|
||||
if (await copyToClipboard(value)) return;
|
||||
textareaRef.current?.focus();
|
||||
textareaRef.current?.select();
|
||||
};
|
||||
@@ -1056,7 +1056,7 @@ function isResponseToolCall(value: unknown): value is ResponseToolCall {
|
||||
}
|
||||
|
||||
function toolCallToResponseInput(call: ResponseToolCall): ResponseInputMessage {
|
||||
return { type: "function_call", call_id: call.id, name: call.function.name, arguments: call.function.arguments };
|
||||
return { type: "function_call", call_id: call.id, name: call.function.name, arguments: call.function.arguments, ...(call.thoughtSignature ? { thoughtSignature: call.thoughtSignature } : {}) };
|
||||
}
|
||||
|
||||
function summarizeToolCalls(calls: ResponseToolCall[]) {
|
||||
@@ -1266,7 +1266,7 @@ async function buildToolAgentMessages(snapshot: CanvasAgentSnapshot, history: Ca
|
||||
...history
|
||||
.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "system")
|
||||
.slice(-8)
|
||||
.map((message): ResponseInputMessage => ({ role: message.role, content: message.text })),
|
||||
.map((message): ResponseInputMessage => ({ role: message.role as "system" | "user" | "assistant", content: message.text })),
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, type RefObject } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Settings2 } from "lucide-react";
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { CSSProperties, KeyboardEvent, MouseEvent, PointerEvent } from "react";
|
||||
import { Button, Image } from "antd";
|
||||
+23
-13
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
import { Image as ImageIcon, LoaderCircle, MessageSquare, Music2, Play, Settings2, Video } from "lucide-react";
|
||||
import { Image as ImageIcon, LoaderCircle, MessageSquare, Music2, Play, Settings2, Square, Video } from "lucide-react";
|
||||
import { Button, Segmented } from "antd";
|
||||
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
@@ -12,7 +10,7 @@ import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { CanvasImageSettingsPopover } from "./canvas-image-settings-popover";
|
||||
import { CanvasAudioSettingsPopover, type CanvasAudioSettingKey } from "./canvas-audio-settings-popover";
|
||||
import { CanvasVideoSettingsPopover } from "./canvas-video-settings-popover";
|
||||
import type { CanvasGenerationMode, CanvasNodeData, CanvasNodeMetadata } from "../types";
|
||||
import type { CanvasGenerationMode, CanvasNodeData, CanvasNodeMetadata } from "@/types/canvas";
|
||||
|
||||
type CanvasConfigNodePanelProps = {
|
||||
node: CanvasNodeData;
|
||||
@@ -20,10 +18,11 @@ type CanvasConfigNodePanelProps = {
|
||||
inputSummary: { textCount: number; imageCount: number; videoCount: number; audioCount: number };
|
||||
onConfigChange: (nodeId: string, patch: Partial<CanvasNodeMetadata>) => void;
|
||||
onGenerate: (nodeId: string) => void;
|
||||
onStop: (nodeId: string) => void;
|
||||
onComposerToggle: () => void;
|
||||
};
|
||||
|
||||
export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigChange, onGenerate, onComposerToggle }: CanvasConfigNodePanelProps) {
|
||||
export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigChange, onGenerate, onStop, onComposerToggle }: CanvasConfigNodePanelProps) {
|
||||
const globalConfig = useEffectiveConfig();
|
||||
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
@@ -113,17 +112,28 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
|
||||
<Button
|
||||
type="primary"
|
||||
className="mt-auto !h-9 !w-full !cursor-pointer !rounded-lg"
|
||||
disabled={isRunning || !canGenerate}
|
||||
danger={isRunning}
|
||||
disabled={!isRunning && !canGenerate}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={() => onGenerate(node.id)}
|
||||
onClick={() => (isRunning ? onStop(node.id) : onGenerate(node.id))}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<CreditSymbol />
|
||||
{credits.toLocaleString()}
|
||||
</span>
|
||||
{isRunning ? <LoaderCircle className="size-4 animate-spin" /> : <Play className="size-4" />}
|
||||
<span>开始生成</span>
|
||||
{isRunning ? (
|
||||
<>
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
<Square className="size-3.5 fill-current" />
|
||||
<span>停止</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<CreditSymbol />
|
||||
{credits.toLocaleString()}
|
||||
</span>
|
||||
<Play className="size-4" />
|
||||
<span>开始生成</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
+1
-1
@@ -2,7 +2,7 @@ import type { MouseEvent as ReactMouseEvent } from "react";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import type { CanvasConnection, CanvasNodeData, ConnectionHandle, Position } from "../types";
|
||||
import type { CanvasConnection, CanvasNodeData, ConnectionHandle, Position } from "@/types/canvas";
|
||||
|
||||
export function ConnectionPath({
|
||||
connection,
|
||||
+1
-3
@@ -1,12 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import type { ContextMenuState } from "../types";
|
||||
import type { ContextMenuState } from "@/types/canvas";
|
||||
|
||||
export function CanvasNodeContextMenu({ menu, onClose, onDuplicate, onDelete }: { menu: ContextMenuState; onClose: () => void; onDuplicate: () => void; onDelete: () => void }) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
+2
-4
@@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Modal } from "antd";
|
||||
|
||||
import { useAssetStore } from "@/stores/use-asset-store";
|
||||
import { useCanvasStore } from "../stores/use-canvas-store";
|
||||
import { useCanvasUiStore } from "../stores/use-canvas-ui-store";
|
||||
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
import { useCanvasUiStore } from "@/stores/canvas/use-canvas-ui-store";
|
||||
|
||||
export function CanvasDeleteProjectsDialog() {
|
||||
const ids = useCanvasUiStore((state) => state.deleteProjectIds);
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, type RefObject } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Settings2 } from "lucide-react";
|
||||
+6
-6
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
||||
import { Button, Card, Checkbox, Form, Modal, Space, Switch, Tag, Tooltip, Typography, theme as antdTheme } from "antd";
|
||||
import { Ellipsis, Image as ImageIcon, Settings2 } from "lucide-react";
|
||||
@@ -102,10 +100,12 @@ export function ImageToolSettingsModal({
|
||||
frames.push(firstFrame);
|
||||
const timer = window.setTimeout(sync, 120);
|
||||
const resizeObserver = typeof ResizeObserver !== "undefined" && toolbar ? new ResizeObserver(sync) : null;
|
||||
resizeObserver?.observe(toolbar);
|
||||
toolbar?.childNodes.forEach((child) => {
|
||||
if (child instanceof Element) resizeObserver?.observe(child);
|
||||
});
|
||||
if (resizeObserver && toolbar) {
|
||||
resizeObserver.observe(toolbar);
|
||||
toolbar.childNodes.forEach((child) => {
|
||||
if (child instanceof Element) resizeObserver.observe(child);
|
||||
});
|
||||
}
|
||||
sync();
|
||||
window.addEventListener("resize", syncPreviewScroll);
|
||||
return () => {
|
||||
+1
-3
@@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { Brush, Camera, Copy, FileText, Grid2x2, Lock, LockOpen, Maximize2, Scissors, Sparkles, Upload, ZoomIn } from "lucide-react";
|
||||
|
||||
import type { CanvasNodeData } from "../types";
|
||||
import type { CanvasNodeData } from "@/types/canvas";
|
||||
|
||||
export type ImageNodeActionToolId = "copyPrompt" | "reversePrompt" | "replace" | "resize" | "maskEdit" | "crop" | "split" | "upscale" | "superResolve" | "angle" | "view";
|
||||
export type ImageQuickToolId = "info" | "delete" | "saveAsset" | "download" | "edit" | ImageNodeActionToolId;
|
||||
+53
-23
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { App, Button, Input, Segmented, Tooltip } from "antd";
|
||||
import copyToClipboard from "copy-to-clipboard";
|
||||
import { Copy, FolderOpen, History, KeyRound, Link2, LoaderCircle, PlugZap, Plus, RefreshCw, RotateCcw, Terminal, Trash2 } from "lucide-react";
|
||||
@@ -9,18 +8,18 @@ import { motion } from "motion/react";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import { useCanvasAgentStore, type AgentAttachment, type AgentChatItem, type AgentEventLog, type AgentPanelTab, type AgentPendingToolCall, type AgentThreadSummary } from "../stores/use-canvas-agent-store";
|
||||
import { summarizeCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "../utils/canvas-agent-ops";
|
||||
import { useCanvasAgentStore, type AgentAttachment, type AgentChatItem, type AgentEventLog, type AgentPanelTab, type AgentPendingToolCall, type AgentThreadSummary } from "@/stores/canvas/use-canvas-agent-store";
|
||||
import { summarizeCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
||||
import { AgentChatComposer, AgentChatMessage, AgentPanelTabs, AgentPendingToolCard, AgentWorkingMessage, type CanvasAgentChatAttachment } from "./canvas-agent-chat-ui";
|
||||
|
||||
const PANEL_MOTION_SECONDS = 0.5;
|
||||
const MAX_ATTACHMENTS = 6;
|
||||
const MAX_ATTACHMENT_PAYLOAD_BYTES = 28 * 1024 * 1024;
|
||||
const DEFAULT_AGENT_URL = "http://127.0.0.1:17371";
|
||||
const AGENT_CONNECT_STEPS = [
|
||||
{ title: "1. 本机已安装并登录 Codex", text: "先确认本机终端里的 Codex 可以正常使用。", command: "codex --version" },
|
||||
{ title: "2. 安装 Canvas Agent", text: "推荐全局安装,后续可以直接运行 canvas-agent。", command: "npm i -g @basketikun/canvas-agent" },
|
||||
{ title: "3. 启动本地 Agent", text: "启动后终端会输出 Local URL 和 Connect token。", command: "canvas-agent" },
|
||||
{ title: "4. 回到网页连接", text: "把终端输出的地址和 token 填到下面,点击连接。" },
|
||||
{ title: "安装 Codex 插件", text: "在 Codex app 安装 Infinite Canvas 插件后,首次使用插件会自动启动本地 Agent。" },
|
||||
{ title: "打开画布连接", text: "回到这里点击连接,网页会自动读取本机 Agent 配置。" },
|
||||
{ title: "手动启动备用", text: "如果自动发现失败,再运行下面命令。", command: "npx -y @basketikun/canvas-agent" },
|
||||
];
|
||||
|
||||
type AgentEventPayload = {
|
||||
@@ -38,11 +37,13 @@ type AgentLogContext = { endpoint: string; connected: boolean; enabled: boolean;
|
||||
type AgentWorkspace = { canvasId: string; workspacePath: string; activeThreadId?: string };
|
||||
type AgentThreadsResponse = { ok?: boolean; workspace?: AgentWorkspace; data?: AgentThreadSummary[] };
|
||||
type AgentThreadResponse = { ok?: boolean; workspace?: AgentWorkspace; thread?: AgentThreadSummary; messages?: AgentChatItem[] };
|
||||
type AgentConfigResponse = { ok?: boolean; url?: string; token?: string; hasToken?: boolean };
|
||||
|
||||
export function CanvasLocalAgentPanel({ snapshot, canUndoOps, collapsed, embedded, onApplyOps, onUndoOps }: { snapshot: CanvasAgentSnapshot; canUndoOps: boolean; collapsed?: boolean; embedded?: boolean; onApplyOps: (ops: CanvasAgentOp[]) => unknown; onUndoOps: () => CanvasAgentSnapshot | null }) {
|
||||
export function CanvasLocalAgentPanel({ snapshot, canUndoOps, collapsed, embedded, headless, autoConnect, onApplyOps, onUndoOps }: { snapshot: CanvasAgentSnapshot; canUndoOps: boolean; collapsed?: boolean; embedded?: boolean; headless?: boolean; autoConnect?: boolean; onApplyOps: (ops: CanvasAgentOp[]) => unknown; onUndoOps: () => CanvasAgentSnapshot | null }) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const user = useUserStore((state) => state.user);
|
||||
const { message, modal } = App.useApp();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { width, url, token, connected, enabled, prompt, attachments, sending, waiting, messages, eventLogs, threads, activeThreadId, workspacePath, loadingThreads, activeTab, confirmTools, activity, connectError, pendingTool, setAgentState, addMessage: pushMessage, addEventLog: pushEventLog, clearEventLogs } = useCanvasAgentStore();
|
||||
const [resizing, setResizing] = useState(false);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
@@ -50,11 +51,13 @@ export function CanvasLocalAgentPanel({ snapshot, canUndoOps, collapsed, embedde
|
||||
const confirmToolsRef = useRef(confirmTools);
|
||||
const pendingToolRef = useRef<AgentPendingToolCall | null>(null);
|
||||
const onApplyOpsRef = useRef(onApplyOps);
|
||||
const autoConnectRef = useRef(false);
|
||||
const connectedRef = useRef(false);
|
||||
const errorLoggedRef = useRef(false);
|
||||
const attachmentUrlsRef = useRef(new Set<string>());
|
||||
const clientIdRef = useRef(typeof crypto === "undefined" ? `${Date.now()}` : crypto.randomUUID());
|
||||
const endpoint = useMemo(() => url.trim().replace(/\/$/, ""), [url]);
|
||||
const urlAgentAutoConnect = searchParams.has("agentUrl") && searchParams.has("agentToken");
|
||||
const loadThreads = useCallback(async () => {
|
||||
const projectId = snapshotRef.current.projectId;
|
||||
if ((!connectedRef.current && !useCanvasAgentStore.getState().connected) || !projectId) return;
|
||||
@@ -106,7 +109,7 @@ export function CanvasLocalAgentPanel({ snapshot, canUndoOps, collapsed, embedde
|
||||
errorLoggedRef.current = false;
|
||||
connectedRef.current = true;
|
||||
setAgentState({ connected: true, activity: "已连接", connectError: "", messages: useCanvasAgentStore.getState().messages.filter((item) => !isConnectionErrorMessage(item)) });
|
||||
message.success("本地 Agent 已连接");
|
||||
if (!headless) message.success("本地 Agent 已连接");
|
||||
void postState(endpoint, token, clientId, snapshotRef.current);
|
||||
});
|
||||
source.addEventListener("tool_call", (event) => {
|
||||
@@ -136,7 +139,7 @@ export function CanvasLocalAgentPanel({ snapshot, canUndoOps, collapsed, embedde
|
||||
const text = wasConnected ? "本地 Agent 连接失败或已断开" : "连接失败,请检查地址和 token";
|
||||
if (!errorLoggedRef.current || wasConnected) {
|
||||
addEventLog(wasConnected ? "连接断开" : "连接失败", { endpoint, error: text });
|
||||
message.error(text);
|
||||
if (!headless) message.error(text);
|
||||
}
|
||||
errorLoggedRef.current = true;
|
||||
connectedRef.current = false;
|
||||
@@ -288,36 +291,51 @@ export function CanvasLocalAgentPanel({ snapshot, canUndoOps, collapsed, embedde
|
||||
if (connected) void postState(endpoint, token, clientIdRef.current, restored);
|
||||
};
|
||||
|
||||
const toggleAgentConnection = () => {
|
||||
const toggleAgentConnection = async () => {
|
||||
if (enabled) {
|
||||
clearAgentSession({ enabled: false, connected: false, activity: "离线", connectError: "" });
|
||||
return;
|
||||
}
|
||||
if (!endpoint) {
|
||||
const urlToken = searchParams.get("agentToken") || "";
|
||||
const urlEndpoint = searchParams.get("agentUrl") || "";
|
||||
const discovered = urlToken ? null : await discoverAgentConfig(endpoint || DEFAULT_AGENT_URL);
|
||||
const nextEndpoint = (urlEndpoint || discovered?.url || endpoint || DEFAULT_AGENT_URL).trim().replace(/\/$/, "");
|
||||
const nextToken = (urlToken || token.trim() || discovered?.token || "").trim();
|
||||
if (!nextEndpoint) {
|
||||
const text = "请填写本地 Agent 地址";
|
||||
setAgentState({ connectError: text });
|
||||
message.warning(text);
|
||||
if (!headless) message.warning(text);
|
||||
return;
|
||||
}
|
||||
if (!token.trim()) {
|
||||
const text = "请填写 Agent token";
|
||||
if (!nextToken) {
|
||||
const text = "没有发现本地 Agent,请先在 Codex 使用插件或手动启动 Canvas Agent";
|
||||
setAgentState({ connectError: text });
|
||||
message.warning(text);
|
||||
if (!headless) message.warning(text);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(endpoint);
|
||||
const parsed = new URL(nextEndpoint);
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("invalid protocol");
|
||||
} catch {
|
||||
const text = "本地 Agent 地址格式不正确";
|
||||
setAgentState({ connectError: text });
|
||||
message.warning(text);
|
||||
if (!headless) message.warning(text);
|
||||
return;
|
||||
}
|
||||
errorLoggedRef.current = false;
|
||||
setAgentState({ url: endpoint, token: token.trim(), enabled: true, connected: false, activity: "连接中", connectError: "", activeTab: "setup" });
|
||||
setAgentState({ url: nextEndpoint, token: nextToken, enabled: true, connected: false, activity: "连接中", connectError: "", activeTab: "setup" });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (urlAgentAutoConnect && confirmTools) setAgentState({ confirmTools: false });
|
||||
}, [confirmTools, setAgentState, urlAgentAutoConnect]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoConnect || autoConnectRef.current || enabled || connected) return;
|
||||
autoConnectRef.current = true;
|
||||
void toggleAgentConnection();
|
||||
}, [autoConnect, connected, enabled]);
|
||||
|
||||
function clearAgentSession(patch: Parameters<typeof setAgentState>[0] = {}) {
|
||||
setAgentState({
|
||||
messages: [],
|
||||
@@ -544,6 +562,7 @@ export function CanvasLocalAgentPanel({ snapshot, canUndoOps, collapsed, embedde
|
||||
</>
|
||||
);
|
||||
|
||||
if (headless) return null;
|
||||
if (embedded) return content;
|
||||
|
||||
return (
|
||||
@@ -624,7 +643,7 @@ function AgentConnectView({ theme, url, token, enabled, connected, activity, con
|
||||
<div>
|
||||
<div className="text-base font-semibold leading-6">连接本地 Agent</div>
|
||||
<div className="mt-1 text-xs leading-5" style={{ color: theme.node.muted }}>
|
||||
本地服务只监听 127.0.0.1,画布通过地址和 token 连接到你电脑上的 Codex。
|
||||
安装 Codex 插件后,画布会优先自动连接本机 Agent。
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -657,7 +676,7 @@ function AgentConnectView({ theme, url, token, enabled, connected, activity, con
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs leading-5" style={{ color: theme.node.muted }}>
|
||||
填入终端输出的 Local URL 和 Connect token。
|
||||
默认自动读取 Local URL 和 Connect token,失败时再手动填写。
|
||||
</div>
|
||||
</div>
|
||||
<Button className="!h-8 !px-3" type={enabled ? "default" : "primary"} icon={<PlugZap className="size-4" />} onClick={onToggleEnabled}>
|
||||
@@ -679,7 +698,7 @@ function AgentConnectView({ theme, url, token, enabled, connected, activity, con
|
||||
连接 Token
|
||||
<span className="font-normal opacity-70">Connect token</span>
|
||||
</span>
|
||||
<Input.Password size="large" prefix={<KeyRound className="mr-1 size-4" style={{ color: theme.node.faint }} />} value={token} onChange={(event) => onTokenChange(event.target.value)} placeholder="终端输出的 Connect token" />
|
||||
<Input.Password size="large" prefix={<KeyRound className="mr-1 size-4" style={{ color: theme.node.faint }} />} value={token} onChange={(event) => onTokenChange(event.target.value)} placeholder="自动发现,或手动填入 Connect token" />
|
||||
</label>
|
||||
{connectError ? (
|
||||
<div className="rounded-md border px-2.5 py-2 text-xs leading-5" style={{ borderColor: "rgba(220,38,38,.35)", color: "#dc2626" }}>
|
||||
@@ -971,6 +990,17 @@ async function fetchAgentJson<T>(endpoint: string, token: string, path: string,
|
||||
return data;
|
||||
}
|
||||
|
||||
async function discoverAgentConfig(endpoint: string) {
|
||||
try {
|
||||
const res = await fetch(`${endpoint}/config`);
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as AgentConfigResponse;
|
||||
return data.ok ? data : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHistoryMessages(messages: AgentChatItem[]) {
|
||||
return messages
|
||||
.map((item, index) => ({
|
||||
+1
-3
@@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { CanvasNodeType, type CanvasNodeData, type ViewportTransform } from "../types";
|
||||
import { CanvasNodeType, type CanvasNodeData, type ViewportTransform } from "@/types/canvas";
|
||||
|
||||
export function Minimap({ nodes, viewport, viewportSize, onViewportChange }: { nodes: CanvasNodeData[]; viewport: ViewportTransform; viewportSize: { width: number; height: number }; onViewportChange: (viewport: ViewportTransform) => void }) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Modal, Segmented, Slider } from "antd";
|
||||
import { RotateCcw, WandSparkles } from "lucide-react";
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { Button, Modal } from "antd";
|
||||
import { Check, Lock, LockOpen, X } from "lucide-react";
|
||||
+2
-2
@@ -3,8 +3,8 @@ import { imageReferenceLabel } from "@/lib/image-reference-prompt";
|
||||
import { seedanceReferenceLabel } from "@/lib/seedance-video";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
import type { ReferenceAudio, ReferenceVideo } from "@/types/media";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "../types";
|
||||
import { getGenerationResourceNodes } from "../utils/canvas-resource-references";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "@/types/canvas";
|
||||
import { getGenerationResourceNodes } from "@/lib/canvas/canvas-resource-references";
|
||||
|
||||
export type NodeGenerationContext = {
|
||||
prompt: string;
|
||||
+4
-5
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { App, Modal, Segmented, Tooltip } from "antd";
|
||||
import { Download, Ellipsis, FolderPlus, Image as ImageIcon, Info, MessageSquare, Minus, Music2, Pencil, Plus, RefreshCw, Settings2, Trash2, Upload, Video } from "lucide-react";
|
||||
@@ -8,7 +6,7 @@ import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { formatBytes, getDataUrlByteSize } from "@/lib/image-utils";
|
||||
import { useCopyText } from "@/hooks/use-copy-text";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { CanvasNodeType, type CanvasNodeData, type ViewportTransform } from "../types";
|
||||
import { CanvasNodeType, type CanvasNodeData, type ViewportTransform } from "@/types/canvas";
|
||||
import { ImageToolSettingsModal, type ImageToolbarSettingsTool } from "./canvas-image-toolbar-settings-modal";
|
||||
import { IMAGE_QUICK_TOOLS_STORAGE_KEY, buildImageToolbarTools, defaultImageQuickToolIds, readImageQuickToolsConfig, type ImageQuickToolId } from "./canvas-image-toolbar-tools";
|
||||
|
||||
@@ -102,6 +100,7 @@ export function CanvasNodeHoverToolbar({
|
||||
|
||||
if (!node) return null;
|
||||
|
||||
const activeNode = node;
|
||||
const left = viewport.x + (node.position.x + node.width / 2) * viewport.k;
|
||||
const top = viewport.y + node.position.y * viewport.k - 14;
|
||||
const isImage = node.type === CanvasNodeType.Image;
|
||||
@@ -126,7 +125,7 @@ export function CanvasNodeHoverToolbar({
|
||||
const imageTools = buildImageToolbarTools(node, { onUpload, onToggleFreeResize, onMaskEdit, onCrop, onSplit, onUpscale, onSuperResolve, onAngle, onViewImage, onCopyPrompt: copyImagePrompt, onReversePrompt });
|
||||
|
||||
function openImageToolSettings() {
|
||||
onKeep(node.id);
|
||||
onKeep(activeNode.id);
|
||||
setDraftImageToolIds(quickImageToolIds);
|
||||
setDraftShowImageToolLabels(showImageToolLabels);
|
||||
setImageToolSettingsOpen(true);
|
||||
@@ -282,7 +281,7 @@ export function CanvasNodeInfoModal({ node, open, onClose }: { node: CanvasNodeD
|
||||
function ToolbarAction({ title, label, icon, onClick, showLabel, active = false, danger = false }: ToolbarTool & { showLabel: boolean }) {
|
||||
const hasText = showLabel && Boolean(label);
|
||||
return (
|
||||
<Tooltip title={title} placement="top" mouseEnterDelay={0.2} color="#ffffff" styles={{ body: { color: "#242529", boxShadow: "0 8px 24px rgba(15,23,42,.16)", fontSize: 13, fontWeight: 500 } }}>
|
||||
<Tooltip title={title} placement="top" mouseEnterDelay={0.2} color="#ffffff" styles={{ root: { color: "#242529", boxShadow: "0 8px 24px rgba(15,23,42,.16)", fontSize: 13, fontWeight: 500 } }}>
|
||||
<button type="button" className={`group relative flex h-12 items-center whitespace-nowrap px-1.5 ${danger ? "text-[#ef4444]" : ""}`} onClick={onClick} aria-label={title}>
|
||||
<span className={`flex h-9 items-center ${hasText ? "gap-2 px-2.5" : "justify-center px-2"} rounded-lg transition group-hover:bg-[#f0f0f1] ${active ? "bg-[#eeeeef]" : ""}`}>
|
||||
{icon}
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { Button, Input, Modal, Slider } from "antd";
|
||||
import { Brush, Eraser, RotateCcw, WandSparkles, X } from "lucide-react";
|
||||
@@ -45,7 +43,7 @@ export function CanvasNodeMaskEditDialog({ dataUrl, open, onClose, onConfirm }:
|
||||
const point = readCanvasPoint(event.currentTarget, event.clientX, event.clientY);
|
||||
const maskCanvas = maskCanvasRef.current;
|
||||
const context = maskCanvas?.getContext("2d");
|
||||
if (!context) return;
|
||||
if (!maskCanvas || !context) return;
|
||||
context.lineCap = "round";
|
||||
context.lineJoin = "round";
|
||||
context.lineWidth = brushSize;
|
||||
+24
-14
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { ArrowUp, LoaderCircle } from "lucide-react";
|
||||
import { ArrowUp, LoaderCircle, Square } from "lucide-react";
|
||||
import { Button } from "antd";
|
||||
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
@@ -14,8 +12,8 @@ import { CanvasPromptLibrary } from "./canvas-prompt-library";
|
||||
import { CanvasAudioSettingsPopover, type CanvasAudioSettingKey } from "./canvas-audio-settings-popover";
|
||||
import { CanvasResourceMentionTextarea } from "./canvas-resource-mention-textarea";
|
||||
import { CanvasVideoSettingsPopover } from "./canvas-video-settings-popover";
|
||||
import { CanvasNodeType, type CanvasGenerationMode, type CanvasNodeData } from "../types";
|
||||
import type { CanvasResourceReference } from "../utils/canvas-resource-references";
|
||||
import { CanvasNodeType, type CanvasGenerationMode, type CanvasNodeData } from "@/types/canvas";
|
||||
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
|
||||
export type CanvasNodeGenerationMode = CanvasGenerationMode;
|
||||
|
||||
@@ -25,11 +23,12 @@ type CanvasNodePromptPanelProps = {
|
||||
onPromptChange: (nodeId: string, prompt: string) => void;
|
||||
onConfigChange: (nodeId: string, patch: Partial<CanvasNodeData["metadata"]>) => void;
|
||||
onGenerate: (nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => void;
|
||||
onStop: (nodeId: string) => void;
|
||||
mentionReferences?: CanvasResourceReference[];
|
||||
onImageSettingsOpenChange?: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfigChange, onGenerate, mentionReferences = [], onImageSettingsOpenChange }: CanvasNodePromptPanelProps) {
|
||||
export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfigChange, onGenerate, onStop, mentionReferences = [], onImageSettingsOpenChange }: CanvasNodePromptPanelProps) {
|
||||
const globalConfig = useEffectiveConfig();
|
||||
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
@@ -107,16 +106,27 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
<Button
|
||||
type="primary"
|
||||
className="!h-10 !min-w-16 shrink-0 !rounded-full !px-3"
|
||||
disabled={isRunning || !prompt.trim()}
|
||||
onClick={submit}
|
||||
aria-label="生成"
|
||||
danger={isRunning}
|
||||
disabled={!isRunning && !prompt.trim()}
|
||||
onClick={() => (isRunning ? onStop(node.id) : submit())}
|
||||
aria-label={isRunning ? "停止生成" : "生成"}
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium tabular-nums">
|
||||
<CreditSymbol />
|
||||
{credits.toLocaleString()}
|
||||
</span>
|
||||
{isRunning ? <LoaderCircle className="size-4 animate-spin" /> : <ArrowUp className="size-4" />}
|
||||
{isRunning ? (
|
||||
<>
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
<Square className="size-3.5 fill-current" />
|
||||
<span className="text-xs font-medium">停止</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium tabular-nums">
|
||||
<CreditSymbol />
|
||||
{credits.toLocaleString()}
|
||||
</span>
|
||||
<ArrowUp className="size-4" />
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
+1
-3
@@ -1,11 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, InputNumber, Modal } from "antd";
|
||||
import { Grid2x2 } from "lucide-react";
|
||||
|
||||
import { readImageMeta } from "@/lib/image-utils";
|
||||
import type { ImageSplitParams } from "../utils/canvas-image-data";
|
||||
import type { ImageSplitParams } from "@/lib/canvas/canvas-image-data";
|
||||
|
||||
export type CanvasImageSplitParams = ImageSplitParams;
|
||||
|
||||
+1
-3
@@ -1,11 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button, Modal, Segmented } from "antd";
|
||||
import { ImagePlus } from "lucide-react";
|
||||
|
||||
import { readImageMeta } from "@/lib/image-utils";
|
||||
import { MAX_UPSCALE_LONG_EDGE, resolveUpscaleSize, type ImageUpscaleAlgorithm, type ImageUpscaleParams } from "../utils/canvas-image-data";
|
||||
import { MAX_UPSCALE_LONG_EDGE, resolveUpscaleSize, type ImageUpscaleAlgorithm, type ImageUpscaleParams } from "@/lib/canvas/canvas-image-data";
|
||||
|
||||
export type CanvasImageUpscaleParams = ImageUpscaleParams;
|
||||
|
||||
+2
-4
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { ChevronRight, Image as ImageIcon, Music2, RefreshCw, Star, Video } from "lucide-react";
|
||||
@@ -8,8 +6,8 @@ import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { formatBytes } from "@/lib/image-utils";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { CanvasResourceMentionTextarea } from "./canvas-resource-mention-textarea";
|
||||
import { CanvasNodeType, type CanvasNodeData, type Position } from "../types";
|
||||
import type { CanvasResourceReference } from "../utils/canvas-resource-references";
|
||||
import { CanvasNodeType, type CanvasNodeData, type Position } from "@/types/canvas";
|
||||
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
|
||||
type ResizeCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
||||
const selectionBlue = "#2f80ff";
|
||||
+7
-8
@@ -1,15 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Check, Download, Pencil, Trash2, X } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { Button, Input } from "antd";
|
||||
|
||||
import { useCanvasStore, type CanvasProject } from "../stores/use-canvas-store";
|
||||
import { useCanvasUiStore } from "../stores/use-canvas-ui-store";
|
||||
import { exportCanvasProjects } from "../utils/canvas-export";
|
||||
import { useCanvasStore, type CanvasProject } from "@/stores/canvas/use-canvas-store";
|
||||
import { useCanvasUiStore } from "@/stores/canvas/use-canvas-ui-store";
|
||||
import { exportCanvasProjects } from "@/lib/canvas/canvas-export";
|
||||
|
||||
export function CanvasProjectCard({ project }: { project: CanvasProject }) {
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const renameProject = useCanvasStore((state) => state.renameProject);
|
||||
const selectedIds = useCanvasUiStore((state) => state.selectedProjectIds);
|
||||
const editingId = useCanvasUiStore((state) => state.editingProjectId);
|
||||
@@ -21,7 +20,7 @@ export function CanvasProjectCard({ project }: { project: CanvasProject }) {
|
||||
const setDeleteIds = useCanvasUiStore((state) => state.setDeleteProjectIds);
|
||||
const editing = editingId === project.id;
|
||||
const selected = selectedIds.includes(project.id);
|
||||
const open = () => router.push(`/canvas/${project.id}`);
|
||||
const open = () => navigate(`/canvas/${project.id}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`);
|
||||
const saveTitle = () => {
|
||||
renameProject(project.id, editingTitle);
|
||||
stopEditing();
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button, Tooltip } from "antd";
|
||||
import { BookOpen } from "lucide-react";
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useMemo, useRef, useState } from "react";
|
||||
import type { CSSProperties, MouseEvent, PointerEvent, TextareaHTMLAttributes } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
@@ -7,7 +5,7 @@ import { FileText, Image as ImageIcon, Music2, Video } from "lucide-react";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import type { CanvasResourceReference } from "../utils/canvas-resource-references";
|
||||
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
|
||||
type MentionState = {
|
||||
start: number;
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Select } from "antd";
|
||||
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, type RefObject } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Settings2 } from "lucide-react";
|
||||
+1
-3
@@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { canvasThemes, type CanvasBackgroundMode } from "@/lib/canvas-theme";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import type { ViewportTransform } from "../types";
|
||||
import type { ViewportTransform } from "@/types/canvas";
|
||||
|
||||
type InfiniteCanvasProps = {
|
||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { ConfigProvider, Switch } from "antd";
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { App, Button, Form, Input, Modal, Progress, Segmented, Select, Tabs } from "antd";
|
||||
import { Cloud, Plus, RefreshCw, Trash2, Wifi } from "lucide-react";
|
||||
import { App, Button, Form, Input, Modal, Progress, Select, Tabs } from "antd";
|
||||
import { CircleAlert, Cloud, Plus, RefreshCw, Trash2, Wifi } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
@@ -9,7 +7,7 @@ import { fetchChannelModels } from "@/services/api/image";
|
||||
import { syncAppDataToWebdav, type AppSyncDomainKey, type AppSyncProgressEvent } from "@/services/app-sync";
|
||||
import { testWebdavConnection, WEBDAV_MANIFEST_FILE_NAME } from "@/services/webdav-sync";
|
||||
import { audioFormatOptions, audioVoiceOptions, normalizeAudioSpeedValue } from "@/lib/audio-generation";
|
||||
import { createModelChannel, filterModelsByCapability, modelOptionLabel, modelOptionsFromChannels, normalizeModelOptionValue, useConfigStore, type AiConfig, type ModelCapability, type ModelChannel } from "@/stores/use-config-store";
|
||||
import { createModelChannel, defaultBaseUrlForApiFormat, filterModelsByCapability, modelOptionLabel, modelOptionsFromChannels, normalizeModelOptionValue, useConfigStore, type AiConfig, type ApiCallFormat, type ModelCapability, type ModelChannel } from "@/stores/use-config-store";
|
||||
|
||||
type ModelGroup = {
|
||||
capability: ModelCapability;
|
||||
@@ -34,6 +32,11 @@ const modelGroups: ModelGroup[] = [
|
||||
{ capability: "audio", modelKey: "audioModel", modelsKey: "audioModels", defaultLabel: "默认音频模型", optionsLabel: "音频模型可选项" },
|
||||
];
|
||||
|
||||
const apiFormatOptions: Array<{ label: string; value: ApiCallFormat }> = [
|
||||
{ label: "OpenAI", value: "openai" },
|
||||
{ label: "Gemini", value: "gemini" },
|
||||
];
|
||||
|
||||
const webdavDomainKeys: AppSyncDomainKey[] = ["canvas", "assets", "image-workbench", "video-workbench"];
|
||||
const webdavDomainLabels: Record<AppSyncDomainKey, string> = {
|
||||
canvas: "画布",
|
||||
@@ -92,6 +95,11 @@ export function AppConfigModal() {
|
||||
updateChannels(config.channels.map((channel) => (channel.id === id ? { ...channel, ...patch, models: patch.models ? uniqueModels(patch.models) : channel.models } : channel)));
|
||||
};
|
||||
|
||||
const updateChannelApiFormat = (channel: ModelChannel, apiFormat: ApiCallFormat) => {
|
||||
const baseUrl = !channel.baseUrl.trim() || channel.baseUrl.trim() === defaultBaseUrlForApiFormat(channel.apiFormat) ? defaultBaseUrlForApiFormat(apiFormat) : channel.baseUrl;
|
||||
updateChannel(channel.id, { apiFormat, baseUrl });
|
||||
};
|
||||
|
||||
const addChannel = () => {
|
||||
updateChannels([...config.channels, createModelChannel({ name: `渠道 ${config.channels.length + 1}` })]);
|
||||
};
|
||||
@@ -226,11 +234,17 @@ export function AppConfigModal() {
|
||||
children: (
|
||||
<Form layout="vertical" requiredMark={false}>
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3 rounded-lg border border-stone-200 p-3 dark:border-stone-800">
|
||||
<div>
|
||||
<div className="text-sm font-semibold">多渠道聚合</div>
|
||||
<div className="mt-1 text-xs text-stone-500">每个渠道保存自己的 Base URL、API Key 和模型列表;模型选择时会显示模型名和渠道名。</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex w-fit max-w-full flex-wrap items-center gap-1.5 rounded-md border border-amber-300 bg-amber-50 px-2.5 py-1.5 text-xs text-amber-900 dark:border-amber-700/60 dark:bg-amber-950/30 dark:text-amber-100">
|
||||
<CircleAlert className="size-3.5 shrink-0" />
|
||||
<span className="font-semibold">重要:</span>
|
||||
<span>新增或拉取模型后,需要到“模型”Tab 选择可选项才会显示。</span>
|
||||
<Button type="link" size="small" className="h-auto p-0 text-xs font-semibold text-amber-900 dark:text-amber-100" onClick={() => setActiveTab("models")}>
|
||||
去模型设置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Button icon={<RefreshCw className="size-4" />} loading={Boolean(loadingChannelId)} onClick={() => void refreshAllModels()}>
|
||||
拉取全部
|
||||
</Button>
|
||||
@@ -245,7 +259,9 @@ export function AppConfigModal() {
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-semibold">{channel.name || "未命名渠道"}</div>
|
||||
<div className="mt-1 text-xs text-stone-500">已保存 {channel.models.length} 个模型</div>
|
||||
<div className="mt-1 text-xs text-stone-500">
|
||||
{apiFormatLabel(channel.apiFormat)} · 已保存 {channel.models.length} 个模型
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Button size="small" loading={loadingChannelId === channel.id} onClick={() => void refreshChannelModels(channel)}>
|
||||
@@ -258,13 +274,16 @@ export function AppConfigModal() {
|
||||
<Form.Item label="渠道名称" className="mb-0">
|
||||
<Input value={channel.name} onChange={(event) => updateChannel(channel.id, { name: event.target.value })} />
|
||||
</Form.Item>
|
||||
<Form.Item label="调用格式" className="mb-0">
|
||||
<Select value={channel.apiFormat} options={apiFormatOptions} onChange={(value: ApiCallFormat) => updateChannelApiFormat(channel, value)} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Base URL" className="mb-0">
|
||||
<Input value={channel.baseUrl} onChange={(event) => updateChannel(channel.id, { baseUrl: event.target.value })} />
|
||||
</Form.Item>
|
||||
<Form.Item label="API Key" className="mb-0">
|
||||
<Input.Password value={channel.apiKey} onChange={(event) => updateChannel(channel.id, { apiKey: event.target.value })} />
|
||||
</Form.Item>
|
||||
<Form.Item label="模型列表" className="mb-0">
|
||||
<Form.Item label="模型列表" className="mb-0 md:col-span-2">
|
||||
<Select mode="tags" showSearch allowClear maxTagCount="responsive" placeholder="输入模型名,或点击拉取模型" value={channel.models} onChange={(models) => updateChannel(channel.id, { models })} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
@@ -364,22 +383,11 @@ export function AppConfigModal() {
|
||||
<Cloud className="size-4" />
|
||||
WebDAV 同步
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-stone-500">同步画布、我的素材、生成记录和本地媒体文件,不包含 AI API Key;服务不支持 CORS 时可走 Next.js 转发。</div>
|
||||
<div className="mt-1 text-xs text-stone-500">同步画布、我的素材、生成记录和本地媒体文件,不包含 AI API Key;浏览器会直接连接 WebDAV 服务。</div>
|
||||
</div>
|
||||
<div className="text-xs text-stone-500">{webdav.lastSyncedAt ? `上次同步 ${formatWebdavTime(webdav.lastSyncedAt)}` : "尚未同步"}</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Form.Item label="连接方式" className="mb-4 md:col-span-2">
|
||||
<Segmented
|
||||
block
|
||||
value={webdav.proxyMode}
|
||||
onChange={(value) => updateWebdavConfig("proxyMode", value as typeof webdav.proxyMode)}
|
||||
options={[
|
||||
{ label: "前端直连", value: "direct" },
|
||||
{ label: "Next.js 转发", value: "nextjs" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="WebDAV 地址" className="mb-4">
|
||||
<Input value={webdav.url} placeholder="https://nas.example.com/webdav" onChange={(event) => updateWebdavConfig("url", event.target.value)} />
|
||||
</Form.Item>
|
||||
@@ -425,6 +433,7 @@ function withChannels(config: AiConfig, channels: ModelChannel[]): AiConfig {
|
||||
models,
|
||||
baseUrl: channels[0]?.baseUrl || config.baseUrl,
|
||||
apiKey: channels[0]?.apiKey || config.apiKey,
|
||||
apiFormat: channels[0]?.apiFormat || config.apiFormat,
|
||||
imageModels,
|
||||
videoModels,
|
||||
textModels,
|
||||
@@ -455,6 +464,10 @@ function uniqueModels(models: string[]) {
|
||||
return Array.from(new Set(models.map((model) => model.trim()).filter(Boolean)));
|
||||
}
|
||||
|
||||
function apiFormatLabel(apiFormat: ApiCallFormat) {
|
||||
return apiFormat === "gemini" ? "Gemini" : "OpenAI";
|
||||
}
|
||||
|
||||
function formatWebdavTime(value: string) {
|
||||
return new Date(value).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { ProConfigProvider } from "@ant-design/pro-components";
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Menu } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
|
||||
import { navigationTools, type NavigationToolSlug } from "@/constant/navigation-tools";
|
||||
import { AppConfigModal } from "@/components/layout/app-config-modal";
|
||||
@@ -12,7 +9,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
export function AppTopNav() {
|
||||
const pathname = usePathname();
|
||||
const { pathname } = useLocation();
|
||||
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
||||
const hideHeader = /^\/canvas\/[^/]+/.test(pathname);
|
||||
const slug = pathname.split("/").filter(Boolean)[0];
|
||||
@@ -24,7 +21,7 @@ export function AppTopNav() {
|
||||
<header className="sticky top-0 z-20 h-16 shrink-0 border-b border-stone-200 bg-background/90 backdrop-blur-xl dark:border-stone-800">
|
||||
<div className="mx-auto flex h-full max-w-7xl items-stretch justify-between gap-5 px-6">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<Link href="/" className="flex h-full shrink-0 items-center gap-2 text-sm font-semibold leading-none tracking-tight text-stone-950 transition hover:text-stone-600 dark:text-stone-100 dark:hover:text-stone-300">
|
||||
<Link to="/" className="flex h-full shrink-0 items-center gap-2 text-sm font-semibold leading-none tracking-tight text-stone-950 transition hover:text-stone-600 dark:text-stone-100 dark:hover:text-stone-300">
|
||||
<span
|
||||
className="size-5 shrink-0 bg-current"
|
||||
style={{
|
||||
@@ -52,7 +49,7 @@ export function AppTopNav() {
|
||||
return (
|
||||
<Link
|
||||
key={tool.slug}
|
||||
href={`/${tool.slug}`}
|
||||
to={`/${tool.slug}`}
|
||||
className={cn(
|
||||
"relative flex h-16 shrink-0 items-center gap-2 text-sm leading-6 transition after:absolute after:inset-x-0 after:bottom-0 after:h-px",
|
||||
active
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { App } from "antd";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { GithubOutlined } from "@ant-design/icons";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Drawer } from "antd";
|
||||
import Link from "next/link";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { navigationTools, type NavigationToolSlug } from "@/constant/navigation-tools";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -22,7 +20,7 @@ export function MobileNavDrawer({ open, activeToolSlug, onClose }: MobileNavDraw
|
||||
return (
|
||||
<Link
|
||||
key={tool.slug}
|
||||
href={`/${tool.slug}`}
|
||||
to={`/${tool.slug}`}
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-3 text-base transition",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
import { BookOpen, Keyboard, Settings2 } from "lucide-react";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
import { Modal, Tag, Timeline } from "antd";
|
||||
import { useVersionCheck } from "@/hooks/use-version-check";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useId, useMemo, useState } from "react";
|
||||
import { Cpu } from "lucide-react";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Copy } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Button, Card, Tag } from "antd";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Check, Search } from "lucide-react";
|
||||
import { type UIEvent, useEffect, useState } from "react";
|
||||
import { App, Empty, Input, Modal, Spin, Tag } from "antd";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { flushSync } from "react-dom";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { animate, motion, useInView, useMotionValue, useReducedMotion, useTransform, type HTMLMotionProps } from "motion/react";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Select as SelectPrimitive } from "radix-ui"
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactNode } from "react";
|
||||
import { Switch } from "antd";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CanvasNodeType } from "./types";
|
||||
import type { CanvasNodeMetadata } from "./types";
|
||||
import { CanvasNodeType } from "@/types/canvas";
|
||||
import type { CanvasNodeMetadata } from "@/types/canvas";
|
||||
|
||||
type CanvasNodeSpec = {
|
||||
width: number;
|
||||
@@ -1,3 +1,3 @@
|
||||
export const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || "dev";
|
||||
export const APP_VERSION = __APP_VERSION__ || "dev";
|
||||
|
||||
export const DOCS_URL = process.env.NEXT_PUBLIC_DOC_URL || "https://docs.canvas.best";
|
||||
export const DOCS_URL = import.meta.env.VITE_DOC_URL || "https://docs.canvas.best";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { App } from "antd";
|
||||
import copy from "copy-to-clipboard";
|
||||
|
||||
|
||||
@@ -7,11 +7,7 @@ const latestVersionUrl = "https://raw.githubusercontent.com/basketikun/infinite-
|
||||
const latestChangelogUrl = "https://raw.githubusercontent.com/basketikun/infinite-canvas/main/CHANGELOG.md";
|
||||
|
||||
function readLocalReleases(): ReleaseInfo[] {
|
||||
try {
|
||||
return JSON.parse(process.env.NEXT_PUBLIC_APP_RELEASES || "[]");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return __APP_RELEASES__ || [];
|
||||
}
|
||||
|
||||
function toVersionParts(version: string) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { AppTopNav } from "@/components/layout/app-top-nav";
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { getNodeSpec } from "../constants";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata, type ViewportTransform } from "../types";
|
||||
import { getNodeSpec } from "@/constant/canvas";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata, type ViewportTransform } from "@/types/canvas";
|
||||
|
||||
export type CanvasAgentOp =
|
||||
| { type: "add_node"; id?: string; nodeType?: CanvasNodeType; title?: string; position?: { x: number; y: number }; x?: number; y?: number; width?: number; height?: number; metadata?: CanvasNodeMetadata }
|
||||
+2
-2
@@ -3,8 +3,8 @@ import { saveAs } from "file-saver";
|
||||
import { createZip } from "@/lib/zip";
|
||||
import { getMediaBlob } from "@/services/file-storage";
|
||||
import { getImageBlob } from "@/services/image-storage";
|
||||
import type { CanvasExportAsset, CanvasExportFile } from "../export-types";
|
||||
import type { CanvasProject } from "../stores/use-canvas-store";
|
||||
import type { CanvasExportAsset, CanvasExportFile } from "@/types/canvas-export";
|
||||
import type { CanvasProject } from "@/stores/canvas/use-canvas-store";
|
||||
|
||||
export async function exportCanvasProjects(projects: CanvasProject[], fileName = "无限画布") {
|
||||
const zipFiles: { name: string; data: BlobPart }[] = [];
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export type ImageCropRect = {
|
||||
x: number;
|
||||
y: number;
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export function fitNodeSize(width: number, height: number, maxWidth = 640, maxHeight = 640) {
|
||||
const w = Math.max(1, width);
|
||||
const h = Math.max(1, height);
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { imageReferenceLabel } from "@/lib/image-reference-prompt";
|
||||
import { seedanceReferenceLabel } from "@/lib/seedance-video";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "../types";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "@/types/canvas";
|
||||
|
||||
export type CanvasResourceKind = "image" | "video" | "audio" | "text";
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "antd/dist/reset.css";
|
||||
import "./styles/globals.css";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
|
||||
import { AppProviders } from "@/components/layout/app-providers";
|
||||
import { router } from "@/router";
|
||||
|
||||
document.body.style.fontFamily = '"SF Pro Display","SF Pro Text","PingFang SC","Microsoft YaHei","Helvetica Neue",sans-serif';
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<AppProviders>
|
||||
<RouterProvider router={router} />
|
||||
</AppProviders>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Copy, Download, PencilLine, Search, Trash2, Upload } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { App, Button, Card, Drawer, Empty, Form, Image, Input, Modal, Pagination, Select, Space, Tag, Typography } from "antd";
|
||||
@@ -1,24 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { App, Button } from "antd";
|
||||
import { Download, FileUp, Plus } from "lucide-react";
|
||||
|
||||
import { readZip } from "@/lib/zip";
|
||||
import { setMediaBlob } from "@/services/file-storage";
|
||||
import { setImageBlob } from "@/services/image-storage";
|
||||
import { CanvasDeleteProjectsDialog } from "./components/canvas-delete-projects-dialog";
|
||||
import { CanvasProjectCard } from "./components/canvas-project-card";
|
||||
import type { CanvasExportFile } from "./export-types";
|
||||
import { useCanvasStore } from "./stores/use-canvas-store";
|
||||
import { useCanvasUiStore } from "./stores/use-canvas-ui-store";
|
||||
import { exportCanvasProjects } from "./utils/canvas-export";
|
||||
import { CanvasDeleteProjectsDialog } from "@/components/canvas/canvas-delete-projects-dialog";
|
||||
import { CanvasProjectCard } from "@/components/canvas/canvas-project-card";
|
||||
import type { CanvasExportFile } from "@/types/canvas-export";
|
||||
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
import { useCanvasUiStore } from "@/stores/canvas/use-canvas-ui-store";
|
||||
import { exportCanvasProjects } from "@/lib/canvas/canvas-export";
|
||||
|
||||
export default function CanvasPage() {
|
||||
const { message } = App.useApp();
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const autoOpenRef = useRef(false);
|
||||
const hydrated = useCanvasStore((state) => state.hydrated);
|
||||
const projects = useCanvasStore((state) => state.projects);
|
||||
const createProject = useCanvasStore((state) => state.createProject);
|
||||
@@ -26,8 +26,11 @@ export default function CanvasPage() {
|
||||
const selectedIds = useCanvasUiStore((state) => state.selectedProjectIds);
|
||||
const setDeleteIds = useCanvasUiStore((state) => state.setDeleteProjectIds);
|
||||
|
||||
const mode = searchParams.get("mode");
|
||||
const agentMode = mode === "new" || mode === "recent" || mode === "choose";
|
||||
const agentQuery = agentMode ? `?${searchParams.toString()}` : "";
|
||||
const enterProject = (id: string) => {
|
||||
router.push(`/canvas/${id}`);
|
||||
navigate(`/canvas/${id}${agentQuery}`);
|
||||
};
|
||||
const createAndEnter = () => enterProject(createProject(`无限画布 ${projects.length + 1}`));
|
||||
const importCanvas = async (file?: File) => {
|
||||
@@ -56,6 +59,14 @@ export default function CanvasPage() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated || autoOpenRef.current || (mode !== "new" && mode !== "recent")) return;
|
||||
autoOpenRef.current = true;
|
||||
enterProject(mode === "new" ? createProject(`无限画布 ${projects.length + 1}`) : projects[0]?.id || createProject(`无限画布 ${projects.length + 1}`));
|
||||
}, [createProject, hydrated, mode, projects]);
|
||||
|
||||
if (hydrated && (mode === "new" || mode === "recent")) return <main className="flex h-full items-center justify-center bg-background text-sm text-stone-500">正在打开画布...</main>;
|
||||
|
||||
return (
|
||||
<main className="h-full overflow-auto bg-background text-stone-950 dark:text-stone-100">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-6 py-10">
|
||||
+205
-61
@@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import type { ChangeEvent as ReactChangeEvent, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { BookOpen, Bot, Home, ImageIcon, Images, List, Menu, Music2, Plus, Redo2, Settings2, Trash2, Undo2, Upload, Video } from "lucide-react";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
@@ -19,33 +17,35 @@ import { canvasThemes, type CanvasBackgroundMode } from "@/lib/canvas-theme";
|
||||
import { UserStatusActions } from "@/components/layout/user-status-actions";
|
||||
import { useAssetStore } from "@/stores/use-asset-store";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { cropDataUrl, splitDataUrl, upscaleDataUrl } from "../utils/canvas-image-data";
|
||||
import { fitNodeSize, nodeSizeFromRatio } from "../utils/canvas-node-size";
|
||||
import { cropDataUrl, splitDataUrl, upscaleDataUrl } from "@/lib/canvas/canvas-image-data";
|
||||
import { fitNodeSize, nodeSizeFromRatio } from "@/lib/canvas/canvas-node-size";
|
||||
import { App, Button, Dropdown, Modal } from "antd";
|
||||
import { NODE_DEFAULT_SIZE, getNodeSpec } from "../constants";
|
||||
import { ActiveConnectionPath, ConnectionPath } from "../components/canvas-connections";
|
||||
import { CanvasConfigComposer } from "../components/canvas-config-composer";
|
||||
import { CanvasConfigNodePanel } from "../components/canvas-config-node-panel";
|
||||
import { CANVAS_AGENT_PANEL_MOTION_MS, CanvasAssistantPanel } from "../components/canvas-assistant-panel";
|
||||
import { CanvasNodeContextMenu } from "../components/canvas-context-menu";
|
||||
import { CanvasNodeAngleDialog, type CanvasImageAngleParams } from "../components/canvas-node-angle-dialog";
|
||||
import { CanvasNodeCropDialog, type CanvasImageCropRect } from "../components/canvas-node-crop-dialog";
|
||||
import { CanvasNodeMaskEditDialog, type CanvasImageMaskEditPayload } from "../components/canvas-node-mask-edit-dialog";
|
||||
import { CanvasNodeSplitDialog, type CanvasImageSplitParams } from "../components/canvas-node-split-dialog";
|
||||
import { CanvasNodeUpscaleDialog, type CanvasImageUpscaleParams } from "../components/canvas-node-upscale-dialog";
|
||||
import { buildNodeGenerationContext, buildNodeGenerationInputs, buildNodeResponseMessages, hydrateNodeGenerationContext, type NodeGenerationInput } from "../components/canvas-node-generation";
|
||||
import { CanvasNodeHoverToolbar, CanvasNodeInfoModal } from "../components/canvas-node-hover-toolbar";
|
||||
import { InfiniteCanvas } from "../components/infinite-canvas";
|
||||
import { Minimap } from "../components/canvas-mini-map";
|
||||
import { CanvasNode } from "../components/canvas-node";
|
||||
import { CanvasNodePromptPanel, type CanvasNodeGenerationMode } from "../components/canvas-node-prompt-panel";
|
||||
import { CanvasToolbar } from "../components/canvas-toolbar";
|
||||
import { AssetPickerModal, type InsertAssetPayload } from "../components/asset-picker-modal";
|
||||
import { CanvasZoomControls } from "../components/canvas-zoom-controls";
|
||||
import { useCanvasStore } from "../stores/use-canvas-store";
|
||||
import { applyCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "../utils/canvas-agent-ops";
|
||||
import { buildCanvasResourceReferences, buildNodeMentionReferences } from "../utils/canvas-resource-references";
|
||||
import type { CanvasAgentMode } from "../components/canvas-agent-chat-ui";
|
||||
import { NODE_DEFAULT_SIZE, getNodeSpec } from "@/constant/canvas";
|
||||
import { ActiveConnectionPath, ConnectionPath } from "@/components/canvas/canvas-connections";
|
||||
import { CanvasConfigComposer } from "@/components/canvas/canvas-config-composer";
|
||||
import { CanvasConfigNodePanel } from "@/components/canvas/canvas-config-node-panel";
|
||||
import { CANVAS_AGENT_PANEL_MOTION_MS, CanvasAssistantPanel } from "@/components/canvas/canvas-assistant-panel";
|
||||
import { CanvasNodeContextMenu } from "@/components/canvas/canvas-context-menu";
|
||||
import { CanvasNodeAngleDialog, type CanvasImageAngleParams } from "@/components/canvas/canvas-node-angle-dialog";
|
||||
import { CanvasNodeCropDialog, type CanvasImageCropRect } from "@/components/canvas/canvas-node-crop-dialog";
|
||||
import { CanvasNodeMaskEditDialog, type CanvasImageMaskEditPayload } from "@/components/canvas/canvas-node-mask-edit-dialog";
|
||||
import { CanvasNodeSplitDialog, type CanvasImageSplitParams } from "@/components/canvas/canvas-node-split-dialog";
|
||||
import { CanvasNodeUpscaleDialog, type CanvasImageUpscaleParams } from "@/components/canvas/canvas-node-upscale-dialog";
|
||||
import { buildNodeGenerationContext, buildNodeGenerationInputs, buildNodeResponseMessages, hydrateNodeGenerationContext, type NodeGenerationInput } from "@/components/canvas/canvas-node-generation";
|
||||
import { CanvasNodeHoverToolbar, CanvasNodeInfoModal } from "@/components/canvas/canvas-node-hover-toolbar";
|
||||
import { InfiniteCanvas } from "@/components/canvas/infinite-canvas";
|
||||
import { Minimap } from "@/components/canvas/canvas-mini-map";
|
||||
import { CanvasNode } from "@/components/canvas/canvas-node";
|
||||
import { CanvasNodePromptPanel, type CanvasNodeGenerationMode } from "@/components/canvas/canvas-node-prompt-panel";
|
||||
import { CanvasToolbar } from "@/components/canvas/canvas-toolbar";
|
||||
import { AssetPickerModal, type InsertAssetPayload } from "@/components/canvas/asset-picker-modal";
|
||||
import { CanvasZoomControls } from "@/components/canvas/canvas-zoom-controls";
|
||||
import { CanvasLocalAgentPanel } from "@/components/canvas/canvas-local-agent-panel";
|
||||
import { useCanvasAgentStore } from "@/stores/canvas/use-canvas-agent-store";
|
||||
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
import { applyCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
||||
import { buildCanvasResourceReferences, buildNodeMentionReferences } from "@/lib/canvas/canvas-resource-references";
|
||||
import type { CanvasAgentMode } from "@/components/canvas/canvas-agent-chat-ui";
|
||||
import {
|
||||
CanvasNodeType,
|
||||
type CanvasAssistantImage,
|
||||
@@ -59,7 +59,7 @@ import {
|
||||
type Position,
|
||||
type SelectionBox,
|
||||
type ViewportTransform,
|
||||
} from "../types";
|
||||
} from "@/types/canvas";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
import type { ReferenceAudio } from "@/types/media";
|
||||
|
||||
@@ -85,10 +85,18 @@ type CanvasHistoryEntry = Pick<CanvasClipboard, "nodes" | "connections"> & {
|
||||
showImageInfo: boolean;
|
||||
};
|
||||
|
||||
type CanvasGenerationRequest = {
|
||||
targetNodeId: string;
|
||||
originNodeId: string;
|
||||
runningNodeId: string;
|
||||
controller: AbortController;
|
||||
};
|
||||
|
||||
const VIDEO_NODE_MAX_WIDTH = 420;
|
||||
const VIDEO_NODE_MAX_HEIGHT = 420;
|
||||
const CONNECTION_HANDLE_HIT_RADIUS = 40;
|
||||
const CONNECTION_NODE_HIT_PADDING = 32;
|
||||
const NODE_STATUS_IDLE = "idle" as const;
|
||||
const NODE_STATUS_LOADING = "loading" as const;
|
||||
const NODE_STATUS_SUCCESS = "success" as const;
|
||||
const NODE_STATUS_ERROR = "error" as const;
|
||||
@@ -208,10 +216,14 @@ function ConnectionCreateOption({ theme, icon, title, description, onClick }: {
|
||||
}
|
||||
|
||||
function InfiniteCanvasPage() {
|
||||
const { message } = App.useApp();
|
||||
const { message, modal } = App.useApp();
|
||||
const params = useParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
const projectId = params.id;
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const projectId = params.id || "";
|
||||
const localAgentConnected = useCanvasAgentStore((state) => state.connected);
|
||||
const localAgentActivity = useCanvasAgentStore((state) => state.activity);
|
||||
const localAgentEnabled = useCanvasAgentStore((state) => state.enabled);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||
const uploadTargetRef = useRef<{ nodeId?: string; position?: Position } | null>(null);
|
||||
@@ -294,6 +306,8 @@ function InfiniteCanvasPage() {
|
||||
const [assistantClosing, setAssistantClosing] = useState(false);
|
||||
const [agentMode, setAgentMode] = useState<CanvasAgentMode>("online");
|
||||
const [agentUndoSnapshot, setAgentUndoSnapshot] = useState<CanvasAgentSnapshot | null>(null);
|
||||
const codexAutoConnect = ["new", "recent", "choose"].includes(searchParams.get("mode") || "");
|
||||
const codexCompactAgent = codexAutoConnect && searchParams.has("agentUrl");
|
||||
const [titleEditing, setTitleEditing] = useState(false);
|
||||
const [titleDraft, setTitleDraft] = useState("");
|
||||
const [historyState, setHistoryState] = useState({ canUndo: false, canRedo: false });
|
||||
@@ -311,6 +325,7 @@ function InfiniteCanvasPage() {
|
||||
const selectionBoxRef = useRef(selectionBox);
|
||||
const agentCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pendingConnectionCreateRef = useRef(pendingConnectionCreate);
|
||||
const generationRequestsRef = useRef(new Map<string, CanvasGenerationRequest>());
|
||||
|
||||
const createHistoryEntry = useCallback(
|
||||
(): CanvasHistoryEntry => ({
|
||||
@@ -331,12 +346,58 @@ function InfiniteCanvasPage() {
|
||||
[cleanupAssetImages],
|
||||
);
|
||||
|
||||
const startGenerationRequest = useCallback((targetNodeId: string, originNodeId: string, runningId = originNodeId, controller = new AbortController()) => {
|
||||
const previous = generationRequestsRef.current.get(targetNodeId);
|
||||
if (previous?.controller !== controller) previous?.controller.abort();
|
||||
generationRequestsRef.current.set(targetNodeId, { targetNodeId, originNodeId, runningNodeId: runningId, controller });
|
||||
return controller;
|
||||
}, []);
|
||||
|
||||
const finishGenerationRequest = useCallback((targetNodeId: string, controller: AbortController) => {
|
||||
const request = generationRequestsRef.current.get(targetNodeId);
|
||||
if (request?.controller === controller) generationRequestsRef.current.delete(targetNodeId);
|
||||
}, []);
|
||||
|
||||
const stopGenerationByRunningId = useCallback((runningId: string) => {
|
||||
const affectedNodeIds = new Set<string>();
|
||||
generationRequestsRef.current.forEach((request) => {
|
||||
if (request.runningNodeId !== runningId) return;
|
||||
request.controller.abort();
|
||||
generationRequestsRef.current.delete(request.targetNodeId);
|
||||
affectedNodeIds.add(request.targetNodeId);
|
||||
affectedNodeIds.add(request.originNodeId);
|
||||
});
|
||||
setRunningNodeId((current) => (current === runningId ? null : current));
|
||||
if (!affectedNodeIds.size) return;
|
||||
setNodes((prev) =>
|
||||
prev.map((node) =>
|
||||
affectedNodeIds.has(node.id) && node.metadata?.status === NODE_STATUS_LOADING
|
||||
? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_IDLE, errorDetails: undefined } }
|
||||
: node,
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const confirmStopGeneration = useCallback(
|
||||
(nodeId: string) => {
|
||||
modal.confirm({
|
||||
title: "停止生成?",
|
||||
content: "当前生成请求会被中断,已经生成完成的内容会保留。",
|
||||
okText: "停止",
|
||||
cancelText: "继续生成",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => stopGenerationByRunningId(nodeId),
|
||||
});
|
||||
},
|
||||
[modal, stopGenerationByRunningId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated) return;
|
||||
setProjectLoaded(false);
|
||||
const project = openProject(projectId);
|
||||
if (!project) {
|
||||
router.replace("/canvas");
|
||||
navigate("/canvas", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -367,7 +428,16 @@ function InfiniteCanvasPage() {
|
||||
setProjectLoaded(true);
|
||||
};
|
||||
void restore();
|
||||
}, [hydrated, openProject, projectId, router]);
|
||||
}, [hydrated, navigate, openProject, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectLoaded || !["new", "recent", "choose"].includes(searchParams.get("mode") || "")) return;
|
||||
if (searchParams.has("agentUrl")) {
|
||||
setAgentMode("local");
|
||||
return;
|
||||
}
|
||||
openAgent("local");
|
||||
}, [projectLoaded, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectLoaded || applyingHistoryRef.current || historyPausedRef.current) return;
|
||||
@@ -960,14 +1030,14 @@ function InfiniteCanvasPage() {
|
||||
|
||||
const createAndOpenProject = useCallback(() => {
|
||||
const id = createProject(`无限画布 ${useCanvasStore.getState().projects.length + 1}`);
|
||||
router.push(`/canvas/${id}`);
|
||||
}, [createProject, router]);
|
||||
navigate(`/canvas/${id}`);
|
||||
}, [createProject, navigate]);
|
||||
|
||||
const deleteCurrentProject = useCallback(() => {
|
||||
deleteProjects([projectId]);
|
||||
cleanupAssetImages();
|
||||
router.push("/canvas");
|
||||
}, [cleanupAssetImages, deleteProjects, projectId, router]);
|
||||
navigate("/canvas");
|
||||
}, [cleanupAssetImages, deleteProjects, navigate, projectId]);
|
||||
|
||||
const handleCanvasMouseDown = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
@@ -1653,20 +1723,23 @@ function InfiniteCanvasPage() {
|
||||
setSelectedNodeIds(new Set([childId]));
|
||||
setSelectedConnectionId(null);
|
||||
setDialogNodeId(childId);
|
||||
const controller = startGenerationRequest(childId, node.id, childId);
|
||||
try {
|
||||
const image = await requestEdit(generationConfig, prompt, [source], { id: `${node.id}-mask`, name: "mask.png", type: "image/png", dataUrl: payload.maskDataUrl }).then((items) => items[0]);
|
||||
const image = await requestEdit(generationConfig, prompt, [source], { id: `${node.id}-mask`, name: "mask.png", type: "image/png", dataUrl: payload.maskDataUrl }, { signal: controller.signal }).then((items) => items[0]);
|
||||
const uploaded = await uploadImage(image.dataUrl);
|
||||
const size = fitNodeSize(uploaded.width, uploaded.height, node.width, node.height);
|
||||
setNodes((prev) => prev.map((item) => (item.id === childId ? { ...item, width: size.width, height: size.height, metadata: { ...item.metadata, ...imageMetadata(uploaded), prompt, ...generationMetadata } } : item)));
|
||||
} catch (error) {
|
||||
if (isGenerationCanceled(error)) return;
|
||||
const errorDetails = error instanceof Error ? error.message : "局部修改失败";
|
||||
message.error(errorDetails);
|
||||
setNodes((prev) => prev.map((item) => (item.id === childId ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails } } : item)));
|
||||
} finally {
|
||||
finishGenerationRequest(childId, controller);
|
||||
setRunningNodeId(null);
|
||||
}
|
||||
},
|
||||
[effectiveConfig, isAiConfigReady, message, openConfigDialog],
|
||||
[effectiveConfig, finishGenerationRequest, isAiConfigReady, message, openConfigDialog, startGenerationRequest],
|
||||
);
|
||||
|
||||
const upscaleImageNode = useCallback(async (node: CanvasNodeData, params: CanvasImageUpscaleParams) => {
|
||||
@@ -1726,21 +1799,24 @@ function InfiniteCanvasPage() {
|
||||
setConnections((prev) => [...prev, { id: nanoid(), fromNodeId: node.id, toNodeId: childId }]);
|
||||
setSelectedNodeIds(new Set([childId]));
|
||||
setDialogNodeId(childId);
|
||||
const controller = startGenerationRequest(childId, node.id, childId);
|
||||
try {
|
||||
const image = await requestEdit(generationConfig, prompt, [{ id: node.id, name: `${node.title || node.id}.png`, type: node.metadata.mimeType || "image/png", dataUrl: node.metadata.content, storageKey: node.metadata.storageKey }]).then(
|
||||
const image = await requestEdit(generationConfig, prompt, [{ id: node.id, name: `${node.title || node.id}.png`, type: node.metadata.mimeType || "image/png", dataUrl: node.metadata.content, storageKey: node.metadata.storageKey }], undefined, { signal: controller.signal }).then(
|
||||
(items) => items[0],
|
||||
);
|
||||
const uploaded = await uploadImage(image.dataUrl);
|
||||
const size = fitNodeSize(uploaded.width, uploaded.height, imageConfig.width, imageConfig.height);
|
||||
setNodes((prev) => prev.map((item) => (item.id === childId ? { ...item, width: size.width, height: size.height, metadata: { ...item.metadata, ...imageMetadata(uploaded), prompt, ...generationMetadata } } : item)));
|
||||
} catch (error) {
|
||||
if (isGenerationCanceled(error)) return;
|
||||
const errorDetails = error instanceof Error ? error.message : "生成失败";
|
||||
setNodes((prev) => prev.map((item) => (item.id === childId ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails } } : item)));
|
||||
} finally {
|
||||
finishGenerationRequest(childId, controller);
|
||||
setRunningNodeId(null);
|
||||
}
|
||||
},
|
||||
[effectiveConfig, openConfigDialog],
|
||||
[effectiveConfig, finishGenerationRequest, openConfigDialog, startGenerationRequest],
|
||||
);
|
||||
|
||||
const handleFontSizeChange = useCallback((nodeId: string, fontSize: number) => {
|
||||
@@ -1880,15 +1956,22 @@ function InfiniteCanvasPage() {
|
||||
}
|
||||
|
||||
setRunningNodeId(nodeId);
|
||||
const runController = startGenerationRequest(nodeId, nodeId, nodeId);
|
||||
const sourceTextContent = sourceNode?.type === CanvasNodeType.Text ? sourceNode.metadata?.content?.trim() || "" : "";
|
||||
const editingTextNode = mode === "text" && Boolean(sourceTextContent);
|
||||
const generationContext = await hydrateNodeGenerationContext(
|
||||
buildNodeGenerationContext(nodeId, nodesRef.current, connectionsRef.current, editingTextNode ? `请根据要求修改以下文本。\n\n原文:\n${sourceTextContent}\n\n修改要求:\n${prompt}` : prompt),
|
||||
);
|
||||
const effectivePrompt = generationContext.prompt.trim();
|
||||
if (runController.signal.aborted) {
|
||||
finishGenerationRequest(nodeId, runController);
|
||||
setRunningNodeId(null);
|
||||
return;
|
||||
}
|
||||
const markSourceStatus = sourceNode?.type !== CanvasNodeType.Image && !editingTextNode;
|
||||
const statusPrompt = sourceNode?.type === CanvasNodeType.Config ? effectivePrompt : prompt;
|
||||
if (!effectivePrompt && (mode === "text" || mode === "audio")) {
|
||||
finishGenerationRequest(nodeId, runController);
|
||||
setRunningNodeId(null);
|
||||
return;
|
||||
}
|
||||
@@ -1991,14 +2074,17 @@ function InfiniteCanvasPage() {
|
||||
setSelectedConnectionId(null);
|
||||
setDialogNodeId(nodeId);
|
||||
|
||||
const controller = runController;
|
||||
targetIds.forEach((targetId) => startGenerationRequest(targetId, nodeId, nodeId, controller));
|
||||
if (count > 1) startGenerationRequest(rootId, nodeId, nodeId, controller);
|
||||
let hasSuccess = false;
|
||||
let hasFailure = false;
|
||||
await Promise.all(
|
||||
targetIds.map(async (targetId) => {
|
||||
try {
|
||||
const image = referenceImages.length
|
||||
? await requestEdit({ ...generationConfig, count: "1" }, effectivePrompt, referenceImages).then((items) => items[0])
|
||||
: await requestGeneration({ ...generationConfig, count: "1" }, effectivePrompt).then((items) => items[0]);
|
||||
? await requestEdit({ ...generationConfig, count: "1" }, effectivePrompt, referenceImages, undefined, { signal: controller.signal }).then((items) => items[0])
|
||||
: await requestGeneration({ ...generationConfig, count: "1" }, effectivePrompt, { signal: controller.signal }).then((items) => items[0]);
|
||||
const uploaded = await uploadImage(image.dataUrl);
|
||||
const imageSize = fitNodeSize(uploaded.width, uploaded.height, imageConfig.width, imageConfig.height);
|
||||
setNodes((prev) => {
|
||||
@@ -2029,13 +2115,21 @@ function InfiniteCanvasPage() {
|
||||
if (isConfigNode) setNodes((prev) => prev.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_SUCCESS, errorDetails: undefined } } : node)));
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isGenerationCanceled(error)) return false;
|
||||
const errorDetails = error instanceof Error ? error.message : "生成失败";
|
||||
hasFailure = true;
|
||||
setNodes((prev) => prev.map((node) => (node.id === targetId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_ERROR, errorDetails } } : node)));
|
||||
return false;
|
||||
} finally {
|
||||
finishGenerationRequest(targetId, controller);
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
if (count > 1) finishGenerationRequest(rootId, controller);
|
||||
if (controller.signal.aborted) {
|
||||
setNodes((prev) => prev.map((node) => (node.id === nodeId && isConfigNode && node.metadata?.status === NODE_STATUS_LOADING ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_IDLE, errorDetails: undefined } } : node)));
|
||||
return;
|
||||
}
|
||||
if (hasFailure) message.error(hasSuccess ? "部分图片生成失败" : "全部图片生成失败");
|
||||
setNodes((prev) =>
|
||||
prev.map((node) =>
|
||||
@@ -2068,9 +2162,14 @@ function InfiniteCanvasPage() {
|
||||
pendingChildIds = [videoId];
|
||||
setNodes((prev) => (isEmptyVideoNode ? prev.map((node) => (node.id === nodeId ? { ...node, ...videoNode } : node)) : [...prev.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_SUCCESS } } : node)), videoNode]));
|
||||
if (!isEmptyVideoNode) setConnections((prev) => [...prev, { id: nanoid(), fromNodeId: nodeId, toNodeId: videoId }]);
|
||||
const video = await storeGeneratedVideo(await requestVideoGeneration(generationConfig, effectivePrompt, generationContext.referenceImages, generationContext.referenceVideos, generationContext.referenceAudios));
|
||||
const videoSize = fitNodeSize(video.width || spec.width, video.height || spec.height, VIDEO_NODE_MAX_WIDTH, VIDEO_NODE_MAX_HEIGHT);
|
||||
setNodes((prev) => prev.map((node) => (node.id === videoId ? { ...node, width: videoSize.width, height: videoSize.height, position: { x: node.position.x + node.width / 2 - videoSize.width / 2, y: node.position.y + node.height / 2 - videoSize.height / 2 }, metadata: { ...node.metadata, ...videoMetadata(video), prompt: effectivePrompt, model: generationConfig.model, size: generationConfig.size, seconds: generationConfig.videoSeconds, vquality: generationConfig.vquality, generateAudio: generationConfig.videoGenerateAudio, watermark: generationConfig.videoWatermark, references: generationReferenceUrls(generationContext) } } : node)));
|
||||
const controller = startGenerationRequest(videoId, nodeId, nodeId, runController);
|
||||
try {
|
||||
const video = await storeGeneratedVideo(await requestVideoGeneration(generationConfig, effectivePrompt, generationContext.referenceImages, generationContext.referenceVideos, generationContext.referenceAudios, { signal: controller.signal }));
|
||||
const videoSize = fitNodeSize(video.width || spec.width, video.height || spec.height, VIDEO_NODE_MAX_WIDTH, VIDEO_NODE_MAX_HEIGHT);
|
||||
setNodes((prev) => prev.map((node) => (node.id === videoId ? { ...node, width: videoSize.width, height: videoSize.height, position: { x: node.position.x + node.width / 2 - videoSize.width / 2, y: node.position.y + node.height / 2 - videoSize.height / 2 }, metadata: { ...node.metadata, ...videoMetadata(video), prompt: effectivePrompt, model: generationConfig.model, size: generationConfig.size, seconds: generationConfig.videoSeconds, vquality: generationConfig.vquality, generateAudio: generationConfig.videoGenerateAudio, watermark: generationConfig.videoWatermark, references: generationReferenceUrls(generationContext) } } : node)));
|
||||
} finally {
|
||||
finishGenerationRequest(videoId, controller);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2091,8 +2190,13 @@ function InfiniteCanvasPage() {
|
||||
pendingChildIds = [audioId];
|
||||
setNodes((prev) => (isEmptyAudioNode ? prev.map((node) => (node.id === nodeId ? { ...node, ...audioNode } : node)) : [...prev.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_SUCCESS } } : node)), audioNode]));
|
||||
if (!isEmptyAudioNode) setConnections((prev) => [...prev, { id: nanoid(), fromNodeId: nodeId, toNodeId: audioId }]);
|
||||
const audio = await storeGeneratedAudio(await requestAudioGeneration(generationConfig, effectivePrompt), generationConfig.audioFormat);
|
||||
setNodes((prev) => prev.map((node) => (node.id === audioId ? { ...node, metadata: { ...node.metadata, ...audioMetadata(audio), prompt: effectivePrompt, ...buildAudioGenerationMetadata(generationConfig) } } : node)));
|
||||
const controller = startGenerationRequest(audioId, nodeId, nodeId, runController);
|
||||
try {
|
||||
const audio = await storeGeneratedAudio(await requestAudioGeneration(generationConfig, effectivePrompt, { signal: controller.signal }), generationConfig.audioFormat);
|
||||
setNodes((prev) => prev.map((node) => (node.id === audioId ? { ...node, metadata: { ...node.metadata, ...audioMetadata(audio), prompt: effectivePrompt, ...buildAudioGenerationMetadata(generationConfig) } } : node)));
|
||||
} finally {
|
||||
finishGenerationRequest(audioId, controller);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2121,17 +2225,21 @@ function InfiniteCanvasPage() {
|
||||
setConnections((prev) => [...prev, ...childIds.map((childId) => ({ id: nanoid(), fromNodeId: nodeId, toNodeId: childId }))]);
|
||||
}
|
||||
|
||||
const controller = runController;
|
||||
const textTargetIds = childIds.length ? childIds : [nodeId];
|
||||
textTargetIds.forEach((targetNodeId) => startGenerationRequest(targetNodeId, nodeId, nodeId, controller));
|
||||
const answers = await Promise.all(
|
||||
(childIds.length ? childIds : [nodeId]).map((targetNodeId) => {
|
||||
textTargetIds.map((targetNodeId) => {
|
||||
let localStreamed = "";
|
||||
return requestImageQuestion(generationConfig, buildNodeResponseMessages({ ...generationContext, prompt: effectivePrompt }), (text) => {
|
||||
localStreamed = text;
|
||||
streamed = text;
|
||||
if (isConfigNode) return;
|
||||
setNodes((prev) => prev.map((node) => (node.id === targetNodeId ? { ...node, type: CanvasNodeType.Text, metadata: { ...node.metadata, content: text, status: NODE_STATUS_LOADING } } : node)));
|
||||
}).then((answer) => ({ nodeId: targetNodeId, content: answer || localStreamed }));
|
||||
}, { signal: controller.signal }).then((answer) => ({ nodeId: targetNodeId, content: answer || localStreamed })).finally(() => finishGenerationRequest(targetNodeId, controller));
|
||||
}),
|
||||
);
|
||||
if (controller.signal.aborted) return;
|
||||
const answerByNodeId = new Map(answers.map((item) => [item.nodeId, item.content]));
|
||||
setNodes((prev) =>
|
||||
prev.map((node) =>
|
||||
@@ -2145,16 +2253,18 @@ function InfiniteCanvasPage() {
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (isGenerationCanceled(error)) return;
|
||||
const errorDetails = error instanceof Error ? error.message : "生成失败";
|
||||
message.error(errorDetails);
|
||||
setNodes((prev) =>
|
||||
prev.map((node) => (node.id === nodeId || pendingChildIds.includes(node.id) ? (node.id === nodeId && !markSourceStatus ? node : { ...node, metadata: { ...node.metadata, status: NODE_STATUS_ERROR, errorDetails } }) : node)),
|
||||
);
|
||||
} finally {
|
||||
finishGenerationRequest(nodeId, runController);
|
||||
setRunningNodeId(null);
|
||||
}
|
||||
},
|
||||
[effectiveConfig, openConfigDialog],
|
||||
[effectiveConfig, finishGenerationRequest, isAiConfigReady, message, openConfigDialog, startGenerationRequest],
|
||||
);
|
||||
useEffect(() => {
|
||||
generateNodeRef.current = handleGenerateNode;
|
||||
@@ -2200,6 +2310,7 @@ function InfiniteCanvasPage() {
|
||||
|
||||
setRunningNodeId(node.id);
|
||||
setNodes((prev) => prev.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_LOADING, errorDetails: undefined } } : item)));
|
||||
const controller = startGenerationRequest(node.id, sourceNode.id, node.id);
|
||||
|
||||
try {
|
||||
if (node.type === CanvasNodeType.Text) {
|
||||
@@ -2208,23 +2319,23 @@ function InfiniteCanvasPage() {
|
||||
const answer = await requestImageQuestion(generationConfig, buildNodeResponseMessages({ ...context, prompt }), (text) => {
|
||||
streamed = text;
|
||||
setNodes((prev) => prev.map((item) => (item.id === node.id ? { ...item, type: CanvasNodeType.Text, metadata: { ...item.metadata, content: text, status: NODE_STATUS_LOADING } } : item)));
|
||||
});
|
||||
}, { signal: controller.signal });
|
||||
setNodes((prev) => prev.map((item) => (item.id === node.id ? { ...item, type: CanvasNodeType.Text, metadata: { ...item.metadata, content: answer || streamed, prompt, status: NODE_STATUS_SUCCESS } } : item)));
|
||||
return;
|
||||
}
|
||||
if (node.type === CanvasNodeType.Video) {
|
||||
const video = await storeGeneratedVideo(await requestVideoGeneration(generationConfig, prompt, retryImages, context?.referenceVideos || [], context?.referenceAudios || []));
|
||||
const video = await storeGeneratedVideo(await requestVideoGeneration(generationConfig, prompt, retryImages, context?.referenceVideos || [], context?.referenceAudios || [], { signal: controller.signal }));
|
||||
const videoSize = fitNodeSize(video.width || node.width, video.height || node.height, VIDEO_NODE_MAX_WIDTH, VIDEO_NODE_MAX_HEIGHT);
|
||||
setNodes((prev) => prev.map((item) => (item.id === node.id ? { ...item, width: videoSize.width, height: videoSize.height, position: { x: item.position.x + item.width / 2 - videoSize.width / 2, y: item.position.y + item.height / 2 - videoSize.height / 2 }, metadata: { ...item.metadata, ...videoMetadata(video), prompt, model: generationConfig.model, size: generationConfig.size, seconds: generationConfig.videoSeconds, vquality: generationConfig.vquality, generateAudio: generationConfig.videoGenerateAudio, watermark: generationConfig.videoWatermark } } : item)));
|
||||
return;
|
||||
}
|
||||
if (node.type === CanvasNodeType.Audio) {
|
||||
const audio = await storeGeneratedAudio(await requestAudioGeneration(generationConfig, prompt), generationConfig.audioFormat);
|
||||
const audio = await storeGeneratedAudio(await requestAudioGeneration(generationConfig, prompt, { signal: controller.signal }), generationConfig.audioFormat);
|
||||
setNodes((prev) => prev.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, ...audioMetadata(audio), prompt, ...buildAudioGenerationMetadata(generationConfig) } } : item)));
|
||||
return;
|
||||
}
|
||||
|
||||
const image = useReferenceImages ? await requestEdit(generationConfig, prompt, retryImages).then((items) => items[0]) : await requestGeneration(generationConfig, prompt).then((items) => items[0]);
|
||||
const image = useReferenceImages ? await requestEdit(generationConfig, prompt, retryImages, undefined, { signal: controller.signal }).then((items) => items[0]) : await requestGeneration(generationConfig, prompt, { signal: controller.signal }).then((items) => items[0]);
|
||||
const uploadedImage = await uploadImage(image.dataUrl);
|
||||
const imageConfig = NODE_DEFAULT_SIZE[CanvasNodeType.Image];
|
||||
const imageSize = fitNodeSize(uploadedImage.width, uploadedImage.height, imageConfig.width, imageConfig.height);
|
||||
@@ -2245,14 +2356,16 @@ function InfiniteCanvasPage() {
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (isGenerationCanceled(error)) return;
|
||||
const errorDetails = error instanceof Error ? error.message : "生成失败";
|
||||
message.error(errorDetails);
|
||||
setNodes((prev) => prev.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails } } : item)));
|
||||
} finally {
|
||||
finishGenerationRequest(node.id, controller);
|
||||
setRunningNodeId(null);
|
||||
}
|
||||
},
|
||||
[effectiveConfig, message, openConfigDialog],
|
||||
[effectiveConfig, finishGenerationRequest, isAiConfigReady, message, openConfigDialog, startGenerationRequest],
|
||||
);
|
||||
|
||||
const generateImageFromTextNode = useCallback(
|
||||
@@ -2388,14 +2501,15 @@ function InfiniteCanvasPage() {
|
||||
onCancelTitleEditing={() => setTitleEditing(false)}
|
||||
canUndo={historyState.canUndo}
|
||||
canRedo={historyState.canRedo}
|
||||
onHome={() => router.push("/")}
|
||||
onProjects={() => router.push("/canvas")}
|
||||
onHome={() => navigate("/")}
|
||||
onProjects={() => navigate("/canvas")}
|
||||
onCreateProject={createAndOpenProject}
|
||||
onDeleteProject={deleteCurrentProject}
|
||||
onImportImage={() => handleUploadRequest()}
|
||||
onUndo={undoCanvas}
|
||||
onRedo={redoCanvas}
|
||||
agentOpen={assistantOpen}
|
||||
compactAgentStatus={codexCompactAgent ? { connected: localAgentConnected, enabled: localAgentEnabled, activity: localAgentActivity } : undefined}
|
||||
onToggleAgent={() => (assistantOpen ? closeAgent() : openAgent())}
|
||||
/>
|
||||
|
||||
@@ -2484,6 +2598,7 @@ function InfiniteCanvasPage() {
|
||||
onPromptChange={handleNodePromptChange}
|
||||
onConfigChange={handleConfigNodeChange}
|
||||
onGenerate={handleGenerateNode}
|
||||
onStop={confirmStopGeneration}
|
||||
onImageSettingsOpenChange={(open) => {
|
||||
setNodeImageSettingsOpen(open);
|
||||
if (open) setToolbarNodeId(null);
|
||||
@@ -2498,6 +2613,7 @@ function InfiniteCanvasPage() {
|
||||
inputSummary={getInputSummary(configInputsById.get(contentNode.id) || [])}
|
||||
onConfigChange={handleConfigNodeChange}
|
||||
onComposerToggle={() => setDialogNodeId((current) => (current === contentNode.id ? null : contentNode.id))}
|
||||
onStop={confirmStopGeneration}
|
||||
onGenerate={(nodeId) => {
|
||||
const target = nodesRef.current.find((item) => item.id === nodeId);
|
||||
void handleGenerateNode(nodeId, target?.metadata?.generationMode || "image", target?.metadata?.composerContent ?? target?.metadata?.prompt ?? "");
|
||||
@@ -2675,6 +2791,7 @@ function InfiniteCanvasPage() {
|
||||
</Modal>
|
||||
|
||||
<AssetPickerModal open={assetPickerOpen} onInsert={handleAssetInsert} onClose={() => setAssetPickerOpen(false)} />
|
||||
{codexCompactAgent && !assistantMounted ? <CanvasLocalAgentPanel headless snapshot={agentSnapshot} canUndoOps={Boolean(agentUndoSnapshot)} onApplyOps={applyAgentOps} onUndoOps={undoAgentOps} autoConnect={codexAutoConnect} /> : null}
|
||||
</section>
|
||||
{assistantMounted ? (
|
||||
<CanvasAssistantPanel
|
||||
@@ -2691,6 +2808,7 @@ function InfiniteCanvasPage() {
|
||||
onPasteImage={pasteAssistantImage}
|
||||
agentMode={agentMode}
|
||||
onAgentModeChange={setAgentMode}
|
||||
autoConnectLocal={codexAutoConnect}
|
||||
closing={assistantClosing}
|
||||
onCollapse={closeAgent}
|
||||
/>
|
||||
@@ -2717,6 +2835,7 @@ function CanvasTopBar({
|
||||
onUndo,
|
||||
onRedo,
|
||||
agentOpen,
|
||||
compactAgentStatus,
|
||||
onToggleAgent,
|
||||
}: {
|
||||
title: string;
|
||||
@@ -2736,6 +2855,7 @@ function CanvasTopBar({
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
agentOpen: boolean;
|
||||
compactAgentStatus?: { connected: boolean; enabled: boolean; activity: string };
|
||||
onToggleAgent: () => void;
|
||||
}) {
|
||||
const colorTheme = useThemeStore((state) => state.theme);
|
||||
@@ -2807,6 +2927,7 @@ function CanvasTopBar({
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-auto flex items-center gap-1.5">
|
||||
{compactAgentStatus ? <CompactAgentStatus status={compactAgentStatus} onClick={onToggleAgent} /> : null}
|
||||
<UserStatusActions
|
||||
variant="canvas"
|
||||
onOpenShortcuts={() => setShortcutsOpen(true)}
|
||||
@@ -2853,6 +2974,25 @@ function MenuLabel({ text, shortcut }: { text: string; shortcut: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function CompactAgentStatus({ status, onClick }: { status: { connected: boolean; enabled: boolean; activity: string }; onClick: () => void }) {
|
||||
const colorTheme = useThemeStore((state) => state.theme);
|
||||
const theme = canvasThemes[colorTheme];
|
||||
const label = status.connected ? "已连接到本地 Codex" : status.enabled ? status.activity || "连接中" : "正在连接本地 Codex";
|
||||
const dotColor = status.connected ? "#22c55e" : status.enabled ? "#f59e0b" : theme.node.muted;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-10 items-center gap-2 rounded-xl px-3 text-sm font-medium transition hover:opacity-85"
|
||||
style={{ background: theme.toolbar.panel, color: theme.node.text, boxShadow: "0 10px 30px rgba(28,25,23,.10)" }}
|
||||
onClick={onClick}
|
||||
title="打开本地 Codex 面板"
|
||||
>
|
||||
<span className="size-2 rounded-full" style={{ background: dotColor }} />
|
||||
<span className="max-w-[180px] truncate">{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Shortcut({ keys, value }: { keys: string[]; value: string }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_120px] items-center gap-6 rounded-lg px-1 py-1.5">
|
||||
@@ -3041,6 +3181,10 @@ function resetInterruptedGeneration(nodes: CanvasNodeData[]) {
|
||||
return nodes.map((node) => (node.metadata?.status === "loading" ? { ...node, metadata: { ...node.metadata, status: "error" as const, errorDetails: "页面刷新后生成已中断,请重新生成。" } } : node));
|
||||
}
|
||||
|
||||
function isGenerationCanceled(error: unknown) {
|
||||
return error instanceof Error && (error.message === "请求已取消" || error.name === "AbortError");
|
||||
}
|
||||
|
||||
function findRetrySourceNode(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[]) {
|
||||
const queue = connections.filter((connection) => connection.toNodeId === nodeId).map((connection) => connection.fromNodeId);
|
||||
const visited = new Set<string>();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user