From dbef018fd145484014742e4218a749bda088990f Mon Sep 17 00:00:00 2001 From: MT-Fire <798521692@qq.com> Date: Fri, 13 Mar 2026 20:08:11 +0800 Subject: [PATCH] Update frontend from deer-flow and add frontend_backup to gitignore --- .gitignore | 3 + docker/nginx/nginx.local.conf | 8 +- frontend/Dockerfile | 39 +- frontend/pnpm-lock.yaml | 269 ++++----- .../src/app/mock/api/threads/search/route.ts | 77 ++- .../[agent_name]/chats/[thread_id]/layout.tsx | 19 + .../[agent_name]/chats/[thread_id]/page.tsx | 181 ++++++ .../src/app/workspace/agents/new/page.tsx | 252 ++++++++ frontend/src/app/workspace/agents/page.tsx | 5 + .../app/workspace/chats/[thread_id]/page.tsx | 556 +++--------------- frontend/src/app/workspace/layout.tsx | 10 +- .../src/components/ai-elements/message.tsx | 3 +- .../landing/sections/case-study-section.tsx | 2 +- .../components/workspace/agent-welcome.tsx | 36 ++ .../workspace/agents/agent-card.tsx | 140 +++++ .../workspace/agents/agent-gallery.tsx | 69 +++ .../artifacts/artifact-file-detail.tsx | 38 +- .../workspace/artifacts/artifact-trigger.tsx | 30 + .../workspace/artifacts/context.tsx | 34 +- .../components/workspace/artifacts/index.ts | 1 + .../components/workspace/chats/chat-box.tsx | 172 ++++++ .../src/components/workspace/chats/index.ts | 3 + .../workspace/chats/use-chat-mode.ts | 41 ++ .../workspace/chats/use-thread-chat.ts | 29 + .../src/components/workspace/input-box.tsx | 492 ++++++++++++++-- .../components/workspace/messages/context.ts | 6 +- .../workspace/messages/message-list-item.tsx | 240 ++++---- .../workspace/messages/message-list.tsx | 30 +- .../settings/appearance-settings-page.tsx | 8 +- .../src/components/workspace/thread-title.tsx | 45 +- .../src/components/workspace/todo-list.tsx | 19 +- .../workspace/workspace-nav-chat-list.tsx | 13 +- frontend/src/core/agents/api.ts | 67 +++ frontend/src/core/agents/hooks.ts | 64 ++ frontend/src/core/agents/index.ts | 3 + frontend/src/core/agents/types.ts | 22 + frontend/src/core/api/api-client.ts | 32 +- frontend/src/core/api/stream-mode.test.ts | 43 ++ frontend/src/core/api/stream-mode.ts | 68 +++ frontend/src/core/artifacts/hooks.ts | 7 +- frontend/src/core/artifacts/loader.ts | 8 +- frontend/src/core/artifacts/utils.ts | 5 + frontend/src/core/config/index.ts | 7 +- frontend/src/core/i18n/hooks.ts | 27 +- frontend/src/core/i18n/index.ts | 28 +- frontend/src/core/i18n/locale.ts | 36 ++ frontend/src/core/i18n/locales/en-US.ts | 57 ++ frontend/src/core/i18n/locales/types.ts | 48 ++ frontend/src/core/i18n/locales/zh-CN.ts | 49 ++ frontend/src/core/i18n/server.ts | 14 +- frontend/src/core/messages/utils.ts | 203 ++++--- frontend/src/core/models/api.ts | 4 +- frontend/src/core/models/hooks.ts | 1 + frontend/src/core/models/types.ts | 1 + frontend/src/core/settings/hooks.ts | 8 +- frontend/src/core/settings/local.ts | 2 + frontend/src/core/skills/api.ts | 80 --- frontend/src/core/threads/hooks.ts | 533 +++++++++++------ frontend/src/core/threads/types.ts | 7 +- frontend/src/core/threads/utils.ts | 16 +- frontend/src/core/uploads/api.ts | 7 - 61 files changed, 3039 insertions(+), 1278 deletions(-) create mode 100644 frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/layout.tsx create mode 100644 frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx create mode 100644 frontend/src/app/workspace/agents/new/page.tsx create mode 100644 frontend/src/app/workspace/agents/page.tsx create mode 100644 frontend/src/components/workspace/agent-welcome.tsx create mode 100644 frontend/src/components/workspace/agents/agent-card.tsx create mode 100644 frontend/src/components/workspace/agents/agent-gallery.tsx create mode 100644 frontend/src/components/workspace/artifacts/artifact-trigger.tsx create mode 100644 frontend/src/components/workspace/chats/chat-box.tsx create mode 100644 frontend/src/components/workspace/chats/index.ts create mode 100644 frontend/src/components/workspace/chats/use-chat-mode.ts create mode 100644 frontend/src/components/workspace/chats/use-thread-chat.ts create mode 100644 frontend/src/core/agents/api.ts create mode 100644 frontend/src/core/agents/hooks.ts create mode 100644 frontend/src/core/agents/index.ts create mode 100644 frontend/src/core/agents/types.ts create mode 100644 frontend/src/core/api/stream-mode.test.ts create mode 100644 frontend/src/core/api/stream-mode.ts create mode 100644 frontend/src/core/i18n/locale.ts diff --git a/.gitignore b/.gitignore index 91cfc7e4..18816453 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ sandbox_image_cache.tar # ignore the legacy `web` folder web/ + +# ignore frontend backup +frontend_backup/ diff --git a/docker/nginx/nginx.local.conf b/docker/nginx/nginx.local.conf index 50e5ac69..0cdcf9a2 100644 --- a/docker/nginx/nginx.local.conf +++ b/docker/nginx/nginx.local.conf @@ -14,17 +14,17 @@ http { access_log /dev/stdout; error_log /dev/stderr; - # Upstream servers (using Docker service names for Docker Compose) + # Upstream servers (using localhost for local development) upstream gateway { - server gateway:8001; + server localhost:8001; } upstream langgraph { - server langgraph:2024; + server localhost:2024; } upstream frontend { - server frontend:3000; + server localhost:3000; } server { diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 941945fd..d6cec9df 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,22 +1,35 @@ -# Frontend Development Dockerfile -FROM node:22-alpine +# Frontend Dockerfile +# Supports two targets: +# --target dev — install deps only, run `pnpm dev` at container start +# --target prod — full build baked in, run `pnpm start` at container start (default if no --target is specified) -# Accept build argument for pnpm store path ARG PNPM_STORE_PATH=/root/.local/share/pnpm/store -# Install pnpm at specific version (matching package.json) +# ── Base: shared setup ──────────────────────────────────────────────────────── +FROM node:22-alpine AS base +ARG PNPM_STORE_PATH RUN corepack enable && corepack install -g pnpm@10.26.2 - RUN pnpm config set store-dir ${PNPM_STORE_PATH} - -# Set working directory WORKDIR /app - -# Copy frontend source code COPY frontend ./frontend -# Install dependencies -RUN sh -c "cd /app/frontend && pnpm install --frozen-lockfile" - -# Expose Next.js dev server port +# ── Dev: install only, CMD is overridden by docker-compose ─────────────────── +FROM base AS dev +RUN cd /app/frontend && pnpm install --frozen-lockfile EXPOSE 3000 + +# ── Builder: install + compile Next.js ─────────────────────────────────────── +FROM base AS builder +RUN cd /app/frontend && pnpm install --frozen-lockfile +# Skip env validation — runtime vars are injected by nginx/container +RUN cd /app/frontend && SKIP_ENV_VALIDATION=1 pnpm build + +# ── Prod: minimal runtime with pre-built output ─────────────────────────────── +FROM node:22-alpine AS prod +ARG PNPM_STORE_PATH +RUN corepack enable && corepack install -g pnpm@10.26.2 +RUN pnpm config set store-dir ${PNPM_STORE_PATH} +WORKDIR /app +COPY --from=builder /app/frontend ./frontend +EXPOSE 3000 +CMD ["sh", "-c", "cd /app/frontend && pnpm start"] diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 4de85325..7a9ba389 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -716,105 +716,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -956,28 +940,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.1.6': resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.1.6': resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.1.6': resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.1.6': resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} @@ -1571,28 +1551,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@resvg/resvg-js-linux-arm64-musl@2.6.2': resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@resvg/resvg-js-linux-x64-gnu@2.6.2': resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@resvg/resvg-js-linux-x64-musl@2.6.2': resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@resvg/resvg-js-win32-arm64-msvc@2.6.2': resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} @@ -1620,141 +1596,128 @@ packages: resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} engines: {node: '>= 10'} - '@rollup/rollup-android-arm-eabi@4.57.1': - resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.57.1': - resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.57.1': - resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.57.1': - resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.57.1': - resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.57.1': - resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.57.1': - resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.57.1': - resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.57.1': - resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.57.1': - resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] - libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.57.1': - resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.57.1': - resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] - libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.57.1': - resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.57.1': - resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] - libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.57.1': - resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.57.1': - resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] - libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.57.1': - resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.57.1': - resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.57.1': - resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] - libc: [musl] - '@rollup/rollup-openbsd-x64@4.57.1': - resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.57.1': - resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.57.1': - resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.57.1': - resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.57.1': - resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.57.1': - resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} cpu: [x64] os: [win32] @@ -1869,28 +1832,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -2253,49 +2212,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -3783,28 +3734,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -3845,8 +3792,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lru-cache@11.2.5: - resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} lucide-react@0.542.0: @@ -4561,8 +4508,8 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rollup@4.57.1: - resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -6602,79 +6549,79 @@ snapshots: '@resvg/resvg-wasm@2.6.2': {} - '@rollup/rollup-android-arm-eabi@4.57.1': + '@rollup/rollup-android-arm-eabi@4.59.0': optional: true - '@rollup/rollup-android-arm64@4.57.1': + '@rollup/rollup-android-arm64@4.59.0': optional: true - '@rollup/rollup-darwin-arm64@4.57.1': + '@rollup/rollup-darwin-arm64@4.59.0': optional: true - '@rollup/rollup-darwin-x64@4.57.1': + '@rollup/rollup-darwin-x64@4.59.0': optional: true - '@rollup/rollup-freebsd-arm64@4.57.1': + '@rollup/rollup-freebsd-arm64@4.59.0': optional: true - '@rollup/rollup-freebsd-x64@4.57.1': + '@rollup/rollup-freebsd-x64@4.59.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.57.1': + '@rollup/rollup-linux-arm-musleabihf@4.59.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.57.1': + '@rollup/rollup-linux-arm64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.57.1': + '@rollup/rollup-linux-arm64-musl@4.59.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.57.1': + '@rollup/rollup-linux-loong64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-loong64-musl@4.57.1': + '@rollup/rollup-linux-loong64-musl@4.59.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.57.1': + '@rollup/rollup-linux-ppc64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-ppc64-musl@4.57.1': + '@rollup/rollup-linux-ppc64-musl@4.59.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.57.1': + '@rollup/rollup-linux-riscv64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.57.1': + '@rollup/rollup-linux-riscv64-musl@4.59.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.57.1': + '@rollup/rollup-linux-s390x-gnu@4.59.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.57.1': + '@rollup/rollup-linux-x64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-x64-musl@4.57.1': + '@rollup/rollup-linux-x64-musl@4.59.0': optional: true - '@rollup/rollup-openbsd-x64@4.57.1': + '@rollup/rollup-openbsd-x64@4.59.0': optional: true - '@rollup/rollup-openharmony-arm64@4.57.1': + '@rollup/rollup-openharmony-arm64@4.59.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.57.1': + '@rollup/rollup-win32-arm64-msvc@4.59.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.57.1': + '@rollup/rollup-win32-ia32-msvc@4.59.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.57.1': + '@rollup/rollup-win32-x64-gnu@4.59.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.57.1': + '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true '@rtsao/scc@1.1.0': {} @@ -9008,7 +8955,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - lru-cache@11.2.5: {} + lru-cache@11.2.6: {} lucide-react@0.542.0(react@19.2.4): dependencies: @@ -10002,35 +9949,35 @@ snapshots: robust-predicates@3.0.2: {} - rollup@4.57.1: + rollup@4.59.0: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.57.1 - '@rollup/rollup-android-arm64': 4.57.1 - '@rollup/rollup-darwin-arm64': 4.57.1 - '@rollup/rollup-darwin-x64': 4.57.1 - '@rollup/rollup-freebsd-arm64': 4.57.1 - '@rollup/rollup-freebsd-x64': 4.57.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 - '@rollup/rollup-linux-arm-musleabihf': 4.57.1 - '@rollup/rollup-linux-arm64-gnu': 4.57.1 - '@rollup/rollup-linux-arm64-musl': 4.57.1 - '@rollup/rollup-linux-loong64-gnu': 4.57.1 - '@rollup/rollup-linux-loong64-musl': 4.57.1 - '@rollup/rollup-linux-ppc64-gnu': 4.57.1 - '@rollup/rollup-linux-ppc64-musl': 4.57.1 - '@rollup/rollup-linux-riscv64-gnu': 4.57.1 - '@rollup/rollup-linux-riscv64-musl': 4.57.1 - '@rollup/rollup-linux-s390x-gnu': 4.57.1 - '@rollup/rollup-linux-x64-gnu': 4.57.1 - '@rollup/rollup-linux-x64-musl': 4.57.1 - '@rollup/rollup-openbsd-x64': 4.57.1 - '@rollup/rollup-openharmony-arm64': 4.57.1 - '@rollup/rollup-win32-arm64-msvc': 4.57.1 - '@rollup/rollup-win32-ia32-msvc': 4.57.1 - '@rollup/rollup-win32-x64-gnu': 4.57.1 - '@rollup/rollup-win32-x64-msvc': 4.57.1 + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 rou3@0.7.12: {} @@ -10553,7 +10500,7 @@ snapshots: chokidar: 5.0.0 destr: 2.0.5 h3: 1.15.5 - lru-cache: 11.2.5 + lru-cache: 11.2.6 node-fetch-native: 1.6.7 ofetch: 1.5.1 ufo: 1.6.3 @@ -10629,7 +10576,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.57.1 + rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 20.19.33 diff --git a/frontend/src/app/mock/api/threads/search/route.ts b/frontend/src/app/mock/api/threads/search/route.ts index 93c48348..74ba79a3 100644 --- a/frontend/src/app/mock/api/threads/search/route.ts +++ b/frontend/src/app/mock/api/threads/search/route.ts @@ -1,27 +1,84 @@ import fs from "fs"; import path from "path"; -export function POST() { +type ThreadSearchRequest = { + limit?: number; + offset?: number; + sortBy?: "updated_at" | "created_at"; + sortOrder?: "asc" | "desc"; +}; + +type MockThreadSearchResult = Record & { + thread_id: string; + updated_at: string | undefined; +}; + +export async function POST(request: Request) { + const body = ((await request.json().catch(() => ({}))) ?? {}) as ThreadSearchRequest; + + const rawLimit = body.limit; + let limit = 50; + if (typeof rawLimit === "number") { + const normalizedLimit = Math.max(0, Math.floor(rawLimit)); + if (!Number.isNaN(normalizedLimit)) { + limit = normalizedLimit; + } + } + + const rawOffset = body.offset; + let offset = 0; + if (typeof rawOffset === "number") { + const normalizedOffset = Math.max(0, Math.floor(rawOffset)); + if (!Number.isNaN(normalizedOffset)) { + offset = normalizedOffset; + } + } + const sortBy = body.sortBy ?? "updated_at"; + const sortOrder = body.sortOrder ?? "desc"; + const threadsDir = fs.readdirSync( path.resolve(process.cwd(), "public/demo/threads"), { withFileTypes: true, }, ); + const threadData = threadsDir - .map((threadId) => { + .map((threadId) => { if (threadId.isDirectory() && !threadId.name.startsWith(".")) { - const threadData = fs.readFileSync( - path.resolve(`public/demo/threads/${threadId.name}/thread.json`), - "utf8", - ); + const threadData = JSON.parse( + fs.readFileSync( + path.resolve(`public/demo/threads/${threadId.name}/thread.json`), + "utf8", + ), + ) as Record; + return { + ...threadData, thread_id: threadId.name, - values: JSON.parse(threadData).values, + updated_at: + typeof threadData.updated_at === "string" + ? threadData.updated_at + : typeof threadData.created_at === "string" + ? threadData.created_at + : undefined, }; } - return false; + return null; }) - .filter(Boolean); - return Response.json(threadData); + .filter((thread): thread is MockThreadSearchResult => thread !== null) + .sort((a, b) => { + const aTimestamp = a[sortBy]; + const bTimestamp = b[sortBy]; + const aParsed = + typeof aTimestamp === "string" ? Date.parse(aTimestamp) : 0; + const bParsed = + typeof bTimestamp === "string" ? Date.parse(bTimestamp) : 0; + const aValue = Number.isNaN(aParsed) ? 0 : aParsed; + const bValue = Number.isNaN(bParsed) ? 0 : bParsed; + return sortOrder === "asc" ? aValue - bValue : bValue - aValue; + }); + + const pagedThreads = threadData.slice(offset, offset + limit); + return Response.json(pagedThreads); } diff --git a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/layout.tsx b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/layout.tsx new file mode 100644 index 00000000..68f51b60 --- /dev/null +++ b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/layout.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { PromptInputProvider } from "@/components/ai-elements/prompt-input"; +import { ArtifactsProvider } from "@/components/workspace/artifacts"; +import { SubtasksProvider } from "@/core/tasks/context"; + +export default function AgentChatLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx new file mode 100644 index 00000000..a44ad3fa --- /dev/null +++ b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx @@ -0,0 +1,181 @@ +"use client"; + +import { BotIcon, PlusSquare } from "lucide-react"; +import { useParams, useRouter } from "next/navigation"; +import { useCallback } from "react"; + +import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; +import { Button } from "@/components/ui/button"; +import { AgentWelcome } from "@/components/workspace/agent-welcome"; +import { ArtifactTrigger } from "@/components/workspace/artifacts"; +import { ChatBox, useThreadChat } from "@/components/workspace/chats"; +import { InputBox } from "@/components/workspace/input-box"; +import { MessageList } from "@/components/workspace/messages"; +import { ThreadContext } from "@/components/workspace/messages/context"; +import { ThreadTitle } from "@/components/workspace/thread-title"; +import { TodoList } from "@/components/workspace/todo-list"; +import { Tooltip } from "@/components/workspace/tooltip"; +import { useAgent } from "@/core/agents"; +import { useI18n } from "@/core/i18n/hooks"; +import { useNotification } from "@/core/notification/hooks"; +import { useLocalSettings } from "@/core/settings"; +import { useThreadStream } from "@/core/threads/hooks"; +import { textOfMessage } from "@/core/threads/utils"; +import { env } from "@/env"; +import { cn } from "@/lib/utils"; + +export default function AgentChatPage() { + const { t } = useI18n(); + const [settings, setSettings] = useLocalSettings(); + const router = useRouter(); + + const { agent_name } = useParams<{ + agent_name: string; + }>(); + + const { agent } = useAgent(agent_name); + + const { threadId, isNewThread, setIsNewThread } = useThreadChat(); + + const { showNotification } = useNotification(); + const [thread, sendMessage] = useThreadStream({ + threadId: isNewThread ? undefined : threadId, + context: { ...settings.context, agent_name: agent_name }, + onStart: () => { + setIsNewThread(false); + // ! Important: Never use next.js router for navigation in this case, otherwise it will cause the thread to re-mount and lose all states. Use native history API instead. + history.replaceState( + null, + "", + `/workspace/agents/${agent_name}/chats/${threadId}`, + ); + }, + onFinish: (state) => { + if (document.hidden || !document.hasFocus()) { + let body = "Conversation finished"; + const lastMessage = state.messages[state.messages.length - 1]; + if (lastMessage) { + const textContent = textOfMessage(lastMessage); + if (textContent) { + body = + textContent.length > 200 + ? textContent.substring(0, 200) + "..." + : textContent; + } + } + showNotification(state.title, { body }); + } + }, + }); + + const handleSubmit = useCallback( + (message: PromptInputMessage) => { + void sendMessage(threadId, message, { agent_name }); + }, + [sendMessage, threadId, agent_name], + ); + + const handleStop = useCallback(async () => { + await thread.stop(); + }, [thread]); + + return ( + + +
+
+ {/* Agent badge */} +
+ + + {agent?.name ?? agent_name} + +
+ +
+ +
+
+ + + + +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ + + ) + } + disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"} + onContextChange={(context) => setSettings("context", context)} + onSubmit={handleSubmit} + onStop={handleStop} + /> + {env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && ( +
+ {t.common.notAvailableInDemoMode} +
+ )} +
+
+
+
+
+
+ ); +} diff --git a/frontend/src/app/workspace/agents/new/page.tsx b/frontend/src/app/workspace/agents/new/page.tsx new file mode 100644 index 00000000..9424a5f5 --- /dev/null +++ b/frontend/src/app/workspace/agents/new/page.tsx @@ -0,0 +1,252 @@ +"use client"; + +import { ArrowLeftIcon, BotIcon, CheckCircleIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useCallback, useMemo, useState } from "react"; + +import { + PromptInput, + PromptInputFooter, + PromptInputSubmit, + PromptInputTextarea, +} from "@/components/ai-elements/prompt-input"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ArtifactsProvider } from "@/components/workspace/artifacts"; +import { MessageList } from "@/components/workspace/messages"; +import { ThreadContext } from "@/components/workspace/messages/context"; +import type { Agent } from "@/core/agents"; +import { checkAgentName, getAgent } from "@/core/agents/api"; +import { useI18n } from "@/core/i18n/hooks"; +import { useThreadStream } from "@/core/threads/hooks"; +import { uuid } from "@/core/utils/uuid"; +import { cn } from "@/lib/utils"; + +type Step = "name" | "chat"; + +const NAME_RE = /^[A-Za-z0-9-]+$/; + +export default function NewAgentPage() { + const { t } = useI18n(); + const router = useRouter(); + + // ── Step 1: name form ────────────────────────────────────────────────────── + const [step, setStep] = useState("name"); + const [nameInput, setNameInput] = useState(""); + const [nameError, setNameError] = useState(""); + const [isCheckingName, setIsCheckingName] = useState(false); + const [agentName, setAgentName] = useState(""); + const [agent, setAgent] = useState(null); + // ── Step 2: chat ─────────────────────────────────────────────────────────── + + // Stable thread ID — all turns belong to the same thread + const threadId = useMemo(() => uuid(), []); + + const [thread, sendMessage] = useThreadStream({ + threadId: step === "chat" ? threadId : undefined, + context: { + mode: "flash", + is_bootstrap: true, + }, + onToolEnd({ name }) { + if (name !== "setup_agent" || !agentName) return; + getAgent(agentName) + .then((fetched) => setAgent(fetched)) + .catch(() => { + // agent write may not be flushed yet — ignore silently + }); + }, + }); + + // ── Handlers ─────────────────────────────────────────────────────────────── + + const handleConfirmName = useCallback(async () => { + const trimmed = nameInput.trim(); + if (!trimmed) return; + if (!NAME_RE.test(trimmed)) { + setNameError(t.agents.nameStepInvalidError); + return; + } + setNameError(""); + setIsCheckingName(true); + try { + const result = await checkAgentName(trimmed); + if (!result.available) { + setNameError(t.agents.nameStepAlreadyExistsError); + return; + } + } catch { + setNameError(t.agents.nameStepCheckError); + return; + } finally { + setIsCheckingName(false); + } + setAgentName(trimmed); + setStep("chat"); + await sendMessage(threadId, { + text: t.agents.nameStepBootstrapMessage.replace("{name}", trimmed), + files: [], + }); + }, [ + nameInput, + sendMessage, + threadId, + t.agents.nameStepBootstrapMessage, + t.agents.nameStepInvalidError, + t.agents.nameStepAlreadyExistsError, + t.agents.nameStepCheckError, + ]); + + const handleNameKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + void handleConfirmName(); + } + }; + + const handleChatSubmit = useCallback( + async (text: string) => { + const trimmed = text.trim(); + if (!trimmed || thread.isLoading) return; + await sendMessage( + threadId, + { text: trimmed, files: [] }, + { agent_name: agentName }, + ); + }, + [thread.isLoading, sendMessage, threadId, agentName], + ); + + // ── Shared header ────────────────────────────────────────────────────────── + + const header = ( +
+ +

{t.agents.createPageTitle}

+
+ ); + + // ── Step 1: name form ────────────────────────────────────────────────────── + + if (step === "name") { + return ( +
+ {header} +
+
+
+
+ +
+
+

+ {t.agents.nameStepTitle} +

+

+ {t.agents.nameStepHint} +

+
+
+ +
+ { + setNameInput(e.target.value); + setNameError(""); + }} + onKeyDown={handleNameKeyDown} + className={cn(nameError && "border-destructive")} + /> + {nameError && ( +

{nameError}

+ )} + +
+
+
+
+ ); + } + + // ── Step 2: chat ─────────────────────────────────────────────────────────── + + return ( + + +
+ {header} + +
+ {/* ── Message area ── */} +
+ +
+ + {/* ── Bottom action area ── */} +
+
+ {agent ? ( + // ✅ Success card +
+ +

{t.agents.agentCreated}

+
+ + +
+
+ ) : ( + // 📝 Normal input + void handleChatSubmit(text)} + > + + + + + + )} +
+
+
+
+
+
+ ); +} diff --git a/frontend/src/app/workspace/agents/page.tsx b/frontend/src/app/workspace/agents/page.tsx new file mode 100644 index 00000000..46fdbf2f --- /dev/null +++ b/frontend/src/app/workspace/agents/page.tsx @@ -0,0 +1,5 @@ +import { AgentGallery } from "@/components/workspace/agents/agent-gallery"; + +export default function AgentsPage() { + return ; +} diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 8dbc546e..3147358a 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -1,515 +1,147 @@ "use client"; -import type { Message } from "@langchain/langgraph-sdk"; -import type { UseStream } from "@langchain/langgraph-sdk/react"; -import { FilesIcon, XIcon } from "lucide-react"; -import { useParams, useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback } from "react"; -import { ConversationEmptyState } from "@/components/ai-elements/conversation"; -import { usePromptInputController } from "@/components/ai-elements/prompt-input"; -import { Button } from "@/components/ui/button"; +import { type PromptInputMessage } from "@/components/ai-elements/prompt-input"; +import { ArtifactTrigger } from "@/components/workspace/artifacts"; import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from "@/components/ui/resizable"; -import { useSidebar } from "@/components/ui/sidebar"; -import { - ArtifactFileDetail, - ArtifactFileList, - useArtifacts, -} from "@/components/workspace/artifacts"; + ChatBox, + useSpecificChatMode, + useThreadChat, +} from "@/components/workspace/chats"; import { InputBox } from "@/components/workspace/input-box"; import { MessageList } from "@/components/workspace/messages"; import { ThreadContext } from "@/components/workspace/messages/context"; import { ThreadTitle } from "@/components/workspace/thread-title"; import { TodoList } from "@/components/workspace/todo-list"; -import { Tooltip } from "@/components/workspace/tooltip"; import { Welcome } from "@/components/workspace/welcome"; import { useI18n } from "@/core/i18n/hooks"; import { useNotification } from "@/core/notification/hooks"; import { useLocalSettings } from "@/core/settings"; -import { bootstrapRemoteSkill } from "@/core/skills"; -import { type AgentThread, type AgentThreadState } from "@/core/threads"; -import { useSubmitThread, useThreadStream } from "@/core/threads/hooks"; -import { - pathOfThread, - textOfMessage, - titleOfThread, -} from "@/core/threads/utils"; -import { uuid } from "@/core/utils/uuid"; +import { useThreadStream } from "@/core/threads/hooks"; +import { textOfMessage } from "@/core/threads/utils"; import { env } from "@/env"; import { cn } from "@/lib/utils"; export default function ChatPage() { const { t } = useI18n(); - const router = useRouter(); const [settings, setSettings] = useLocalSettings(); - const { setOpen: setSidebarOpen } = useSidebar(); - const { - artifacts, - open: artifactsOpen, - setOpen: setArtifactsOpen, - setArtifacts, - select: selectArtifact, - selectedArtifact, - } = useArtifacts(); - const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>(); - const searchParams = useSearchParams(); - const promptInputController = usePromptInputController(); - const inputInitialValue = useMemo(() => { - if (threadIdFromPath !== "new" || searchParams.get("mode") !== "skill") { - return undefined; - } - return t.inputBox.createSkillPrompt; - }, [threadIdFromPath, searchParams, t.inputBox.createSkillPrompt]); - const lastInitialValueRef = useRef(undefined); - const setInputRef = useRef(promptInputController.textInput.setInput); - setInputRef.current = promptInputController.textInput.setInput; - useEffect(() => { - if (inputInitialValue && inputInitialValue !== lastInitialValueRef.current) { - lastInitialValueRef.current = inputInitialValue; - setTimeout(() => { - setInputRef.current(inputInitialValue); - const textarea = document.querySelector("textarea"); - if (textarea) { - textarea.focus(); - textarea.selectionStart = textarea.value.length; - textarea.selectionEnd = textarea.value.length; - } - }, 100); - } - }, [inputInitialValue]); - // UI mode depends only on route: /workspace/chats/new is always "new page" mode. - const isNewThread = useMemo(() => threadIdFromPath === "new", [threadIdFromPath]); - // Submission strategy is controlled by `isnew` query param only. - // - isnew=false: reuse existing thread - // - otherwise: create/start a new session - const createNewSession = useMemo(() => { - if (threadIdFromPath !== "new") { - return false; - } - - return searchParams.get("isnew")?.trim().toLowerCase() !== "false"; - }, [threadIdFromPath, searchParams]); - - const uploadTarget = useMemo(() => { - const target = searchParams.get("upload_target")?.trim().toLowerCase(); - return target === "skill" ? "skill" : undefined; - }, [searchParams]); - - const skillBootstrap = useMemo(() => { - const skillIdRaw = searchParams.get("skill_id")?.trim(); - if (!skillIdRaw) return undefined; - - const contentId = Number(skillIdRaw); - if (!Number.isFinite(contentId)) return undefined; - - const languageTypeRaw = - searchParams.get("languageType")?.trim() ?? - searchParams.get("language_type")?.trim(); - const languageType = languageTypeRaw - ? Number(languageTypeRaw) - : 0; - - return { - contentId, - languageType: Number.isFinite(languageType) ? languageType : 0, - }; - }, [threadIdFromPath, searchParams]); - - const [threadId, setThreadId] = useState(null); - useEffect(() => { - if (threadIdFromPath !== "new") { - setThreadId(threadIdFromPath); - } else { - const queryThreadId = searchParams.get("thread_id")?.trim(); - setThreadId(queryThreadId || uuid()); - } - }, [threadIdFromPath, searchParams]); - - // Runtime strategy for /new page: - // - UI remains new-page mode - // - if isnew=false, execute against existing thread_id without creating a new one - const reuseExistingThread = useMemo( - () => threadIdFromPath === "new" && !createNewSession && !!threadId, - [threadIdFromPath, createNewSession, threadId], - ); + const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat(); + useSpecificChatMode(); const { showNotification } = useNotification(); - const [isSkillBootstrapping, setIsSkillBootstrapping] = useState(false); - const [skillBootstrapError, setSkillBootstrapError] = useState( - null, - ); - const skillBootstrappedKeyRef = useRef(null); - const [finalState, setFinalState] = useState(null); - const thread = useThreadStream({ - // Keep UI in new-page mode, but runtime may reuse existing thread - isNewThread: reuseExistingThread ? false : isNewThread, - threadId, - fetchStateHistory: true, + + const [thread, sendMessage] = useThreadStream({ + threadId: isNewThread ? undefined : threadId, + context: settings.context, + isMock, + onStart: () => { + setIsNewThread(false); + // ! Important: Never use next.js router for navigation in this case, otherwise it will cause the thread to re-mount and lose all states. Use native history API instead. + history.replaceState(null, "", `/workspace/chats/${threadId}`); + }, onFinish: (state) => { - setFinalState(state); if (document.hidden || !document.hasFocus()) { let body = "Conversation finished"; - const lastMessage = state.messages[state.messages.length - 1]; + const lastMessage = state.messages.at(-1); if (lastMessage) { const textContent = textOfMessage(lastMessage); if (textContent) { - if (textContent.length > 200) { - body = textContent.substring(0, 200) + "..."; - } else { - body = textContent; - } + body = + textContent.length > 200 + ? textContent.substring(0, 200) + "..." + : textContent; } } - showNotification(state.title, { - body, - }); + showNotification(state.title, { body }); } }, - }) as unknown as UseStream; - useEffect(() => { - if (thread.isLoading) setFinalState(null); - }, [thread.isLoading]); - - const title = useMemo(() => { - let result = isNewThread - ? "" - : titleOfThread(thread as unknown as AgentThread); - if (result === "Untitled") { - result = ""; - } - return result; - }, [thread, isNewThread]); - - const [hasSubmitted, setHasSubmitted] = useState(false); - const suppressExistingThreadPrefetchUi = reuseExistingThread && !hasSubmitted; - - useEffect(() => { - const pageTitle = isNewThread - ? t.pages.newChat - : thread.values?.title && thread.values.title !== "Untitled" - ? thread.values.title - : t.pages.untitled; - if (thread.isThreadLoading && !suppressExistingThreadPrefetchUi) { - document.title = `Loading... - ${t.pages.appName}`; - } else { - document.title = `${pageTitle} - ${t.pages.appName}`; - } - }, [ - isNewThread, - t.pages.newChat, - t.pages.untitled, - t.pages.appName, - thread.values.title, - thread.isThreadLoading, - suppressExistingThreadPrefetchUi, - ]); - - const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true); - useEffect(() => { - setArtifacts(thread.values.artifacts); - if ( - env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && - autoSelectFirstArtifact - ) { - if (thread?.values?.artifacts?.length > 0) { - setAutoSelectFirstArtifact(false); - selectArtifact(thread.values.artifacts[0]!); - } - } - }, [ - autoSelectFirstArtifact, - selectArtifact, - setArtifacts, - thread.values.artifacts, - ]); - - const artifactPanelOpen = useMemo(() => { - if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true") { - return artifactsOpen && artifacts?.length > 0; - } - return artifactsOpen; - }, [artifactsOpen, artifacts]); - - const [todoListCollapsed, setTodoListCollapsed] = useState(true); - - useEffect(() => { - if (!threadId || !skillBootstrap?.contentId) { - setIsSkillBootstrapping(false); - setSkillBootstrapError(null); - return; - } - - const languageType = skillBootstrap.languageType ?? 0; - const initKey = `${threadId}:${skillBootstrap.contentId}:${languageType}`; - if (skillBootstrappedKeyRef.current === initKey) { - return; - } - - let cancelled = false; - - const runBootstrap = async () => { - setIsSkillBootstrapping(true); - setSkillBootstrapError(null); - try { - await bootstrapRemoteSkill({ - thread_id: threadId, - content_id: skillBootstrap.contentId, - language_type: languageType, - target_dir: "/mnt/user-data/uploads/skill", - clear_target: true, - }); - - if (!cancelled) { - skillBootstrappedKeyRef.current = initKey; - setIsSkillBootstrapping(false); - } - } catch (error) { - if (!cancelled) { - const message = error instanceof Error ? error.message : "Skill 初始化失败"; - setSkillBootstrapError(message); - setIsSkillBootstrapping(false); - showNotification("Skill 初始化失败", { body: message }); - } - } - }; - - void runBootstrap(); - - return () => { - cancelled = true; - }; - }, [threadId, skillBootstrap, showNotification]); - - const submitThread = useSubmitThread({ - isNewThread, - createNewSession, - threadId, - thread, - uploadTarget, - threadContext: { - ...settings.context, - thinking_enabled: settings.context.mode !== "flash", - is_plan_mode: - settings.context.mode === "pro" || settings.context.mode === "ultra", - subagent_enabled: settings.context.mode === "ultra", - }, - afterSubmit() { - router.push(pathOfThread(threadId!)); - }, }); + const handleSubmit = useCallback( - (message: Parameters[0]) => { - if (isSkillBootstrapping) { - return; - } - setHasSubmitted(true); - void submitThread(message); + (message: PromptInputMessage) => { + void sendMessage(threadId, message); }, - [isSkillBootstrapping, submitThread], + [sendMessage, threadId], ); const handleStop = useCallback(async () => { await thread.stop(); }, [thread]); - if (!threadId) { - return null; - } - return ( - - - -
-
-
- {title !== "Untitled" && ( - - )} -
-
- {artifacts?.length > 0 && !artifactsOpen && ( - - - - )} -
-
-
-
- -
-
-
-
-
-
-
- - } - disabled={ - env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || - isSkillBootstrapping - } - onContextChange={(context) => - setSettings("context", context) - } - onSubmit={handleSubmit} - onStop={handleStop} - /> - {(isSkillBootstrapping || skillBootstrapError) && ( -
- {isSkillBootstrapping - ? "正在初始化 Skill 文件..." - : `Skill 初始化失败:${skillBootstrapError}`} -
- )} - {env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && ( -
- {t.common.notAvailableInDemoMode} -
- )} -
-
-
-
-
- - -
+ +
+
- {selectedArtifact ? ( - + +
+
+ +
+ +
+
+ - ) : ( -
-
- +
+
+
+
+
+
- {thread.values.artifacts?.length === 0 ? ( - } - title="No artifact selected" - description="Select an artifact to view its details" - /> - ) : ( -
-
-

Artifacts

-
-
- -
+ + } + disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"} + onContextChange={(context) => setSettings("context", context)} + onSubmit={handleSubmit} + onStop={handleStop} + /> + {env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && ( +
+ {t.common.notAvailableInDemoMode}
)}
- )} -
- - +
+
+
+
); } diff --git a/frontend/src/app/workspace/layout.tsx b/frontend/src/app/workspace/layout.tsx index aa67dfa1..57a45be9 100644 --- a/frontend/src/app/workspace/layout.tsx +++ b/frontend/src/app/workspace/layout.tsx @@ -1,12 +1,12 @@ "use client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useState } from "react"; import { Toaster } from "sonner"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar"; -import { useLocalSettings } from "@/core/settings"; +import { getLocalSettings, useLocalSettings } from "@/core/settings"; const queryClient = new QueryClient(); @@ -14,7 +14,11 @@ export default function WorkspaceLayout({ children, }: Readonly<{ children: React.ReactNode }>) { const [settings, setSettings] = useLocalSettings(); - const [open, setOpen] = useState(() => !settings.layout.sidebar_collapsed); + const [open, setOpen] = useState(false); // SSR default: open (matches server render) + useLayoutEffect(() => { + // Runs synchronously before first paint on the client — no visual flash + setOpen(!getLocalSettings().layout.sidebar_collapsed); + }, []); useEffect(() => { setOpen(!settings.layout.sidebar_collapsed); }, [settings.layout.sidebar_collapsed]); diff --git a/frontend/src/components/ai-elements/message.tsx b/frontend/src/components/ai-elements/message.tsx index f929e147..c0071c21 100644 --- a/frontend/src/components/ai-elements/message.tsx +++ b/frontend/src/components/ai-elements/message.tsx @@ -44,7 +44,8 @@ export const MessageContent = ({ }: MessageContentProps) => (
( diff --git a/frontend/src/components/workspace/agent-welcome.tsx b/frontend/src/components/workspace/agent-welcome.tsx new file mode 100644 index 00000000..7d30b9b3 --- /dev/null +++ b/frontend/src/components/workspace/agent-welcome.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { BotIcon } from "lucide-react"; + +import { type Agent } from "@/core/agents"; +import { cn } from "@/lib/utils"; + +export function AgentWelcome({ + className, + agent, + agentName, +}: { + className?: string; + agent: Agent | null | undefined; + agentName: string; +}) { + const displayName = agent?.name ?? agentName; + const description = agent?.description; + + return ( +
+
+ +
+
{displayName}
+ {description && ( +

{description}

+ )} +
+ ); +} diff --git a/frontend/src/components/workspace/agents/agent-card.tsx b/frontend/src/components/workspace/agents/agent-card.tsx new file mode 100644 index 00000000..6b2a510b --- /dev/null +++ b/frontend/src/components/workspace/agents/agent-card.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { BotIcon, MessageSquareIcon, Trash2Icon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { toast } from "sonner"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useDeleteAgent } from "@/core/agents"; +import type { Agent } from "@/core/agents"; +import { useI18n } from "@/core/i18n/hooks"; + +interface AgentCardProps { + agent: Agent; +} + +export function AgentCard({ agent }: AgentCardProps) { + const { t } = useI18n(); + const router = useRouter(); + const deleteAgent = useDeleteAgent(); + const [deleteOpen, setDeleteOpen] = useState(false); + + function handleChat() { + router.push(`/workspace/agents/${agent.name}/chats/new`); + } + + async function handleDelete() { + try { + await deleteAgent.mutateAsync(agent.name); + toast.success(t.agents.deleteSuccess); + setDeleteOpen(false); + } catch (err) { + toast.error(err instanceof Error ? err.message : String(err)); + } + } + + return ( + <> + + +
+
+
+ +
+
+ + {agent.name} + + {agent.model && ( + + {agent.model} + + )} +
+
+
+ {agent.description && ( + + {agent.description} + + )} +
+ + {agent.tool_groups && agent.tool_groups.length > 0 && ( + +
+ {agent.tool_groups.map((group) => ( + + {group} + + ))} +
+
+ )} + + + +
+ +
+
+
+ + {/* Delete Confirm */} + + + + {t.agents.delete} + {t.agents.deleteConfirm} + + + + + + + + + ); +} diff --git a/frontend/src/components/workspace/agents/agent-gallery.tsx b/frontend/src/components/workspace/agents/agent-gallery.tsx new file mode 100644 index 00000000..73986265 --- /dev/null +++ b/frontend/src/components/workspace/agents/agent-gallery.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { BotIcon, PlusIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; + +import { Button } from "@/components/ui/button"; +import { useAgents } from "@/core/agents"; +import { useI18n } from "@/core/i18n/hooks"; + +import { AgentCard } from "./agent-card"; + +export function AgentGallery() { + const { t } = useI18n(); + const { agents, isLoading } = useAgents(); + const router = useRouter(); + + const handleNewAgent = () => { + router.push("/workspace/agents/new"); + }; + + return ( +
+ {/* Page header */} +
+
+

{t.agents.title}

+

+ {t.agents.description} +

+
+ +
+ + {/* Content */} +
+ {isLoading ? ( +
+ {t.common.loading} +
+ ) : agents.length === 0 ? ( +
+
+ +
+
+

{t.agents.emptyTitle}

+

+ {t.agents.emptyDescription} +

+
+ +
+ ) : ( +
+ {agents.map((agent) => ( + + ))} +
+ )} +
+
+ ); +} diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx index ba62a14c..0539d181 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -39,6 +39,7 @@ import { env } from "@/env"; import { cn } from "@/lib/utils"; import { CitationLink } from "../citations/citation-link"; +import { useThread } from "../messages/context"; import { Tooltip } from "../tooltip"; import { useArtifacts } from "./context"; @@ -79,9 +80,9 @@ export function ArtifactFileDetail({ } return checkCodeFile(filepath); }, [filepath, isWriteFile, isSkillFile]); - const previewable = useMemo(() => { - return (language === "html" && !isWriteFile) || language === "markdown"; - }, [isWriteFile, language]); + const isSupportPreview = useMemo(() => { + return language === "html" || language === "markdown"; + }, [language]); const { content } = useArtifactContent({ threadId, filepath: filepathFromProps, @@ -92,14 +93,14 @@ export function ArtifactFileDetail({ const [viewMode, setViewMode] = useState<"code" | "preview">("code"); const [isInstalling, setIsInstalling] = useState(false); - + const { isMock } = useThread(); useEffect(() => { - if (previewable) { + if (isSupportPreview) { setViewMode("preview"); } else { setViewMode("code"); } - }, [previewable]); + }, [isSupportPreview]); const handleInstallSkill = useCallback(async () => { if (isInstalling) return; @@ -148,16 +149,18 @@ export function ArtifactFileDetail({
- {previewable && ( + {isSupportPreview && ( - setViewMode(value as "code" | "preview") - } + onValueChange={(value) => { + if (value) { + setViewMode(value as "code" | "preview"); + } + }} > @@ -232,12 +235,10 @@ export function ArtifactFileDetail({
- {previewable && + {isSupportPreview && viewMode === "preview" && (language === "markdown" || language === "html") && ( @@ -252,7 +253,7 @@ export function ArtifactFileDetail({ {!isCodeFile && (