-
Integrations
-
- Integrate with your team's most important tools
-
+
+ {/* Heading */}
+
+
+ Integrate with your
+
+ team's most important tools
+
+
-
-
-
-
+ {/* 5 scrolling columns */}
+
+ {COLUMNS.map((column, colIndex) => (
+
+ ))}
diff --git a/surfsense_web/public/connectors/composio.svg b/surfsense_web/public/connectors/composio.svg
deleted file mode 100644
index 7c06babeb..000000000
--- a/surfsense_web/public/connectors/composio.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
From 2add10629666f88a0877cbdb445368c3171657f9 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Mon, 9 Feb 2026 03:15:27 +0530
Subject: [PATCH 05/35] feat: enhance rate limiting by allowing requests to
proceed when Redis is unavailable, with logging for monitoring
---
surfsense_backend/app/app.py | 22 ++++++++++++++++------
1 file changed, 16 insertions(+), 6 deletions(-)
diff --git a/surfsense_backend/app/app.py b/surfsense_backend/app/app.py
index f7d4601ef..541393564 100644
--- a/surfsense_backend/app/app.py
+++ b/surfsense_backend/app/app.py
@@ -84,17 +84,27 @@ def _check_rate_limit(
"""
Check per-IP rate limit using Redis. Raises 429 if exceeded.
Uses atomic INCR + EXPIRE to avoid race conditions.
+ Fails open (allows request) if Redis is unavailable.
"""
client_ip = get_remote_address(request)
key = f"surfsense:auth_rate_limit:{scope}:{client_ip}"
- r = _get_rate_limit_redis()
+ try:
+ r = _get_rate_limit_redis()
- # Atomic: increment first, then set TTL if this is a new key
- pipe = r.pipeline()
- pipe.incr(key)
- pipe.expire(key, window_seconds)
- result = pipe.execute()
+ # Atomic: increment first, then set TTL if this is a new key
+ pipe = r.pipeline()
+ pipe.incr(key)
+ pipe.expire(key, window_seconds)
+ result = pipe.execute()
+ except (redis.exceptions.RedisError, OSError) as exc:
+ # Redis unavailable — fail open to preserve auth availability.
+ # SlowAPI middleware provides a secondary rate-limiting layer.
+ rate_limit_logger.warning(
+ f"Redis unavailable for rate limiting ({scope}), "
+ f"allowing request from {client_ip}: {exc}"
+ )
+ return
current_count = result[0] # INCR returns the new value
From c1016591dab9f4b95aa5960db55d8872c4568bfa Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Mon, 9 Feb 2026 12:57:32 +0530
Subject: [PATCH 06/35] refactor: update authentication error handling to
prevent user enumeration and improve error messages
---
surfsense_backend/app/app.py | 2 +-
surfsense_backend/app/users.py | 40 +++++++-------------------------
surfsense_web/lib/auth-errors.ts | 16 ++++---------
3 files changed, 14 insertions(+), 44 deletions(-)
diff --git a/surfsense_backend/app/app.py b/surfsense_backend/app/app.py
index 541393564..e35b310e0 100644
--- a/surfsense_backend/app/app.py
+++ b/surfsense_backend/app/app.py
@@ -62,7 +62,7 @@ def _rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded):
# ============================================================================
# Stricter per-IP limits on auth endpoints to prevent:
# - Brute force password attacks
-# - User enumeration via LOGIN_USER_NOT_FOUND / REGISTER_USER_ALREADY_EXISTS
+# - User enumeration via REGISTER_USER_ALREADY_EXISTS
# - Email spam via forgot-password
#
# These use direct Redis INCR+EXPIRE for simplicity and reliability.
diff --git a/surfsense_backend/app/users.py b/surfsense_backend/app/users.py
index b68461fc0..7ec657781 100644
--- a/surfsense_backend/app/users.py
+++ b/surfsense_backend/app/users.py
@@ -2,7 +2,7 @@ import logging
import uuid
import httpx
-from fastapi import Depends, HTTPException, Request, Response
+from fastapi import Depends, Request, Response
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models
from fastapi_users.authentication import (
@@ -47,6 +47,14 @@ if config.AUTH_TYPE == "GOOGLE":
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
+ """
+ Custom user manager extending fastapi-users BaseUserManager.
+
+ Authentication returns a generic error for both non-existent accounts
+ and incorrect passwords to comply with OWASP WSTG-IDNT-04 and
+ prevent user enumeration attacks.
+ """
+
reset_password_token_secret = SECRET
verification_token_secret = SECRET
@@ -180,36 +188,6 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
):
print(f"Verification requested for user {user.id}. Verification token: {token}")
- async def authenticate(self, credentials):
- """
- Override to return a specific error when user account doesn't exist,
- instead of a generic LOGIN_BAD_CREDENTIALS.
- """
- from fastapi_users.exceptions import UserNotExists
-
- try:
- user = await self.get_by_email(credentials.username)
- except UserNotExists:
- # Still hash the password to mitigate timing attacks
- self.password_helper.hash(credentials.password)
- logger.warning(
- f"Login attempt for non-existent account: {credentials.username}"
- )
- raise HTTPException(
- status_code=400,
- detail="LOGIN_USER_NOT_FOUND",
- ) from None
-
- verified, updated_password_hash = self.password_helper.verify_and_update(
- credentials.password, user.hashed_password
- )
- if not verified:
- logger.warning(f"Failed login attempt (wrong password) for user: {user.id}")
- return None
- if updated_password_hash is not None:
- await self.user_db.update(user, {"hashed_password": updated_password_hash})
- return user
-
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
yield UserManager(user_db)
diff --git a/surfsense_web/lib/auth-errors.ts b/surfsense_web/lib/auth-errors.ts
index c1f0a3be3..d1816cbdd 100644
--- a/surfsense_web/lib/auth-errors.ts
+++ b/surfsense_web/lib/auth-errors.ts
@@ -20,8 +20,8 @@ const AUTH_ERROR_MESSAGES: AuthErrorMapping = {
description: "Your account may be suspended or restricted",
},
"404": {
- title: "Account not found",
- description: "No account exists with this email address",
+ title: "Not found",
+ description: "The requested resource was not found",
},
"409": {
title: "Account conflict",
@@ -46,12 +46,8 @@ const AUTH_ERROR_MESSAGES: AuthErrorMapping = {
// FastAPI specific errors
LOGIN_BAD_CREDENTIALS: {
- title: "Invalid credentials",
- description: "The email or password you entered is incorrect",
- },
- LOGIN_USER_NOT_FOUND: {
- title: "Account not found",
- description: "No account exists with this email address. Please sign up first.",
+ title: "Login failed",
+ description: "Invalid email or password. If you don't have an account, please sign up.",
},
LOGIN_USER_NOT_VERIFIED: {
title: "Account not verified",
@@ -147,10 +143,6 @@ export function getAuthErrorMessage(errorCode: string, returnTitle: boolean = fa
if (!errorInfo) {
const patterns = [
{ pattern: /credential|password|email/i, code: "LOGIN_BAD_CREDENTIALS" },
- {
- pattern: /not found|no account|does not exist|user not found/i,
- code: "LOGIN_USER_NOT_FOUND",
- },
{ pattern: /verify|verification/i, code: "LOGIN_USER_NOT_VERIFIED" },
{ pattern: /inactive|disabled|suspended/i, code: "USER_INACTIVE" },
{ pattern: /exists|duplicate/i, code: "REGISTER_USER_ALREADY_EXISTS" },
From 5b5e4823057bb22d021008576d46c71a3766607e Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Mon, 9 Feb 2026 13:09:38 +0530
Subject: [PATCH 07/35] feat: implement in-memory fallback for rate limiting
when Redis is unavailable, enhancing reliability and logging
---
surfsense_backend/app/app.py | 50 +++++++++++++++++++++++++++++++-----
1 file changed, 44 insertions(+), 6 deletions(-)
diff --git a/surfsense_backend/app/app.py b/surfsense_backend/app/app.py
index e35b310e0..70269e723 100644
--- a/surfsense_backend/app/app.py
+++ b/surfsense_backend/app/app.py
@@ -1,6 +1,9 @@
import logging
import os
+import time
+from collections import defaultdict
from contextlib import asynccontextmanager
+from threading import Lock
import redis
from fastapi import Depends, FastAPI, HTTPException, Request, status
@@ -58,17 +61,22 @@ def _rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded):
# ============================================================================
-# Auth-Specific Rate Limits (Redis-backed FastAPI dependencies)
+# Auth-Specific Rate Limits (Redis-backed with in-memory fallback)
# ============================================================================
# Stricter per-IP limits on auth endpoints to prevent:
# - Brute force password attacks
# - User enumeration via REGISTER_USER_ALREADY_EXISTS
# - Email spam via forgot-password
#
-# These use direct Redis INCR+EXPIRE for simplicity and reliability.
+# Primary: Redis INCR+EXPIRE (shared across all workers).
+# Fallback: In-memory sliding window (per-worker) when Redis is unavailable.
# Same Redis instance as SlowAPI / Celery.
_rate_limit_redis: redis.Redis | None = None
+# In-memory fallback rate limiter (per-worker, used only when Redis is down)
+_memory_rate_limits: dict[str, list[float]] = defaultdict(list)
+_memory_lock = Lock()
+
def _get_rate_limit_redis() -> redis.Redis:
"""Get or create Redis client for auth rate limiting."""
@@ -78,13 +86,43 @@ def _get_rate_limit_redis() -> redis.Redis:
return _rate_limit_redis
+def _check_rate_limit_memory(
+ client_ip: str, max_requests: int, window_seconds: int, scope: str
+):
+ """
+ In-memory fallback rate limiter using a sliding window.
+ Used only when Redis is unavailable. Per-worker only (not shared),
+ so effective limit = max_requests x num_workers.
+ """
+ key = f"{scope}:{client_ip}"
+ now = time.monotonic()
+
+ with _memory_lock:
+ # Evict timestamps outside the current window
+ _memory_rate_limits[key] = [
+ t for t in _memory_rate_limits[key] if now - t < window_seconds
+ ]
+
+ if len(_memory_rate_limits[key]) >= max_requests:
+ rate_limit_logger.warning(
+ f"Rate limit exceeded (in-memory fallback) on {scope} for IP {client_ip} "
+ f"({len(_memory_rate_limits[key])}/{max_requests} in {window_seconds}s)"
+ )
+ raise HTTPException(
+ status_code=429,
+ detail="RATE_LIMIT_EXCEEDED",
+ )
+
+ _memory_rate_limits[key].append(now)
+
+
def _check_rate_limit(
request: Request, max_requests: int, window_seconds: int, scope: str
):
"""
Check per-IP rate limit using Redis. Raises 429 if exceeded.
Uses atomic INCR + EXPIRE to avoid race conditions.
- Fails open (allows request) if Redis is unavailable.
+ Falls back to in-memory sliding window if Redis is unavailable.
"""
client_ip = get_remote_address(request)
key = f"surfsense:auth_rate_limit:{scope}:{client_ip}"
@@ -98,12 +136,12 @@ def _check_rate_limit(
pipe.expire(key, window_seconds)
result = pipe.execute()
except (redis.exceptions.RedisError, OSError) as exc:
- # Redis unavailable — fail open to preserve auth availability.
- # SlowAPI middleware provides a secondary rate-limiting layer.
+ # Redis unavailable — fall back to in-memory rate limiting
rate_limit_logger.warning(
f"Redis unavailable for rate limiting ({scope}), "
- f"allowing request from {client_ip}: {exc}"
+ f"falling back to in-memory limiter for {client_ip}: {exc}"
)
+ _check_rate_limit_memory(client_ip, max_requests, window_seconds, scope)
return
current_count = result[0] # INCR returns the new value
From 038cdb3ed32e7a77a10575431185825d8933c5dd Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Mon, 9 Feb 2026 15:24:38 +0530
Subject: [PATCH 08/35] feat: add new integrations and update icon handling in
the homepage
---
.../components/homepage/integrations.tsx | 35 +++++++++----
.../contracts/enums/connectorIcons.tsx | 10 ++--
.../public/connectors/circleback.svg | 19 +++++++
surfsense_web/public/connectors/linkup.svg | 50 +++++++++++++++++++
4 files changed, 98 insertions(+), 16 deletions(-)
create mode 100644 surfsense_web/public/connectors/circleback.svg
create mode 100644 surfsense_web/public/connectors/linkup.svg
diff --git a/surfsense_web/components/homepage/integrations.tsx b/surfsense_web/components/homepage/integrations.tsx
index d8361d721..fc250e65b 100644
--- a/surfsense_web/components/homepage/integrations.tsx
+++ b/surfsense_web/components/homepage/integrations.tsx
@@ -1,6 +1,7 @@
"use client";
import type React from "react";
+import Image from "next/image";
interface Integration {
name: string;
@@ -44,15 +45,24 @@ const INTEGRATIONS: Integration[] = [
// Media
{ name: "YouTube", icon: "/connectors/youtube.svg" },
+
+ // Search
+ { name: "Linkup", icon: "/connectors/linkup.svg" },
+
+ // Meetings
+ { name: "Circleback", icon: "/connectors/circleback.svg" },
+
+ // AI
+ { name: "MCP", icon: "/connectors/modelcontextprotocol.svg" },
];
-// 5 vertical columns — 21 icons spread across categories
+// 5 vertical columns — 23 icons spread across categories
const COLUMNS: number[][] = [
- [2, 5, 10, 0, 11],
+ [2, 5, 10, 0, 21, 11],
[1, 7, 20, 17],
- [13, 6, 4, 16],
+ [13, 6, 23, 4, 16],
[12, 8, 15, 18],
- [3, 9, 14, 19],
+ [3, 9, 14, 22, 19],
];
// Different scroll speeds per column for organic feel (seconds)
@@ -61,7 +71,7 @@ const SCROLL_DURATIONS = [26, 32, 22, 30, 28];
function IntegrationCard({ integration }: { integration: Integration }) {
return (
-
+
);
}
diff --git a/surfsense_web/contracts/enums/connectorIcons.tsx b/surfsense_web/contracts/enums/connectorIcons.tsx
index 18a872d94..e29e5be6e 100644
--- a/surfsense_web/contracts/enums/connectorIcons.tsx
+++ b/surfsense_web/contracts/enums/connectorIcons.tsx
@@ -1,4 +1,4 @@
-import { IconLinkPlus, IconUsersGroup } from "@tabler/icons-react";
+import { IconUsersGroup } from "@tabler/icons-react";
import {
BookOpen,
File,
@@ -15,11 +15,11 @@ import { EnumConnectorName } from "./connector";
export const getConnectorIcon = (connectorType: EnumConnectorName | string, className?: string) => {
const iconProps = { className: className || "h-4 w-4" };
- const imgProps = { className: className || "h-5 w-5", width: 20, height: 20 };
+ const imgProps = { className: `${className || "h-5 w-5"} select-none pointer-events-none`, width: 20, height: 20, draggable: false as const };
switch (connectorType) {
- case EnumConnectorName.LINKUP_API:
- return
;
+ case EnumConnectorName.LINKUP_API:
+ return
;
case EnumConnectorName.LINEAR_CONNECTOR:
return
;
case EnumConnectorName.GITHUB_CONNECTOR:
@@ -63,7 +63,7 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
case EnumConnectorName.YOUTUBE_CONNECTOR:
return
;
case EnumConnectorName.CIRCLEBACK_CONNECTOR:
- return
;
+ return
;
case EnumConnectorName.MCP_CONNECTOR:
return
;
case EnumConnectorName.OBSIDIAN_CONNECTOR:
diff --git a/surfsense_web/public/connectors/circleback.svg b/surfsense_web/public/connectors/circleback.svg
new file mode 100644
index 000000000..76bdcddd8
--- /dev/null
+++ b/surfsense_web/public/connectors/circleback.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/surfsense_web/public/connectors/linkup.svg b/surfsense_web/public/connectors/linkup.svg
new file mode 100644
index 000000000..8b0ffb071
--- /dev/null
+++ b/surfsense_web/public/connectors/linkup.svg
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From dfa05917404f00e2a654bba9ae970232020a8657 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Mon, 9 Feb 2026 15:24:47 +0530
Subject: [PATCH 09/35] chore: ran linting
---
.../(manage)/components/RowActions.tsx | 3 +-
.../components/homepage/integrations.tsx | 61 ++++++++++---------
.../contracts/enums/connectorIcons.tsx | 11 +++-
3 files changed, 41 insertions(+), 34 deletions(-)
diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx
index eb44d114a..5c5a44964 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx
@@ -208,7 +208,8 @@ export function RowActions({
Delete document?
- This action cannot be undone. This will permanently delete this document from your search space.
+ This action cannot be undone. This will permanently delete this document from your
+ search space.
diff --git a/surfsense_web/components/homepage/integrations.tsx b/surfsense_web/components/homepage/integrations.tsx
index fc250e65b..662387de5 100644
--- a/surfsense_web/components/homepage/integrations.tsx
+++ b/surfsense_web/components/homepage/integrations.tsx
@@ -73,21 +73,19 @@ function IntegrationCard({ integration }: { integration: Integration }) {
-
+
);
}
@@ -132,7 +130,10 @@ function ScrollingColumn({
));
return (
-
+
{/* Outer div has NO gap — each inner copy uses pb matching the gap so both halves are identical in height → seamless -50% loop */}
{/* Heading */}
@@ -203,15 +204,15 @@ export default function ExternalIntegrations() {
{/* 5 scrolling columns */}
{COLUMNS.map((column, colIndex) => (
-
+
))}
diff --git a/surfsense_web/contracts/enums/connectorIcons.tsx b/surfsense_web/contracts/enums/connectorIcons.tsx
index e29e5be6e..c9375a5ca 100644
--- a/surfsense_web/contracts/enums/connectorIcons.tsx
+++ b/surfsense_web/contracts/enums/connectorIcons.tsx
@@ -15,11 +15,16 @@ import { EnumConnectorName } from "./connector";
export const getConnectorIcon = (connectorType: EnumConnectorName | string, className?: string) => {
const iconProps = { className: className || "h-4 w-4" };
- const imgProps = { className: `${className || "h-5 w-5"} select-none pointer-events-none`, width: 20, height: 20, draggable: false as const };
+ const imgProps = {
+ className: `${className || "h-5 w-5"} select-none pointer-events-none`,
+ width: 20,
+ height: 20,
+ draggable: false as const,
+ };
switch (connectorType) {
- case EnumConnectorName.LINKUP_API:
- return
;
+ case EnumConnectorName.LINKUP_API:
+ return
;
case EnumConnectorName.LINEAR_CONNECTOR:
return
;
case EnumConnectorName.GITHUB_CONNECTOR:
From 4b60068e8b7a73484c4d71d913fb00a8865fb959 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Mon, 9 Feb 2026 18:30:52 +0530
Subject: [PATCH 10/35] feat: add user_id to new_llm_configs and
image_generation_configs for user association
---
...96_add_user_id_to_llm_and_image_configs.py | 144 ++++++++++++++++++
surfsense_backend/app/db.py | 55 +++++++
.../app/routes/image_generation_routes.py | 2 +-
.../app/routes/new_llm_config_routes.py | 4 +-
.../app/schemas/image_generation.py | 3 +
.../app/schemas/new_llm_config.py | 3 +
.../contracts/types/new-llm-config.types.ts | 7 +-
7 files changed, 214 insertions(+), 4 deletions(-)
create mode 100644 surfsense_backend/alembic/versions/96_add_user_id_to_llm_and_image_configs.py
diff --git a/surfsense_backend/alembic/versions/96_add_user_id_to_llm_and_image_configs.py b/surfsense_backend/alembic/versions/96_add_user_id_to_llm_and_image_configs.py
new file mode 100644
index 000000000..7f600a9e3
--- /dev/null
+++ b/surfsense_backend/alembic/versions/96_add_user_id_to_llm_and_image_configs.py
@@ -0,0 +1,144 @@
+"""Add user_id to new_llm_configs and image_generation_configs
+
+Revision ID: 96
+Revises: 95
+"""
+
+from collections.abc import Sequence
+
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "96"
+down_revision: str | None = "95"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ """Add user_id column to new_llm_configs and image_generation_configs.
+
+ Backfills existing rows with the search space owner's user_id.
+ """
+ # --- new_llm_configs ---
+ # 1. Add nullable column first
+ op.execute(
+ """
+ ALTER TABLE new_llm_configs
+ ADD COLUMN IF NOT EXISTS user_id UUID;
+ """
+ )
+
+ # 2. Backfill from search space owner
+ op.execute(
+ """
+ UPDATE new_llm_configs nlc
+ SET user_id = ss.user_id
+ FROM searchspaces ss
+ WHERE nlc.search_space_id = ss.id
+ AND nlc.user_id IS NULL;
+ """
+ )
+
+ # 3. Make NOT NULL
+ op.execute(
+ """
+ ALTER TABLE new_llm_configs
+ ALTER COLUMN user_id SET NOT NULL;
+ """
+ )
+
+ # 4. Add FK constraint
+ op.execute(
+ """
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM information_schema.table_constraints
+ WHERE constraint_name = 'fk_new_llm_configs_user_id'
+ AND table_name = 'new_llm_configs'
+ ) THEN
+ ALTER TABLE new_llm_configs
+ ADD CONSTRAINT fk_new_llm_configs_user_id
+ FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE;
+ END IF;
+ END$$;
+ """
+ )
+
+ # 5. Add index for user_id lookups
+ op.execute(
+ """
+ CREATE INDEX IF NOT EXISTS ix_new_llm_configs_user_id
+ ON new_llm_configs (user_id);
+ """
+ )
+
+ # --- image_generation_configs ---
+ # 1. Add nullable column first
+ op.execute(
+ """
+ ALTER TABLE image_generation_configs
+ ADD COLUMN IF NOT EXISTS user_id UUID;
+ """
+ )
+
+ # 2. Backfill from search space owner
+ op.execute(
+ """
+ UPDATE image_generation_configs igc
+ SET user_id = ss.user_id
+ FROM searchspaces ss
+ WHERE igc.search_space_id = ss.id
+ AND igc.user_id IS NULL;
+ """
+ )
+
+ # 3. Make NOT NULL
+ op.execute(
+ """
+ ALTER TABLE image_generation_configs
+ ALTER COLUMN user_id SET NOT NULL;
+ """
+ )
+
+ # 4. Add FK constraint
+ op.execute(
+ """
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM information_schema.table_constraints
+ WHERE constraint_name = 'fk_image_generation_configs_user_id'
+ AND table_name = 'image_generation_configs'
+ ) THEN
+ ALTER TABLE image_generation_configs
+ ADD CONSTRAINT fk_image_generation_configs_user_id
+ FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE;
+ END IF;
+ END$$;
+ """
+ )
+
+ # 5. Add index for user_id lookups
+ op.execute(
+ """
+ CREATE INDEX IF NOT EXISTS ix_image_generation_configs_user_id
+ ON image_generation_configs (user_id);
+ """
+ )
+
+
+def downgrade() -> None:
+ """Remove user_id from new_llm_configs and image_generation_configs."""
+ op.execute(
+ """
+ ALTER TABLE new_llm_configs DROP COLUMN IF EXISTS user_id;
+ """
+ )
+ op.execute(
+ """
+ ALTER TABLE image_generation_configs DROP COLUMN IF EXISTS user_id;
+ """
+ )
+
diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py
index 0cab2820b..4f8648616 100644
--- a/surfsense_backend/app/db.py
+++ b/surfsense_backend/app/db.py
@@ -1032,6 +1032,12 @@ class ImageGenerationConfig(BaseModel, TimestampMixin):
"SearchSpace", back_populates="image_generation_configs"
)
+ # User who created this config
+ user_id = Column(
+ UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False
+ )
+ user = relationship("User", back_populates="image_generation_configs")
+
class ImageGeneration(BaseModel, TimestampMixin):
"""
@@ -1244,6 +1250,7 @@ class SearchSourceConnector(BaseModel, TimestampMixin):
user_id = Column(
UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False
)
+ user = relationship("User", back_populates="search_source_connectors")
# Documents created by this connector (for cleanup on connector deletion)
documents = relationship("Document", back_populates="connector")
@@ -1300,6 +1307,12 @@ class NewLLMConfig(BaseModel, TimestampMixin):
)
search_space = relationship("SearchSpace", back_populates="new_llm_configs")
+ # User who created this config
+ user_id = Column(
+ UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False
+ )
+ user = relationship("User", back_populates="new_llm_configs")
+
class Log(BaseModel, TimestampMixin):
__tablename__ = "logs"
@@ -1568,6 +1581,27 @@ if config.AUTH_TYPE == "GOOGLE":
passive_deletes=True,
)
+ # Connectors created by this user
+ search_source_connectors = relationship(
+ "SearchSourceConnector",
+ back_populates="user",
+ passive_deletes=True,
+ )
+
+ # LLM configs created by this user
+ new_llm_configs = relationship(
+ "NewLLMConfig",
+ back_populates="user",
+ passive_deletes=True,
+ )
+
+ # Image generation configs created by this user
+ image_generation_configs = relationship(
+ "ImageGenerationConfig",
+ back_populates="user",
+ passive_deletes=True,
+ )
+
# User memories for personalized AI responses
memories = relationship(
"UserMemory",
@@ -1647,6 +1681,27 @@ else:
passive_deletes=True,
)
+ # Connectors created by this user
+ search_source_connectors = relationship(
+ "SearchSourceConnector",
+ back_populates="user",
+ passive_deletes=True,
+ )
+
+ # LLM configs created by this user
+ new_llm_configs = relationship(
+ "NewLLMConfig",
+ back_populates="user",
+ passive_deletes=True,
+ )
+
+ # Image generation configs created by this user
+ image_generation_configs = relationship(
+ "ImageGenerationConfig",
+ back_populates="user",
+ passive_deletes=True,
+ )
+
# User memories for personalized AI responses
memories = relationship(
"UserMemory",
diff --git a/surfsense_backend/app/routes/image_generation_routes.py b/surfsense_backend/app/routes/image_generation_routes.py
index 9406867c6..a8963e181 100644
--- a/surfsense_backend/app/routes/image_generation_routes.py
+++ b/surfsense_backend/app/routes/image_generation_routes.py
@@ -273,7 +273,7 @@ async def create_image_gen_config(
"You don't have permission to create image generation configs in this search space",
)
- db_config = ImageGenerationConfig(**config_data.model_dump())
+ db_config = ImageGenerationConfig(**config_data.model_dump(), user_id=user.id)
session.add(db_config)
await session.commit()
await session.refresh(db_config)
diff --git a/surfsense_backend/app/routes/new_llm_config_routes.py b/surfsense_backend/app/routes/new_llm_config_routes.py
index ed7d62d31..f90e86594 100644
--- a/surfsense_backend/app/routes/new_llm_config_routes.py
+++ b/surfsense_backend/app/routes/new_llm_config_routes.py
@@ -149,8 +149,8 @@ async def create_new_llm_config(
detail=f"Invalid LLM configuration: {error_message}",
)
- # Create the config
- db_config = NewLLMConfig(**config_data.model_dump())
+ # Create the config with user association
+ db_config = NewLLMConfig(**config_data.model_dump(), user_id=user.id)
session.add(db_config)
await session.commit()
await session.refresh(db_config)
diff --git a/surfsense_backend/app/schemas/image_generation.py b/surfsense_backend/app/schemas/image_generation.py
index 6ef4feff8..69f534e20 100644
--- a/surfsense_backend/app/schemas/image_generation.py
+++ b/surfsense_backend/app/schemas/image_generation.py
@@ -6,6 +6,7 @@ ImageGeneration: Schemas for the actual image generation requests/results.
GlobalImageGenConfigRead: Schema for admin-configured YAML configs.
"""
+import uuid
from datetime import datetime
from typing import Any
@@ -79,6 +80,7 @@ class ImageGenerationConfigRead(ImageGenerationConfigBase):
id: int
created_at: datetime
search_space_id: int
+ user_id: uuid.UUID
model_config = ConfigDict(from_attributes=True)
@@ -97,6 +99,7 @@ class ImageGenerationConfigPublic(BaseModel):
litellm_params: dict[str, Any] | None = None
created_at: datetime
search_space_id: int
+ user_id: uuid.UUID
model_config = ConfigDict(from_attributes=True)
diff --git a/surfsense_backend/app/schemas/new_llm_config.py b/surfsense_backend/app/schemas/new_llm_config.py
index a6294fba2..9863665b6 100644
--- a/surfsense_backend/app/schemas/new_llm_config.py
+++ b/surfsense_backend/app/schemas/new_llm_config.py
@@ -7,6 +7,7 @@ NewLLMConfig combines LLM model settings with prompt configuration:
- Citation toggle
"""
+import uuid
from datetime import datetime
from typing import Any
@@ -90,6 +91,7 @@ class NewLLMConfigRead(NewLLMConfigBase):
id: int
created_at: datetime
search_space_id: int
+ user_id: uuid.UUID
model_config = ConfigDict(from_attributes=True)
@@ -118,6 +120,7 @@ class NewLLMConfigPublic(BaseModel):
created_at: datetime
search_space_id: int
+ user_id: uuid.UUID
model_config = ConfigDict(from_attributes=True)
diff --git a/surfsense_web/contracts/types/new-llm-config.types.ts b/surfsense_web/contracts/types/new-llm-config.types.ts
index b99df1022..7b3fca8b0 100644
--- a/surfsense_web/contracts/types/new-llm-config.types.ts
+++ b/surfsense_web/contracts/types/new-llm-config.types.ts
@@ -62,6 +62,7 @@ export const newLLMConfig = z.object({
// Metadata
created_at: z.string(),
search_space_id: z.number(),
+ user_id: z.string(),
});
/**
@@ -75,6 +76,7 @@ export const newLLMConfigPublic = newLLMConfig.omit({ api_key: true });
export const createNewLLMConfigRequest = newLLMConfig.omit({
id: true,
created_at: true,
+ user_id: true,
});
export const createNewLLMConfigResponse = newLLMConfig;
@@ -109,6 +111,7 @@ export const updateNewLLMConfigRequest = z.object({
id: true,
created_at: true,
search_space_id: true,
+ user_id: true,
})
.partial(),
});
@@ -200,11 +203,13 @@ export const imageGenerationConfig = z.object({
litellm_params: z.record(z.string(), z.any()).nullable().optional(),
created_at: z.string(),
search_space_id: z.number(),
+ user_id: z.string(),
});
export const createImageGenConfigRequest = imageGenerationConfig.omit({
id: true,
created_at: true,
+ user_id: true,
});
export const createImageGenConfigResponse = imageGenerationConfig;
@@ -213,7 +218,7 @@ export const getImageGenConfigsResponse = z.array(imageGenerationConfig);
export const updateImageGenConfigRequest = z.object({
id: z.number(),
- data: imageGenerationConfig.omit({ id: true, created_at: true, search_space_id: true }).partial(),
+ data: imageGenerationConfig.omit({ id: true, created_at: true, search_space_id: true, user_id: true }).partial(),
});
export const updateImageGenConfigResponse = imageGenerationConfig;
From a33a2161aa5b712ada5a9a9171e5b224254c5f39 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Mon, 9 Feb 2026 19:00:57 +0530
Subject: [PATCH 11/35] feat: enhance user association by integrating member
data into public chat and image model managers
---
surfsense_backend/app/schemas/new_chat.py | 1 +
.../app/services/public_chat_service.py | 1 +
.../public-chat-snapshot-row.tsx | 177 ++++++---
.../public-chat-snapshots-list.tsx | 5 +-
.../public-chat-snapshots-manager.tsx | 72 ++--
.../settings/image-model-manager.tsx | 55 ++-
.../components/settings/llm-role-manager.tsx | 4 +-
.../settings/model-config-manager.tsx | 350 ++++++++++--------
.../settings/prompt-config-manager.tsx | 2 +-
.../contracts/types/chat-threads.types.ts | 1 +
10 files changed, 412 insertions(+), 256 deletions(-)
diff --git a/surfsense_backend/app/schemas/new_chat.py b/surfsense_backend/app/schemas/new_chat.py
index 61af0d92c..84ba1b950 100644
--- a/surfsense_backend/app/schemas/new_chat.py
+++ b/surfsense_backend/app/schemas/new_chat.py
@@ -246,6 +246,7 @@ class PublicChatSnapshotDetail(BaseModel):
message_count: int
thread_id: int
thread_title: str
+ created_by_user_id: str | None = None
class PublicChatSnapshotsBySpaceResponse(BaseModel):
diff --git a/surfsense_backend/app/services/public_chat_service.py b/surfsense_backend/app/services/public_chat_service.py
index 4da316240..9088ed748 100644
--- a/surfsense_backend/app/services/public_chat_service.py
+++ b/surfsense_backend/app/services/public_chat_service.py
@@ -439,6 +439,7 @@ async def list_snapshots_for_search_space(
"message_count": len(s.message_ids) if s.message_ids else 0,
"thread_id": s.thread_id,
"thread_title": thread_titles.get(s.thread_id, "Untitled"),
+ "created_by_user_id": str(s.created_by_user_id) if s.created_by_user_id else None,
}
for s in snapshots
]
diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx
index 5f0048100..d27166bf1 100644
--- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx
+++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx
@@ -1,15 +1,28 @@
"use client";
-import { Copy, MessageSquare, Trash2 } from "lucide-react";
+import { Copy, ExternalLink, MessageSquare, Trash2 } from "lucide-react";
+import Image from "next/image";
+import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import type { PublicChatSnapshotDetail } from "@/contracts/types/chat-threads.types";
+function getInitials(name: string): string {
+ const parts = name.trim().split(/\s+/);
+ if (parts.length >= 2) {
+ return (parts[0][0] + parts[1][0]).toUpperCase();
+ }
+ return name.slice(0, 2).toUpperCase();
+}
+
interface PublicChatSnapshotRowProps {
snapshot: PublicChatSnapshotDetail;
canDelete: boolean;
onCopy: (snapshot: PublicChatSnapshotDetail) => void;
onDelete: (snapshot: PublicChatSnapshotDetail) => void;
isDeleting?: boolean;
+ memberMap: Map
;
}
export function PublicChatSnapshotRow({
@@ -18,6 +31,7 @@ export function PublicChatSnapshotRow({
onCopy,
onDelete,
isDeleting = false,
+ memberMap,
}: PublicChatSnapshotRowProps) {
const formattedDate = new Date(snapshot.created_at).toLocaleDateString(undefined, {
year: "numeric",
@@ -25,50 +39,127 @@ export function PublicChatSnapshotRow({
day: "numeric",
});
+ const member = snapshot.created_by_user_id
+ ? memberMap.get(snapshot.created_by_user_id)
+ : null;
+
return (
-
-
-
- {snapshot.thread_title}
-
-
-
{formattedDate}
-
-
- {snapshot.message_count}
-
+
+
+ {/* Header: Title + Actions */}
+
+
+
+ {snapshot.thread_title}
+
+
+
+
+
+
+ onCopy(snapshot)}
+ className="h-7 w-7 text-muted-foreground hover:text-foreground"
+ >
+
+
+
+ Copy link
+
+
+
+
+
+
+
+
+
+ Open link
+
+
+ {canDelete && (
+
+
+
+ onDelete(snapshot)}
+ disabled={isDeleting}
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
+ >
+
+
+
+ Delete
+
+
+ )}
+
- (e.target as HTMLInputElement).select()}
- />
-
-
-
onCopy(snapshot)}
- className="h-8 px-2"
- title="Copy link"
- >
-
-
- {canDelete && (
-
onDelete(snapshot)}
- disabled={isDeleting}
- className="h-8 px-2 text-destructive hover:text-destructive hover:bg-destructive/10"
- title="Delete link"
+
+ {/* Message count badge */}
+
+
-
-
- )}
-
-
+
+ {snapshot.message_count} messages
+
+
+
+ {/* Footer: Date + Creator */}
+
+
+ {formattedDate}
+
+ {member && (
+ <>
+
·
+
+
+
+
+ {member.avatarUrl ? (
+
+ ) : (
+
+
+ {getInitials(member.name)}
+
+
+ )}
+
+ {member.name}
+
+
+
+
+ {member.email || member.name}
+
+
+
+ >
+ )}
+
+
+
);
}
diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-list.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-list.tsx
index 38c435059..8daf9a07f 100644
--- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-list.tsx
+++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-list.tsx
@@ -10,6 +10,7 @@ interface PublicChatSnapshotsListProps {
onCopy: (snapshot: PublicChatSnapshotDetail) => void;
onDelete: (snapshot: PublicChatSnapshotDetail) => void;
deletingId?: number;
+ memberMap: Map
;
}
export function PublicChatSnapshotsList({
@@ -18,13 +19,14 @@ export function PublicChatSnapshotsList({
onCopy,
onDelete,
deletingId,
+ memberMap,
}: PublicChatSnapshotsListProps) {
if (snapshots.length === 0) {
return ;
}
return (
-
+
{snapshots.map((snapshot) => (
))}
diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx
index 167443f66..07ff57c0d 100644
--- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx
+++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx
@@ -1,14 +1,13 @@
"use client";
import { useAtomValue } from "jotai";
-import { AlertCircle, Globe, Info } from "lucide-react";
+import { AlertCircle, Info } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
-import { myAccessAtom } from "@/atoms/members/members-query.atoms";
+import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import { deletePublicChatSnapshotMutationAtom } from "@/atoms/public-chat-snapshots/public-chat-snapshots-mutation.atoms";
import { publicChatSnapshotsAtom } from "@/atoms/public-chat-snapshots/public-chat-snapshots-query.atoms";
import { Alert, AlertDescription } from "@/components/ui/alert";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import type { PublicChatSnapshotDetail } from "@/contracts/types/chat-threads.types";
import { PublicChatSnapshotsList } from "./public-chat-snapshots-list";
@@ -25,6 +24,22 @@ export function PublicChatSnapshotsManager({
// Data fetching
const { data: snapshotsData, isLoading, isError } = useAtomValue(publicChatSnapshotsAtom);
+ // Members for user resolution
+ const { data: members } = useAtomValue(membersAtom);
+ const memberMap = useMemo(() => {
+ const map = new Map
();
+ if (members) {
+ for (const m of members) {
+ map.set(m.user_id, {
+ name: m.user_display_name || m.user_email || "Unknown",
+ email: m.user_email || undefined,
+ avatarUrl: m.user_avatar_url || undefined,
+ });
+ }
+ }
+ return map;
+ }, [members]);
+
// Permissions
const { data: access } = useAtomValue(myAccessAtom);
const canView = useMemo(() => {
@@ -69,16 +84,13 @@ export function PublicChatSnapshotsManager({
// Loading state
if (isLoading) {
return (
-
-
-
-
-
-
-
-
-
-
+
);
}
@@ -110,35 +122,23 @@ export function PublicChatSnapshotsManager({
const snapshots = snapshotsData?.snapshots ?? [];
return (
-
-
-
+
+
+
Public chat links allow anyone with the URL to view a snapshot of a chat. These links do
not update when the original chat changes.
-
-
-
-
- Public Chat Links
-
-
- Manage public links to chats in this search space.
-
-
-
-
-
-
+
);
}
diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx
index e87cc9a95..013b7cae2 100644
--- a/surfsense_web/components/settings/image-model-manager.tsx
+++ b/surfsense_web/components/settings/image-model-manager.tsx
@@ -12,13 +12,15 @@ import {
Plus,
RefreshCw,
Shuffle,
- Sparkles,
+ Info,
Trash2,
+ User,
Wand2,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
+import { membersAtom } from "@/atoms/members/members-query.atoms";
import {
createImageGenConfigMutationAtom,
deleteImageGenConfigMutationAtom,
@@ -122,6 +124,21 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
useAtomValue(globalImageGenConfigsAtom);
const { data: preferences = {}, isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom);
+ // Members for user resolution
+ const { data: members } = useAtomValue(membersAtom);
+ const memberMap = useMemo(() => {
+ const map = new Map();
+ if (members) {
+ for (const m of members) {
+ map.set(m.user_id, {
+ name: m.user_display_name || m.user_email || "Unknown",
+ email: m.user_email || undefined,
+ });
+ }
+ }
+ return map;
+ }, [members]);
+
// Local state
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingConfig, setEditingConfig] = useState(null);
@@ -320,16 +337,16 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{/* Global info */}
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && (
-
-
-
-
- {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length} global
- image model(s)
- {" "}
- available from your administrator.
-
-
+
+
+
+
+ {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length} global
+ image model(s)
+ {" "}
+ available from your administrator.
+
+
)}
{/* Active Preference Card */}
@@ -521,9 +538,17 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{config.description}
)}
-
-
- {new Date(config.created_at).toLocaleDateString()}
+
+
+
+ {new Date(config.created_at).toLocaleDateString()}
+
+ {config.user_id && memberMap.get(config.user_id) && (
+
+
+ {memberMap.get(config.user_id)?.name}
+
+ )}
diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx
index dac68a358..d91150a4f 100644
--- a/surfsense_web/components/settings/llm-role-manager.tsx
+++ b/surfsense_web/components/settings/llm-role-manager.tsx
@@ -239,7 +239,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
) : !isAssignmentComplete ? (
-
+
Complete all role assignments to enable full functionality. Each role serves
@@ -247,7 +247,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
) : (
-
+
All roles are assigned and ready to use! Your LLM configuration is complete.
diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx
index bdd951349..3ee5458b8 100644
--- a/surfsense_web/components/settings/model-config-manager.tsx
+++ b/surfsense_web/components/settings/model-config-manager.tsx
@@ -3,19 +3,19 @@
import { useAtomValue } from "jotai";
import {
AlertCircle,
- Bot,
- Clock,
Edit3,
FileText,
MessageSquareQuote,
Plus,
RefreshCw,
- Sparkles,
+ Info,
Trash2,
Wand2,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
-import { useCallback, useState } from "react";
+import Image from "next/image";
+import { useCallback, useMemo, useState } from "react";
+import { membersAtom } from "@/atoms/members/members-query.atoms";
import {
createNewLLMConfigMutationAtom,
deleteNewLLMConfigMutationAtom,
@@ -71,6 +71,14 @@ const item = {
show: { opacity: 1, y: 0 },
};
+function getInitials(name: string): string {
+ const parts = name.trim().split(/\s+/);
+ if (parts.length >= 2) {
+ return (parts[0][0] + parts[1][0]).toUpperCase();
+ }
+ return name.slice(0, 2).toUpperCase();
+}
+
export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
// Mutations
const {
@@ -98,6 +106,22 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
} = useAtomValue(newLLMConfigsAtom);
const { data: globalConfigs = [] } = useAtomValue(globalNewLLMConfigsAtom);
+ // Members for user resolution
+ const { data: members } = useAtomValue(membersAtom);
+ const memberMap = useMemo(() => {
+ const map = new Map();
+ if (members) {
+ for (const m of members) {
+ map.set(m.user_id, {
+ name: m.user_display_name || m.user_email || "Unknown",
+ email: m.user_email || undefined,
+ avatarUrl: m.user_avatar_url || undefined,
+ });
+ }
+ }
+ return map;
+ }, [members]);
+
// Local state
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingConfig, setEditingConfig] = useState(null);
@@ -153,21 +177,27 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
};
return (
-
- {/* Header */}
-
-
- refreshConfigs()}
- disabled={isLoading}
- className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
- >
-
- Refresh
-
-
+
+ {/* Header actions */}
+
+
refreshConfigs()}
+ disabled={isLoading}
+ className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
+ >
+
+ Refresh
+
+
+
+ Add Configuration
+
{/* Error Alerts */}
@@ -193,12 +223,12 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
{/* Global Configs Info */}
{globalConfigs.length > 0 && (
-
-
-
+
+
+
{globalConfigs.length} global configuration(s) {" "}
available from your administrator. These are pre-configured and ready to use.{" "}
-
+
Global configs: {globalConfigs.map((g) => g.name).join(", ")}
@@ -222,18 +252,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
{/* Configurations List */}
{!isLoading && (
-
-
-
Your Configurations
-
-
- Add Configuration
-
-
-
+
{configs?.length === 0 ? (
@@ -259,9 +278,16 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
) : (
-
+
{configs?.map((config) => {
+ const member = config.user_id ? memberMap.get(config.user_id) : null;
+
return (
-
-
-
- {/* Left accent bar */}
-
+
+
+ {/* Header: Name + Actions */}
+
+
+
+ {config.name}
+
+ {config.description && (
+
+ {config.description}
+
+ )}
+
+
+
+
+
+ openEditDialog(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-foreground"
+ >
+
+
+
+ Edit
+
+
+
+
+
+ setConfigToDelete(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
+ >
+
+
+
+ Delete
+
+
+
+
-
-
- {/* Main content */}
-
-
-
-
-
- {/* Title row */}
-
-
- {config.name}
-
-
-
- {config.provider}
-
- {config.citations_enabled && (
-
-
-
-
-
- Citations
-
-
-
- Citations are enabled for this configuration
-
-
-
+ {/* Provider + Model */}
+
+
+ {config.provider}
+
+
+ {config.model_name}
+
+
+
+ {/* Feature badges */}
+
+ {config.citations_enabled && (
+
+
+ Citations
+
+ )}
+ {!config.use_default_system_instructions &&
+ config.system_instructions && (
+
+
+ Custom
+
+ )}
+
+
+ {/* Footer: Date + Creator */}
+
+
+ {new Date(config.created_at).toLocaleDateString(
+ undefined,
+ {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ }
+ )}
+
+ {member && (
+ <>
+
·
+
+
+
+
+ {member.avatarUrl ? (
+
+ ) : (
+
+
+ {getInitials(member.name)}
+
+
)}
- {!config.use_default_system_instructions &&
- config.system_instructions && (
-
-
-
-
-
- Custom
-
-
-
- Using custom system instructions
-
-
-
- )}
-
-
-
- {/* Model name */}
-
- {config.model_name}
-
-
- {/* Description if any */}
- {config.description && (
-
- {config.description}
-
- )}
-
- {/* Footer row */}
-
-
-
-
- {new Date(config.created_at).toLocaleDateString()}
+
+ {member.name}
-
-
-
-
- {/* Actions */}
-
-
-
-
- openEditDialog(config)}
- className="h-7 w-7 md:h-8 md:w-8 p-0 text-muted-foreground hover:text-foreground"
- >
-
-
-
- Edit
-
-
-
-
-
- setConfigToDelete(config)}
- className="h-7 w-7 md:h-8 md:w-8 p-0 text-muted-foreground hover:text-destructive"
- >
-
-
-
- Delete
-
-
-
-
-
+
+
+ {member.email || member.name}
+
+
+
+ >
+ )}
@@ -408,7 +439,10 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
{/* Add/Edit Configuration Dialog */}
!open && closeDialog()}>
-
+ e.preventDefault()}
+ >
{editingConfig ? (
diff --git a/surfsense_web/components/settings/prompt-config-manager.tsx b/surfsense_web/components/settings/prompt-config-manager.tsx
index 64f6adf23..54058759f 100644
--- a/surfsense_web/components/settings/prompt-config-manager.tsx
+++ b/surfsense_web/components/settings/prompt-config-manager.tsx
@@ -122,7 +122,7 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
-
+
System instructions apply to all AI interactions in this search space. They guide how the
diff --git a/surfsense_web/contracts/types/chat-threads.types.ts b/surfsense_web/contracts/types/chat-threads.types.ts
index df561092e..d245a4168 100644
--- a/surfsense_web/contracts/types/chat-threads.types.ts
+++ b/surfsense_web/contracts/types/chat-threads.types.ts
@@ -55,6 +55,7 @@ export const publicChatSnapshotDetail = z.object({
message_count: z.number(),
thread_id: z.number(),
thread_title: z.string(),
+ created_by_user_id: z.string().nullable().optional(),
});
/**
From adc4bc7075f0f436424dc285cc1cde55bb82d71c Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Mon, 9 Feb 2026 19:26:23 +0530
Subject: [PATCH 12/35] feat: enhance loading states and skeleton UI for
various components in settings and chat snapshots
---
.../public-chat-snapshot-row.tsx | 2 +-
.../public-chat-snapshots-manager.tsx | 27 +-
.../settings/general-settings-manager.tsx | 2 +-
.../settings/image-model-manager.tsx | 304 ++++++++++++------
.../settings/model-config-manager.tsx | 95 +++---
5 files changed, 277 insertions(+), 153 deletions(-)
diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx
index d27166bf1..e1fdb5605 100644
--- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx
+++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx
@@ -146,7 +146,7 @@ export function PublicChatSnapshotRow({
)}
-
+
{member.name}
diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx
index 07ff57c0d..a11e11198 100644
--- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx
+++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx
@@ -8,6 +8,7 @@ import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import { deletePublicChatSnapshotMutationAtom } from "@/atoms/public-chat-snapshots/public-chat-snapshots-mutation.atoms";
import { publicChatSnapshotsAtom } from "@/atoms/public-chat-snapshots/public-chat-snapshots-query.atoms";
import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import type { PublicChatSnapshotDetail } from "@/contracts/types/chat-threads.types";
import { PublicChatSnapshotsList } from "./public-chat-snapshots-list";
@@ -85,11 +86,31 @@ export function PublicChatSnapshotsManager({
if (isLoading) {
return (
+ {/* Info alert skeleton */}
+
+ {/* Cards grid skeleton */}
-
-
-
+ {["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
+
+
+ {/* Header: Title */}
+
+
+
+ {/* Message count badge */}
+
+
+
+ {/* Footer: Date + Creator */}
+
+
+
+
+
+
+
+ ))}
);
diff --git a/surfsense_web/components/settings/general-settings-manager.tsx b/surfsense_web/components/settings/general-settings-manager.tsx
index 64d9ed876..bd22e9180 100644
--- a/surfsense_web/components/settings/general-settings-manager.tsx
+++ b/surfsense_web/components/settings/general-settings-manager.tsx
@@ -108,7 +108,7 @@ export function GeneralSettingsManager({ searchSpaceId }: GeneralSettingsManager
return (
-
+
Update your search space name and description. These details help identify and organize
diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx
index 013b7cae2..445d87a2a 100644
--- a/surfsense_web/components/settings/image-model-manager.tsx
+++ b/surfsense_web/components/settings/image-model-manager.tsx
@@ -5,7 +5,6 @@ import {
AlertCircle,
Check,
ChevronsUpDown,
- Clock,
Edit3,
ImageIcon,
Key,
@@ -14,10 +13,10 @@ import {
Shuffle,
Info,
Trash2,
- User,
Wand2,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
+import Image from "next/image";
import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { membersAtom } from "@/atoms/members/members-query.atoms";
@@ -72,6 +71,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
+import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
@@ -95,6 +95,14 @@ const item = {
show: { opacity: 1, y: 0 },
};
+function getInitials(name: string): string {
+ const parts = name.trim().split(/\s+/);
+ if (parts.length >= 2) {
+ return (parts[0][0] + parts[1][0]).toUpperCase();
+ }
+ return name.slice(0, 2).toUpperCase();
+}
+
export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
// Image gen config atoms
const {
@@ -127,12 +135,13 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
// Members for user resolution
const { data: members } = useAtomValue(membersAtom);
const memberMap = useMemo(() => {
- const map = new Map();
+ const map = new Map();
if (members) {
for (const m of members) {
map.set(m.user_id, {
name: m.user_display_name || m.user_email || "Unknown",
email: m.user_email || undefined,
+ avatarUrl: m.user_avatar_url || undefined,
});
}
}
@@ -459,13 +468,61 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)}
- {/* Loading */}
+ {/* Loading Skeleton */}
{isLoading && (
-
-
-
-
-
+
+ {/* Active Preference Skeleton */}
+
+
+
+
+
+
+
+
+
+ {/* Your Image Models Section Skeleton */}
+
+
+
+
+
+
+ {/* Cards Grid Skeleton */}
+
+ {["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
+
+
+ {/* Header */}
+
+ {/* Provider + Model */}
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+
+
+ ))}
+
+
+
)}
{/* User Configs */}
@@ -477,7 +534,6 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
onClick={openNewDialog}
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
>
-
Add Image Model
@@ -492,105 +548,143 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
Add your own image generation model (DALL-E 3, GPT Image 1, etc.)
-
+
Add First Image Model
) : (
-
+
- {userConfigs?.map((config) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {config.name}
-
-
{
+ const member = config.user_id ? memberMap.get(config.user_id) : null;
+
+ return (
+
+
+
+ {/* Header: Name + Actions */}
+
+
+
+ {config.name}
+
+ {config.description && (
+
+ {config.description}
+
+ )}
+
+
+
+
+
+ openEditDialog(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-foreground"
>
- {config.provider}
-
-
-
- {config.model_name}
-
- {config.description && (
-
- {config.description}
-
- )}
-
-
-
- {new Date(config.created_at).toLocaleDateString()}
-
- {config.user_id && memberMap.get(config.user_id) && (
-
-
- {memberMap.get(config.user_id)?.name}
-
- )}
-
-
-
-
-
-
-
- openEditDialog(config)}
- className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
- >
-
-
-
- Edit
-
-
-
-
-
- setConfigToDelete(config)}
- className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive"
- >
-
-
-
- Delete
-
-
-
+
+
+
+
Edit
+
+
+
+
+
+ setConfigToDelete(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
+ >
+
+
+
+ Delete
+
+
-
-
-
-
- ))}
+
+ {/* Provider + Model */}
+
+
+ {config.provider}
+
+
+ {config.model_name}
+
+
+
+ {/* Footer: Date + Creator */}
+
+
+ {new Date(config.created_at).toLocaleDateString(
+ undefined,
+ {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ }
+ )}
+
+ {member && (
+ <>
+
·
+
+
+
+
+ {member.avatarUrl ? (
+
+ ) : (
+
+
+ {getInitials(member.name)}
+
+
+ )}
+
+ {member.name}
+
+
+
+
+ {member.email || member.name}
+
+
+
+ >
+ )}
+
+
+
+
+ );
+ })}
)}
@@ -608,14 +702,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
}
}}
>
-
+ e.preventDefault()}
+ >
-
- {editingConfig ? (
-
- ) : (
-
- )}
+
{editingConfig ? "Edit Image Model" : "Add Image Model"}
diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx
index 3ee5458b8..238286a96 100644
--- a/surfsense_web/components/settings/model-config-manager.tsx
+++ b/surfsense_web/components/settings/model-config-manager.tsx
@@ -47,6 +47,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
+import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import type { NewLLMConfig } from "@/contracts/types/new-llm-config.types";
@@ -84,17 +85,14 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
const {
mutateAsync: createConfig,
isPending: isCreating,
- error: createError,
} = useAtomValue(createNewLLMConfigMutationAtom);
const {
mutateAsync: updateConfig,
isPending: isUpdating,
- error: updateError,
} = useAtomValue(updateNewLLMConfigMutationAtom);
const {
mutateAsync: deleteConfig,
isPending: isDeleting,
- error: deleteError,
} = useAtomValue(deleteNewLLMConfigMutationAtom);
// Queries
@@ -128,7 +126,6 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
const [configToDelete, setConfigToDelete] = useState(null);
const isSubmitting = isCreating || isUpdating;
- const errors = [createError, updateError, deleteError, fetchError].filter(Boolean) as Error[];
const handleFormSubmit = useCallback(
async (formData: LLMConfigFormData) => {
@@ -145,7 +142,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
setIsDialogOpen(false);
setEditingConfig(null);
} catch {
- // Error handled by mutation
+ // Error is displayed inside the dialog by the form
}
},
[editingConfig, createConfig, updateConfig]
@@ -157,7 +154,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
await deleteConfig({ id: configToDelete.id });
setConfigToDelete(null);
} catch {
- // Error handled by mutation
+ // Error handled by mutation state
}
};
@@ -195,29 +192,27 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
size="sm"
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
>
-
Add Configuration
- {/* Error Alerts */}
+ {/* Fetch Error Alert */}
- {errors.length > 0 &&
- errors.map((err) => (
-
-
-
-
- {err?.message ?? "Something went wrong"}
-
-
-
- ))}
+ {fetchError && (
+
+
+
+
+ {fetchError?.message ?? "Failed to load configurations"}
+
+
+
+ )}
{/* Global Configs Info */}
@@ -236,18 +231,39 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
)}
- {/* Loading State */}
+ {/* Loading Skeleton */}
{isLoading && (
-
-
-
-
-
- Loading configurations...
-
-
-
-
+
+ {["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
+
+
+ {/* Header */}
+
+ {/* Provider + Model */}
+
+
+
+
+ {/* Feature badges */}
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+
+
+ ))}
+
)}
{/* Configurations List */}
@@ -413,7 +429,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
)}
-
+
{member.name}
@@ -444,12 +460,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
onOpenAutoFocus={(e) => e.preventDefault()}
>
-
- {editingConfig ? (
-
- ) : (
-
- )}
+
{editingConfig ? "Edit Configuration" : "Create New Configuration"}
From 83033cebb9d4393a756ab2e266e3a8667a61014e Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Mon, 9 Feb 2026 19:37:59 +0530
Subject: [PATCH 13/35] feat: implement permission-based access controls for
image and model configuration managers
---
.../settings/image-model-manager.tsx | 140 +++++++++++-----
.../settings/model-config-manager.tsx | 153 ++++++++++++------
2 files changed, 204 insertions(+), 89 deletions(-)
diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx
index 445d87a2a..cba2295c6 100644
--- a/surfsense_web/components/settings/image-model-manager.tsx
+++ b/surfsense_web/components/settings/image-model-manager.tsx
@@ -19,7 +19,7 @@ import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
-import { membersAtom } from "@/atoms/members/members-query.atoms";
+import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import {
createImageGenConfigMutationAtom,
deleteImageGenConfigMutationAtom,
@@ -148,6 +148,22 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
return map;
}, [members]);
+ // Permissions
+ const { data: access } = useAtomValue(myAccessAtom);
+ const canCreate = useMemo(() => {
+ if (!access) return false;
+ if (access.is_owner) return true;
+ return access.permissions?.includes("image_generations:create") ?? false;
+ }, [access]);
+ const canDelete = useMemo(() => {
+ if (!access) return false;
+ if (access.is_owner) return true;
+ return access.permissions?.includes("image_generations:delete") ?? false;
+ }, [access]);
+ // Backend uses image_generations:create for update as well
+ const canUpdate = canCreate;
+ const isReadOnly = !canCreate && !canDelete;
+
// Local state
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingConfig, setEditingConfig] = useState(null);
@@ -344,6 +360,34 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
))}
+ {/* Read-only / Limited permissions notice */}
+ {access && !isLoading && isReadOnly && (
+
+
+
+
+ You have read-only access to image generation
+ configurations. Contact a space owner to request additional permissions.
+
+
+
+ )}
+ {access && !isLoading && !isReadOnly && (!canCreate || !canDelete) && (
+
+
+
+
+ You can{" "}
+ {[canCreate && "create and edit", canDelete && "delete"]
+ .filter(Boolean)
+ .join(" and ")}{" "}
+ image model configurations
+ {!canDelete && ", but cannot delete them"}.
+
+
+
+ )}
+
{/* Global info */}
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && (
@@ -530,12 +574,14 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
Your Image Models
-
- Add Image Model
-
+ {canCreate && (
+
+ Add Image Model
+
+ )}
{(userConfigs?.length ?? 0) === 0 ? (
@@ -546,12 +592,16 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
No Image Models Yet
- Add your own image generation model (DALL-E 3, GPT Image 1, etc.)
+ {canCreate
+ ? "Add your own image generation model (DALL-E 3, GPT Image 1, etc.)"
+ : "No image models have been added to this space yet. Contact a space owner to add one."}
-
-
- Add First Image Model
-
+ {canCreate && (
+
+
+ Add First Image Model
+
+ )}
) : (
@@ -586,38 +636,44 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)}
+ {(canUpdate || canDelete) && (
-
-
-
- openEditDialog(config)}
- className="h-7 w-7 text-muted-foreground hover:text-foreground"
- >
-
-
-
- Edit
-
-
-
-
-
- setConfigToDelete(config)}
- className="h-7 w-7 text-muted-foreground hover:text-destructive"
- >
-
-
-
- Delete
-
-
+ {canUpdate && (
+
+
+
+ openEditDialog(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-foreground"
+ >
+
+
+
+ Edit
+
+
+ )}
+ {canDelete && (
+
+
+
+ setConfigToDelete(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
+ >
+
+
+
+ Delete
+
+
+ )}
+ )}
{/* Provider + Model */}
diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx
index 238286a96..1666fae20 100644
--- a/surfsense_web/components/settings/model-config-manager.tsx
+++ b/surfsense_web/components/settings/model-config-manager.tsx
@@ -15,7 +15,7 @@ import {
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import { useCallback, useMemo, useState } from "react";
-import { membersAtom } from "@/atoms/members/members-query.atoms";
+import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import {
createNewLLMConfigMutationAtom,
deleteNewLLMConfigMutationAtom,
@@ -120,6 +120,25 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
return map;
}, [members]);
+ // Permissions
+ const { data: access } = useAtomValue(myAccessAtom);
+ const canCreate = useMemo(() => {
+ if (!access) return false;
+ if (access.is_owner) return true;
+ return access.permissions?.includes("llm_configs:create") ?? false;
+ }, [access]);
+ const canUpdate = useMemo(() => {
+ if (!access) return false;
+ if (access.is_owner) return true;
+ return access.permissions?.includes("llm_configs:update") ?? false;
+ }, [access]);
+ const canDelete = useMemo(() => {
+ if (!access) return false;
+ if (access.is_owner) return true;
+ return access.permissions?.includes("llm_configs:delete") ?? false;
+ }, [access]);
+ const isReadOnly = !canCreate && !canUpdate && !canDelete;
+
// Local state
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingConfig, setEditingConfig] = useState
(null);
@@ -187,13 +206,15 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
Refresh
-
- Add Configuration
-
+ {canCreate && (
+
+ Add Configuration
+
+ )}
{/* Fetch Error Alert */}
@@ -215,6 +236,34 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
)}
+ {/* Read-only / Limited permissions notice */}
+ {access && !isLoading && isReadOnly && (
+
+
+
+
+ You have read-only access to LLM configurations.
+ Contact a space owner to request additional permissions.
+
+
+
+ )}
+ {access && !isLoading && !isReadOnly && (!canCreate || !canUpdate || !canDelete) && (
+
+
+
+
+ You can{" "}
+ {[canCreate && "create", canUpdate && "edit", canDelete && "delete"]
+ .filter(Boolean)
+ .join(" and ")}{" "}
+ configurations
+ {!canDelete && ", but cannot delete them"}.
+
+
+
+ )}
+
{/* Global Configs Info */}
{globalConfigs.length > 0 && (
@@ -279,17 +328,21 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
No Configurations Yet
- Create your first AI configuration to customize how your agent responds
+ {canCreate
+ ? "Create your first AI configuration to customize how your agent responds"
+ : "No AI configurations have been added to this space yet. Contact a space owner to add one."}
-
-
- Create First Configuration
-
+ {canCreate && (
+
+
+ Create First Configuration
+
+ )}
@@ -325,38 +378,44 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
)}
+ {(canUpdate || canDelete) && (
-
-
-
- openEditDialog(config)}
- className="h-7 w-7 text-muted-foreground hover:text-foreground"
- >
-
-
-
- Edit
-
-
-
-
-
- setConfigToDelete(config)}
- className="h-7 w-7 text-muted-foreground hover:text-destructive"
- >
-
-
-
- Delete
-
-
+ {canUpdate && (
+
+
+
+ openEditDialog(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-foreground"
+ >
+
+
+
+ Edit
+
+
+ )}
+ {canDelete && (
+
+
+
+ setConfigToDelete(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
+ >
+
+
+
+ Delete
+
+
+ )}
+ )}
{/* Provider + Model */}
From 5ce6011c9b448116c6ee34cafce4fa8ed0749123 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Mon, 9 Feb 2026 19:38:07 +0530
Subject: [PATCH 14/35] refactor: streamline image model manager by removing
unused preference state and related logic
---
.../settings/image-model-manager.tsx | 179 +-----------------
1 file changed, 3 insertions(+), 176 deletions(-)
diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx
index cba2295c6..123cc1a8f 100644
--- a/surfsense_web/components/settings/image-model-manager.tsx
+++ b/surfsense_web/components/settings/image-model-manager.tsx
@@ -6,18 +6,16 @@ import {
Check,
ChevronsUpDown,
Edit3,
- ImageIcon,
Key,
Plus,
RefreshCw,
- Shuffle,
Info,
Trash2,
Wand2,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import {
@@ -44,7 +42,7 @@ import {
} from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Card, CardContent } from "@/components/ui/card";
import {
Command,
CommandEmpty,
@@ -130,7 +128,7 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
} = useAtomValue(imageGenConfigsAtom);
const { data: globalConfigs = [], isFetching: globalLoading } =
useAtomValue(globalImageGenConfigsAtom);
- const { data: preferences = {}, isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom);
+ const { isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom);
// Members for user resolution
const { data: members } = useAtomValue(membersAtom);
@@ -169,18 +167,6 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
const [editingConfig, setEditingConfig] = useState(null);
const [configToDelete, setConfigToDelete] = useState(null);
- // Preference state
- const [selectedPrefId, setSelectedPrefId] = useState(
- preferences.image_generation_config_id ?? ""
- );
- const [hasPrefChanges, setHasPrefChanges] = useState(false);
- const [isSavingPref, setIsSavingPref] = useState(false);
-
- useEffect(() => {
- setSelectedPrefId(preferences.image_generation_config_id ?? "");
- setHasPrefChanges(false);
- }, [preferences]);
-
const isSubmitting = isCreating || isUpdating;
const isLoading = configsLoading || globalLoading || prefsLoading;
const errors = [createError, updateError, deleteError, fetchError].filter(Boolean) as Error[];
@@ -290,39 +276,6 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
setIsDialogOpen(true);
};
- const handlePrefChange = (value: string) => {
- const newVal = value === "unassigned" ? "" : parseInt(value);
- setSelectedPrefId(newVal);
- setHasPrefChanges(newVal !== (preferences.image_generation_config_id ?? ""));
- };
-
- const handleSavePref = async () => {
- setIsSavingPref(true);
- try {
- await updatePreferences({
- search_space_id: searchSpaceId,
- data: {
- image_generation_config_id:
- typeof selectedPrefId === "string"
- ? selectedPrefId
- ? parseInt(selectedPrefId)
- : undefined
- : selectedPrefId,
- },
- });
- setHasPrefChanges(false);
- toast.success("Image generation model preference saved!");
- } catch {
- toast.error("Failed to save preference");
- } finally {
- setIsSavingPref(false);
- }
- };
-
- const allConfigs = [
- ...globalConfigs.map((c) => ({ ...c, _source: "global" as const })),
- ...(userConfigs ?? []).map((c) => ({ ...c, _source: "user" as const })),
- ];
const selectedProvider = IMAGE_GEN_PROVIDERS.find((p) => p.value === formData.provider);
const suggestedModels = getImageGenModelsByProvider(formData.provider);
@@ -402,135 +355,9 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)}
- {/* Active Preference Card */}
- {!isLoading && allConfigs.length > 0 && (
-
-
-
-
-
-
-
-
- Active Image Model
-
- Select which model to use for image generation
-
-
-
-
-
-
-
-
-
-
-
- Unassigned
-
- {globalConfigs.length > 0 && (
- <>
-
- Global
-
- {globalConfigs.map((c) => {
- const isAuto = "is_auto_mode" in c && c.is_auto_mode;
- return (
-
-
- {isAuto ? (
-
-
- AUTO
-
- ) : (
-
- {c.provider}
-
- )}
- {c.name}
-
-
- );
- })}
- >
- )}
- {(userConfigs?.length ?? 0) > 0 && (
- <>
-
- Your Models
-
- {userConfigs?.map((c) => (
-
-
-
- {c.provider}
-
- {c.name}
- ({c.model_name})
-
-
- ))}
- >
- )}
-
-
- {hasPrefChanges && (
-
-
- {isSavingPref ? "Saving..." : "Save"}
-
- {
- setSelectedPrefId(preferences.image_generation_config_id ?? "");
- setHasPrefChanges(false);
- }}
- className="text-xs h-8"
- >
- Reset
-
-
- )}
-
-
-
- )}
-
{/* Loading Skeleton */}
{isLoading && (
- {/* Active Preference Skeleton */}
-
-
-
-
-
-
-
-
-
{/* Your Image Models Section Skeleton */}
From 147530e1d25ef5fe253a214986d45779f052eaf7 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Mon, 9 Feb 2026 19:58:28 +0530
Subject: [PATCH 15/35] feat: enhance LLM role manager with improved loading
states, error handling, and UI updates
---
.../components/settings/llm-role-manager.tsx | 658 ++++++++++--------
1 file changed, 370 insertions(+), 288 deletions(-)
diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx
index d91150a4f..a0c13d116 100644
--- a/surfsense_web/components/settings/llm-role-manager.tsx
+++ b/surfsense_web/components/settings/llm-role-manager.tsx
@@ -5,13 +5,14 @@ import {
AlertCircle,
Bot,
CheckCircle,
+ CircleDashed,
FileText,
RefreshCw,
RotateCcw,
Save,
Shuffle,
} from "lucide-react";
-import { motion } from "motion/react";
+import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { updateLLMPreferencesMutationAtom } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms";
@@ -23,16 +24,18 @@ import {
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
+ SelectGroup,
SelectItem,
+ SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { Spinner } from "@/components/ui/spinner";
+import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
const ROLE_DESCRIPTIONS = {
@@ -40,17 +43,15 @@ const ROLE_DESCRIPTIONS = {
icon: Bot,
title: "Agent LLM",
description: "Primary LLM for chat interactions and agent operations",
- color: "bg-blue-100 text-blue-800 border-blue-200",
- examples: "Chat responses, agent tasks, real-time interactions",
- characteristics: ["Fast responses", "Conversational", "Agent operations"],
+ color: "text-blue-600 dark:text-blue-400",
+ bgColor: "bg-blue-500/10",
},
document_summary: {
icon: FileText,
title: "Document Summary LLM",
- description: "Handles document summarization",
- color: "bg-purple-100 text-purple-800 border-purple-200",
- examples: "Document analysis, podcasts, research synthesis",
- characteristics: ["Large context window", "Deep reasoning", "Summarization"],
+ description: "Handles document summarization and research synthesis",
+ color: "text-purple-600 dark:text-purple-400",
+ bgColor: "bg-purple-500/10",
},
};
@@ -59,7 +60,6 @@ interface LLMRoleManagerProps {
}
export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
- // Use new LLM config system
const {
data: newLLMConfigs = [],
isFetching: configsLoading,
@@ -70,7 +70,6 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
data: globalConfigs = [],
isFetching: globalConfigsLoading,
error: globalConfigsError,
- refetch: refreshGlobalConfigs,
} = useAtomValue(globalNewLLMConfigsAtom);
const {
data: preferences = {},
@@ -105,7 +104,6 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
setAssignments(newAssignments);
- // Check if there are changes compared to current preferences
const currentPrefs = {
agent_llm_id: preferences.agent_llm_id ?? "",
document_summary_llm_id: preferences.document_summary_llm_id ?? "",
@@ -165,325 +163,409 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
assignments.document_summary_llm_id !== null &&
assignments.document_summary_llm_id !== undefined;
- // Combine global and custom configs (new system)
+ // Combine global and custom configs
const allConfigs = [
...globalConfigs.map((config) => ({ ...config, is_global: true })),
...newLLMConfigs.filter((config) => config.id && config.id.toString().trim() !== ""),
];
- const availableConfigs = allConfigs;
-
const isLoading = configsLoading || preferencesLoading || globalConfigsLoading;
const hasError = configsError || preferencesError || globalConfigsError;
return (
-
- {/* Header */}
-
-
-
+ {/* Header actions */}
+
+ refreshConfigs()}
+ disabled={isLoading}
+ className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
+ >
+
+ Refresh
+
+ {isAssignmentComplete && !isLoading && !hasError && (
+ refreshConfigs()}
- disabled={isLoading}
- className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
+ className="text-xs gap-1.5 border-emerald-500/30 text-emerald-700 dark:text-emerald-300 bg-emerald-500/5"
>
-
- Refresh Configs
- Configs
-
-
+
+ All roles assigned
+
+ )}
{/* Error Alert */}
- {hasError && (
+
+ {hasError && (
+
+
+
+
+ {(configsError?.message ?? "Failed to load LLM configurations") ||
+ (preferencesError?.message ?? "Failed to load preferences") ||
+ (globalConfigsError?.message ??
+ "Failed to load global configurations")}
+
+
+
+ )}
+
+
+ {/* Loading Skeleton */}
+ {isLoading && (
+
+ {["skeleton-a", "skeleton-b"].map((key) => (
+
+
+ {/* Header: icon + title + status */}
+
+ {/* Label */}
+
+
+
+
+ {/* Summary block */}
+
+
+
+ ))}
+
+ )}
+
+ {/* No configs warning */}
+ {!isLoading && !hasError && allConfigs.length === 0 && (
- {(configsError?.message ?? "Failed to load LLM configurations") ||
- (preferencesError?.message ?? "Failed to load preferences") ||
- (globalConfigsError?.message ?? "Failed to load global configurations")}
+ No LLM configurations found. Please add at least one LLM provider in the
+ Agent Configs tab before assigning roles.
)}
- {/* Loading State */}
- {isLoading && (
-
-
-
-
-
- {configsLoading && preferencesLoading
- ? "Loading configurations and preferences..."
- : configsLoading
- ? "Loading configurations..."
- : "Loading preferences..."}
-
-
-
-
- )}
+ {/* Role Assignment Cards */}
+ {!isLoading && !hasError && allConfigs.length > 0 && (
+
+ {Object.entries(ROLE_DESCRIPTIONS).map(([key, role], index) => {
+ const IconComponent = role.icon;
+ const currentAssignment =
+ assignments[`${key}_llm_id` as keyof typeof assignments];
+ const assignedConfig = allConfigs.find(
+ (config) => config.id === currentAssignment
+ );
+ const isAssigned =
+ currentAssignment !== "" &&
+ currentAssignment !== null &&
+ currentAssignment !== undefined;
+ const isAutoMode =
+ assignedConfig &&
+ "is_auto_mode" in assignedConfig &&
+ assignedConfig.is_auto_mode;
- {/* Info Alert */}
- {!isLoading && !hasError && (
-
- {availableConfigs.length === 0 ? (
-
-
-
- No LLM configurations found. Please add at least one LLM provider in the Agent
- Configs tab before assigning roles.
-
-
- ) : !isAssignmentComplete ? (
-
-
-
- Complete all role assignments to enable full functionality. Each role serves
- different purposes in your workflow.
-
-
- ) : (
-
-
-
- All roles are assigned and ready to use! Your LLM configuration is complete.
-
-
- )}
+ return (
+
+
+
+ {/* Role Header */}
+
+
+
+
+
+
+
+ {role.title}
+
+
+ {role.description}
+
+
+
+ {isAssigned ? (
+
+ ) : (
+
+ )}
+
- {/* Role Assignment Cards */}
- {availableConfigs.length > 0 && (
-
- {Object.entries(ROLE_DESCRIPTIONS).map(([key, role]) => {
- const IconComponent = role.icon;
- const currentAssignment = assignments[`${key}_llm_id` as keyof typeof assignments];
- const assignedConfig = availableConfigs.find(
- (config) => config.id === currentAssignment
- );
+ {/* Selector */}
+
+
+ Configuration
+
+
+ handleRoleAssignment(`${key}_llm_id`, value)
+ }
+ >
+
+
+
+
+
+
+ Unassigned
+
+
- return (
-
- 0 && (
+
+
+ Global Configurations
+
+ {globalConfigs.map((config) => {
+ const isAuto =
+ "is_auto_mode" in config &&
+ config.is_auto_mode;
+ return (
+
+
+ {isAuto ? (
+
+
+ AUTO
+
+ ) : (
+
+ {config.provider}
+
+ )}
+
+ {config.name}
+
+ {!isAuto && (
+
+ (
+ {
+ config.model_name
+ }
+ )
+
+ )}
+ {isAuto && (
+
+ Recommended
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+ {/* Custom Configurations */}
+ {newLLMConfigs.length > 0 && (
+
+
+ Your Configurations
+
+ {newLLMConfigs
+ .filter(
+ (config) =>
+ config.id &&
+ config.id
+ .toString()
+ .trim() !== ""
+ )
+ .map((config) => (
+
+
+
+ {config.provider}
+
+
+ {config.name}
+
+
+ (
+ {
+ config.model_name
+ }
+ )
+
+
+
+ ))}
+
+ )}
+
+
+
+
+ {/* Assigned Config Summary */}
+ {assignedConfig && (
+
-
-
-
-
-
-
-
-
{role.title}
-
- {role.description}
-
+ {isAutoMode ? (
+
+
+
+
+ Auto Load Balanced
+
+
+ Routes across all available providers
+
- {currentAssignment && (
-
- )}
-
-
-
-
-
- Assign LLM Configuration:
-
-
handleRoleAssignment(`${key}_llm_id`, value)}
- >
-
-
-
-
-
- Unassigned
-
-
- {/* Global Configurations */}
- {globalConfigs.length > 0 && (
- <>
-
- Global Configurations
-
- {globalConfigs.map((config) => {
- const isAutoMode =
- "is_auto_mode" in config && config.is_auto_mode;
- return (
-
-
- {isAutoMode ? (
-
-
- AUTO
-
- ) : (
-
- {config.provider}
-
- )}
- {config.name}
- {!isAutoMode && (
-
- ({config.model_name})
-
- )}
- {isAutoMode ? (
-
- Recommended
-
- ) : (
-
- 🌐 Global
-
- )}
-
-
- );
- })}
- >
- )}
-
- {/* Custom Configurations */}
- {newLLMConfigs.length > 0 && (
- <>
-
- Your Configurations
-
- {newLLMConfigs
- .filter(
- (config) => config.id && config.id.toString().trim() !== ""
- )
- .map((config) => (
-
-
-
- {config.provider}
-
- {config.name}
-
- ({config.model_name})
-
-
-
- ))}
- >
- )}
-
-
-
-
- {assignedConfig && (
-
-
- {"is_auto_mode" in assignedConfig && assignedConfig.is_auto_mode ? (
-
- ) : (
-
- )}
-
Assigned:
- {"is_auto_mode" in assignedConfig && assignedConfig.is_auto_mode ? (
-
- AUTO
-
- ) : (
-
- {assignedConfig.provider}
-
- )}
-
{assignedConfig.name}
- {"is_auto_mode" in assignedConfig && assignedConfig.is_auto_mode ? (
+ ) : (
+
+
+
+
+
+ {assignedConfig.name}
+
+ {"is_global" in assignedConfig &&
+ assignedConfig.is_global && (
+
+ 🌐 Global
+
+ )}
+
+
- Recommended
+ {assignedConfig.provider}
- ) : (
- "is_global" in assignedConfig &&
- assignedConfig.is_global && (
-
- 🌐 Global
-
- )
+
+ {assignedConfig.model_name}
+
+
+ {assignedConfig.api_base && (
+
+ {assignedConfig.api_base}
+
)}
- {"is_auto_mode" in assignedConfig && assignedConfig.is_auto_mode ? (
-
- Automatically load balances across all available LLM providers
-
- ) : (
- <>
-
- Model: {assignedConfig.model_name}
-
- {assignedConfig.api_base && (
-
- Base: {assignedConfig.api_base}
-
- )}
- >
- )}
)}
-
-
-
- );
- })}
-
- )}
+
+ )}
+
+
+
+ );
+ })}
+
+ )}
- {/* Action Buttons */}
- {hasChanges && (
-
-
-
- {isSaving ? "Saving" : "Save Changes"}
-
+ {/* Save / Reset Bar */}
+
+ {hasChanges && (
+
+
+ You have unsaved changes
+
+
-
+
Reset
+
+
+ {isSaving ? "Saving…" : "Save Changes"}
+
- )}
-
- )}
+
+ )}
+
);
}
From 302ee5ad2f92805c6037f3c477770d40bc2a19a4 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Mon, 9 Feb 2026 20:48:42 +0530
Subject: [PATCH 16/35] feat: centralize provider icon logic by creating a
reusable getProviderIcon function and update components to utilize it
---
.../components/new-chat/model-selector.tsx | 37 +------
.../settings/image-model-manager.tsx | 8 +-
.../components/settings/llm-role-manager.tsx | 55 ++++------
.../settings/model-config-manager.tsx | 8 +-
surfsense_web/lib/provider-icons.tsx | 103 ++++++++++++++++++
5 files changed, 133 insertions(+), 78 deletions(-)
create mode 100644 surfsense_web/lib/provider-icons.tsx
diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx
index ec1143e04..3f5786a3a 100644
--- a/surfsense_web/components/new-chat/model-selector.tsx
+++ b/surfsense_web/components/new-chat/model-selector.tsx
@@ -5,15 +5,10 @@ import {
Bot,
Check,
ChevronDown,
- Cloud,
Edit3,
Globe,
Plus,
- Settings2,
- Shuffle,
- Sparkles,
User,
- Zap,
} from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
@@ -42,33 +37,7 @@ import type {
NewLLMConfigPublic,
} from "@/contracts/types/new-llm-config.types";
import { cn } from "@/lib/utils";
-
-// Provider icons mapping
-const getProviderIcon = (provider: string, isAutoMode?: boolean) => {
- const iconClass = "size-4";
-
- // Special icon for Auto mode
- if (isAutoMode || provider?.toUpperCase() === "AUTO") {
- return
;
- }
-
- switch (provider?.toUpperCase()) {
- case "OPENAI":
- return
;
- case "ANTHROPIC":
- return
;
- case "GOOGLE":
- return
;
- case "GROQ":
- return
;
- case "OLLAMA":
- return
;
- case "XAI":
- return
;
- default:
- return
;
- }
-};
+import { getProviderIcon } from "@/lib/provider-icons";
interface ModelSelectorProps {
onEdit: (config: NewLLMConfigPublic | GlobalNewLLMConfig, isGlobal: boolean) => void;
@@ -196,7 +165,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
>
) : currentConfig ? (
<>
- {getProviderIcon(currentConfig.provider, isCurrentAutoMode ?? false)}
+ {getProviderIcon(currentConfig.provider, { isAutoMode: isCurrentAutoMode ?? false })}
{currentConfig.name}
@@ -283,7 +252,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
- {getProviderIcon(config.provider, isAutoMode)}
+ {getProviderIcon(config.provider, { isAutoMode })}
diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx
index 123cc1a8f..0170fcf8c 100644
--- a/surfsense_web/components/settings/image-model-manager.tsx
+++ b/surfsense_web/components/settings/image-model-manager.tsx
@@ -78,6 +78,7 @@ import {
} from "@/contracts/enums/image-gen-providers";
import type { ImageGenerationConfig } from "@/contracts/types/new-llm-config.types";
import { cn } from "@/lib/utils";
+import { getProviderIcon } from "@/lib/provider-icons";
interface ImageModelManagerProps {
searchSpaceId: number;
@@ -505,12 +506,7 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{/* Provider + Model */}
-
- {config.provider}
-
+ {getProviderIcon(config.provider, { className: "size-3.5 shrink-0" })}
{config.model_name}
diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx
index a0c13d116..c634ffd49 100644
--- a/surfsense_web/components/settings/llm-role-manager.tsx
+++ b/surfsense_web/components/settings/llm-role-manager.tsx
@@ -37,6 +37,7 @@ import {
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
+import { getProviderIcon } from "@/lib/provider-icons";
const ROLE_DESCRIPTIONS = {
agent: {
@@ -342,11 +343,14 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
handleRoleAssignment(`${key}_llm_id`, value)
}
>
-
+
-
-
+
+
Unassigned
@@ -355,7 +359,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{/* Global Configurations */}
{globalConfigs.length > 0 && (
-
+
Global Configurations
{globalConfigs.map((config) => {
@@ -366,29 +370,25 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
-
+
{isAuto ? (
-
+
AUTO
) : (
-
- {config.provider}
-
+ getProviderIcon(config.provider, { className: "size-3 md:size-3.5 shrink-0" })
)}
-
+
{config.name}
{!isAuto && (
-
+
(
{
config.model_name
@@ -399,7 +399,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{isAuto && (
Recommended
@@ -414,7 +414,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{/* Custom Configurations */}
{newLLMConfigs.length > 0 && (
-
+
Your Configurations
{newLLMConfigs
@@ -429,18 +429,14 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
-
-
- {config.provider}
-
-
+
+ {getProviderIcon(config.provider, { className: "size-3 md:size-3.5 shrink-0" })}
+
{config.name}
-
+
(
{
config.model_name
@@ -501,12 +497,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
)}
-
- {assignedConfig.provider}
-
+ {getProviderIcon(assignedConfig.provider, { className: "size-3 shrink-0" })}
{assignedConfig.model_name}
diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx
index 1666fae20..6500c1c6e 100644
--- a/surfsense_web/components/settings/model-config-manager.tsx
+++ b/surfsense_web/components/settings/model-config-manager.tsx
@@ -52,6 +52,7 @@ import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import type { NewLLMConfig } from "@/contracts/types/new-llm-config.types";
import { cn } from "@/lib/utils";
+import { getProviderIcon } from "@/lib/provider-icons";
interface ModelConfigManagerProps {
searchSpaceId: number;
@@ -420,12 +421,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
{/* Provider + Model */}
-
- {config.provider}
-
+ {getProviderIcon(config.provider, { className: "size-3.5 shrink-0" })}
{config.model_name}
diff --git a/surfsense_web/lib/provider-icons.tsx b/surfsense_web/lib/provider-icons.tsx
new file mode 100644
index 000000000..62ed19f61
--- /dev/null
+++ b/surfsense_web/lib/provider-icons.tsx
@@ -0,0 +1,103 @@
+import {
+ Bot,
+ Cloud,
+ Cpu,
+ Database,
+ Globe,
+ Layers,
+ Server,
+ Settings2,
+ Shuffle,
+ Sparkles,
+ Wand2,
+ Zap,
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+
+/**
+ * Returns a Lucide icon element for the given LLM / image-gen provider.
+ * Accepts an optional `className` override for the icon size.
+ */
+export function getProviderIcon(
+ provider: string,
+ {
+ isAutoMode,
+ className = "size-4",
+ }: { isAutoMode?: boolean; className?: string } = {}
+) {
+ if (isAutoMode || provider?.toUpperCase() === "AUTO") {
+ return
;
+ }
+
+ switch (provider?.toUpperCase()) {
+ case "OPENAI":
+ return
;
+ case "ANTHROPIC":
+ return
;
+ case "GOOGLE":
+ return
;
+ case "AZURE_OPENAI":
+ return
;
+ case "GROQ":
+ return
;
+ case "OLLAMA":
+ return
;
+ case "XAI":
+ return
;
+ case "MISTRAL":
+ return
;
+ case "DEEPSEEK":
+ return
;
+ case "COHERE":
+ return
;
+ case "BEDROCK":
+ return
;
+ case "VERTEX_AI":
+ return
;
+ case "OPENROUTER":
+ return
;
+ case "TOGETHER_AI":
+ return
;
+ case "FIREWORKS_AI":
+ return
;
+ case "PERPLEXITY":
+ return
;
+ case "CEREBRAS":
+ return
;
+ case "RECRAFT":
+ return
;
+ case "REPLICATE":
+ return
;
+ case "ALIBABA_QWEN":
+ return
;
+ case "MOONSHOT":
+ return
;
+ case "ZHIPU":
+ return
;
+ case "ANYSCALE":
+ return
;
+ case "DEEPINFRA":
+ return
;
+ case "SAMBANOVA":
+ return
;
+ case "AI21":
+ return
;
+ case "CLOUDFLARE":
+ return
;
+ case "DATABRICKS":
+ return
;
+ case "COMETAPI":
+ return
;
+ case "HUGGINGFACE":
+ return
;
+ case "CUSTOM":
+ return
;
+ case "XINFERENCE":
+ return
;
+ case "NSCALE":
+ return
;
+ default:
+ return
;
+ }
+}
+
From efe87551320572d433e88f6f282fea196b508060 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 10 Feb 2026 02:15:13 +0530
Subject: [PATCH 17/35] feat: enhance public chat snapshot row with copy
functionality and URL display, improving user interaction
---
.../public-chat-snapshot-row.tsx | 89 +++++++++++++------
.../public-chat-snapshots-list.tsx | 2 +-
.../public-chat-snapshots-manager.tsx | 15 ++--
3 files changed, 69 insertions(+), 37 deletions(-)
diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx
index e1fdb5605..1e7de9f23 100644
--- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx
+++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx
@@ -1,7 +1,8 @@
"use client";
-import { Copy, ExternalLink, MessageSquare, Trash2 } from "lucide-react";
+import { Check, Copy, ExternalLink, MessageSquare, Trash2 } from "lucide-react";
import Image from "next/image";
+import { useCallback, useRef, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
@@ -33,6 +34,16 @@ export function PublicChatSnapshotRow({
isDeleting = false,
memberMap,
}: PublicChatSnapshotRowProps) {
+ const [copied, setCopied] = useState(false);
+ const copyTimeoutRef = useRef
>();
+
+ const handleCopyClick = useCallback(() => {
+ onCopy(snapshot);
+ setCopied(true);
+ clearTimeout(copyTimeoutRef.current);
+ copyTimeoutRef.current = setTimeout(() => setCopied(false), 2000);
+ }, [onCopy, snapshot]);
+
const formattedDate = new Date(snapshot.created_at).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
@@ -63,27 +74,18 @@ export function PublicChatSnapshotRow({
onCopy(snapshot)}
+ asChild
className="h-7 w-7 text-muted-foreground hover:text-foreground"
>
-
+
+
+
- Copy link
-
-
-
-
-
-
-
-
-
Open link
@@ -108,18 +110,47 @@ export function PublicChatSnapshotRow({
- {/* Message count badge */}
-
-
-
- {snapshot.message_count} messages
-
-
+ {/* Message count badge */}
+
+
+
+ {snapshot.message_count} messages
+
+
- {/* Footer: Date + Creator */}
+ {/* Public URL – selectable fallback for manual copy */}
+
+
+ {snapshot.public_url}
+
+
+
+
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+ {copied ? "Copied!" : "Copy link"}
+
+
+
+
+ {/* Footer: Date + Creator */}
{formattedDate}
diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-list.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-list.tsx
index 8daf9a07f..7a07c62ee 100644
--- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-list.tsx
+++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-list.tsx
@@ -26,7 +26,7 @@ export function PublicChatSnapshotsList({
}
return (
-
+
{snapshots.map((snapshot) => (
{
const publicUrl = `${window.location.origin}/public/${snapshot.share_token}`;
navigator.clipboard.writeText(publicUrl);
- toast.success("Link copied to clipboard");
}, []);
const handleDelete = useCallback(
@@ -90,7 +89,7 @@ export function PublicChatSnapshotsManager({
{/* Cards grid skeleton */}
-
+
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
@@ -98,11 +97,13 @@ export function PublicChatSnapshotsManager({
- {/* Message count badge */}
-
-
-
- {/* Footer: Date + Creator */}
+ {/* Message count badge */}
+
+
+
+ {/* URL skeleton */}
+
+ {/* Footer: Date + Creator */}
From d152605682bc87c91927b19e340de05e73de6a5c Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Mon, 9 Feb 2026 17:46:41 -0800
Subject: [PATCH 18/35] fix: update pages reward for incentive tasks
- Reduced the pages reward for GitHub and Reddit tasks from 100 to 30.
- Adjusted the pages reward for the Discord task from 100 to 40.
---
surfsense_backend/app/db.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py
index 3d9374342..d58145f23 100644
--- a/surfsense_backend/app/db.py
+++ b/surfsense_backend/app/db.py
@@ -273,19 +273,19 @@ INCENTIVE_TASKS_CONFIG = {
IncentiveTaskType.GITHUB_STAR: {
"title": "Star our GitHub repository",
"description": "Show your support by starring SurfSense on GitHub",
- "pages_reward": 100,
+ "pages_reward": 30,
"action_url": "https://github.com/MODSetter/SurfSense",
},
IncentiveTaskType.REDDIT_FOLLOW: {
"title": "Join our Subreddit",
"description": "Join the SurfSense community on Reddit",
- "pages_reward": 100,
+ "pages_reward": 30,
"action_url": "https://www.reddit.com/r/SurfSense/",
},
IncentiveTaskType.DISCORD_JOIN: {
"title": "Join our Discord",
"description": "Join the SurfSense community on Discord",
- "pages_reward": 100,
+ "pages_reward": 40,
"action_url": "https://discord.gg/ejRNvftDp9",
},
# Future tasks can be configured here:
From 7e00e5683dafda75bb36f1fa36498a06230c78b4 Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Mon, 9 Feb 2026 17:59:07 -0800
Subject: [PATCH 19/35] feat: bumped version to 0.0.13
---
surfsense_backend/pyproject.toml | 2 +-
surfsense_backend/uv.lock | 2 +-
surfsense_browser_extension/package.json | 2 +-
.../changelog/content/2026-02-09.mdx | 72 +++++++++++++++++++
surfsense_web/package.json | 2 +-
5 files changed, 76 insertions(+), 4 deletions(-)
create mode 100644 surfsense_web/changelog/content/2026-02-09.mdx
diff --git a/surfsense_backend/pyproject.toml b/surfsense_backend/pyproject.toml
index 8e9a57786..2fe421e48 100644
--- a/surfsense_backend/pyproject.toml
+++ b/surfsense_backend/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "surf-new-backend"
-version = "0.0.12"
+version = "0.0.13"
description = "SurfSense Backend"
requires-python = ">=3.12"
dependencies = [
diff --git a/surfsense_backend/uv.lock b/surfsense_backend/uv.lock
index 7d3b5e692..9dd7c1d2a 100644
--- a/surfsense_backend/uv.lock
+++ b/surfsense_backend/uv.lock
@@ -6794,7 +6794,7 @@ wheels = [
[[package]]
name = "surf-new-backend"
-version = "0.0.12"
+version = "0.0.13"
source = { editable = "." }
dependencies = [
{ name = "alembic" },
diff --git a/surfsense_browser_extension/package.json b/surfsense_browser_extension/package.json
index bf926d09f..c01ec1514 100644
--- a/surfsense_browser_extension/package.json
+++ b/surfsense_browser_extension/package.json
@@ -1,7 +1,7 @@
{
"name": "surfsense_browser_extension",
"displayName": "Surfsense Browser Extension",
- "version": "0.0.12",
+ "version": "0.0.13",
"description": "Extension to collect Browsing History for SurfSense.",
"author": "https://github.com/MODSetter",
"engines": {
diff --git a/surfsense_web/changelog/content/2026-02-09.mdx b/surfsense_web/changelog/content/2026-02-09.mdx
new file mode 100644
index 000000000..8e222a16b
--- /dev/null
+++ b/surfsense_web/changelog/content/2026-02-09.mdx
@@ -0,0 +1,72 @@
+---
+title: "SurfSense v0.0.13 - Public Sharing, Image Generation & Redesigned Documents"
+description: "SurfSense v0.0.13 introduces public chat sharing with permissions, image generation support, an auto load-balanced model mode, a redesigned Documents page, and numerous bug fixes across connectors and UI."
+date: "2026-02-09"
+tags: ["Public Sharing", "Image Generation", "Documents", "UI", "Bug Fixes"]
+version: "0.0.13"
+---
+
+## What's New in v0.0.13
+
+This update brings **public sharing, image generation**, a redesigned Documents page, and numerous bug fixes.
+
+### Features & Improvements
+
+#### Image Generation
+
+- **Image Generation**: Generate images directly in chat with custom model and provider configurations.
+
+#### Public Sharing
+
+- **Public Chat Links**: Share snapshots of chats via public links.
+- **Sharing Permissions**: Search Space owners control who can create and manage public links.
+- **Link Management Page**: View and revoke all public chat links from Search Space Settings.
+
+#### Auto (Load Balanced) Mode
+
+- **Auto Model Selection**: The default cloud model now automatically picks the lowest-load model for faster responses.
+
+#### Redesigned Documents Page
+
+- **Unified Page**: Merged Logs and Documents into one page with real-time statuses.
+- **Inline Connector Management**: Configure and monitor connectors alongside your documents.
+
+#### UI & UX Polish
+
+- **Inbox Refresh**: Cleaner inbox layout for easier scanning.
+- **Streamlined Login**: Consolidated loading screens into a single unified flow.
+- **Chat UI Tweaks**: Small refinements for a cleaner, more consistent chat experience.
+- **Prompt Suggestions**: Empty chat windows now show contextual suggestions.
+
+#### Documentation
+
+- **New Connector Docs**: Added docs for Luma, Circleback, Elasticsearch, Bookstack, and Obsidian connectors.
+
+
+
+ Bug Fixes
+
+
+ Fixed cloud scaling issues where document queue congestion occurred under high load
+ Documents now correctly attribute to the uploading user and de-index on disconnect or deletion
+ Fixed common backend errors in indexing and large file handling
+ Fixed Notion indexing failures caused by transcription blocks
+ Chat refresh button now correctly regenerates AI responses
+ Restored the previously disabled Role Editor
+ Fixed Mentions tab appearing empty when document notifications pushed mentions out of the pagination window
+ Bundled git in the Docker image to fix GitHub connector failures with gitingest
+ Fixed Google Calendar default date range errors and aligned backend defaults with the frontend
+
+
+
+
+ Technical Improvements
+
+
+ Rebuilt the GitHub connector on gitingest for more efficient, lower-cost repository fetching
+
+
+
+
+
+SurfSense is your AI-powered federated search solution, connecting all your knowledge sources in one place.
diff --git a/surfsense_web/package.json b/surfsense_web/package.json
index 52bd9a5a2..5f3631128 100644
--- a/surfsense_web/package.json
+++ b/surfsense_web/package.json
@@ -1,6 +1,6 @@
{
"name": "surfsense_web",
- "version": "0.0.12",
+ "version": "0.0.13",
"private": true,
"description": "SurfSense Frontend",
"scripts": {
From 63ca97a08e4bff2f6def3cfac2f657c718af0761 Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Mon, 9 Feb 2026 18:15:53 -0800
Subject: [PATCH 20/35] fix: update LinkedIn link in footer component
---
surfsense_web/components/homepage/footer-new.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/surfsense_web/components/homepage/footer-new.tsx b/surfsense_web/components/homepage/footer-new.tsx
index 56b29b16b..e52ab09b9 100644
--- a/surfsense_web/components/homepage/footer-new.tsx
+++ b/surfsense_web/components/homepage/footer-new.tsx
@@ -43,7 +43,7 @@ export function FooterNew() {
},
{
title: "LinkedIn",
- href: "https://www.linkedin.com/in/rohan-verma-sde/",
+ href: "https://www.linkedin.com/company/surfsense/",
icon: IconBrandLinkedin,
},
{
From 0caba8de506e85fc430a027ebea72e81f2990f42 Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Mon, 9 Feb 2026 18:20:57 -0800
Subject: [PATCH 21/35] chore: expand incentive task type enum to include new
tasks
---
surfsense_web/contracts/types/incentive-tasks.types.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/surfsense_web/contracts/types/incentive-tasks.types.ts b/surfsense_web/contracts/types/incentive-tasks.types.ts
index 56a62fa06..c45121c29 100644
--- a/surfsense_web/contracts/types/incentive-tasks.types.ts
+++ b/surfsense_web/contracts/types/incentive-tasks.types.ts
@@ -3,7 +3,7 @@ import { z } from "zod";
/**
* Incentive task type enum - matches backend IncentiveTaskType
*/
-export const incentiveTaskTypeEnum = z.enum(["GITHUB_STAR"]);
+export const incentiveTaskTypeEnum = z.enum(["GITHUB_STAR", "REDDIT_FOLLOW", "DISCORD_JOIN"]);
/**
* Single incentive task info schema
From 33b9c1fc4b597f7d5bac277111582914ad1813a1 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 10 Feb 2026 11:46:49 +0530
Subject: [PATCH 22/35] fix: use REDIS_APP_URL for auth rate limiting
---
surfsense_backend/app/app.py | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/surfsense_backend/app/app.py b/surfsense_backend/app/app.py
index 70269e723..cdba7c7b7 100644
--- a/surfsense_backend/app/app.py
+++ b/surfsense_backend/app/app.py
@@ -1,5 +1,4 @@
import logging
-import os
import time
from collections import defaultdict
from contextlib import asynccontextmanager
@@ -36,16 +35,12 @@ rate_limit_logger = logging.getLogger("surfsense.rate_limit")
# ============================================================================
# Uses the same Redis instance as Celery for zero additional infrastructure.
# Protects auth endpoints from brute force and user enumeration attacks.
-REDIS_URL = os.getenv(
- "REDIS_APP_URL",
- os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0"),
-)
# SlowAPI limiter — provides default rate limits (60/min) for ALL routes
# via the ASGI middleware. This is the general safety net.
limiter = Limiter(
key_func=get_remote_address,
- storage_uri=REDIS_URL,
+ storage_uri=config.REDIS_APP_URL,
default_limits=["60/minute"],
)
@@ -82,7 +77,7 @@ def _get_rate_limit_redis() -> redis.Redis:
"""Get or create Redis client for auth rate limiting."""
global _rate_limit_redis
if _rate_limit_redis is None:
- _rate_limit_redis = redis.from_url(REDIS_URL, decode_responses=True)
+ _rate_limit_redis = redis.from_url(config.REDIS_APP_URL, decode_responses=True)
return _rate_limit_redis
From 8dd2b157968e923cb4ea5143c0801fc367bcf017 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 10 Feb 2026 16:23:12 +0530
Subject: [PATCH 23/35] feat: integrate SVGR for SVG handling in webpack and
add new provider icons
---
.../components/icons/providers/ai21.svg | 1 +
.../components/icons/providers/anthropic.svg | 1 +
.../components/icons/providers/anyscale.svg | 1 +
.../components/icons/providers/bedrock.svg | 1 +
.../components/icons/providers/cerebras.svg | 1 +
.../components/icons/providers/cohere.svg | 1 +
.../components/icons/providers/cometapi.svg | 1 +
.../components/icons/providers/dbrx.svg | 1 +
.../components/icons/providers/deepinfra.svg | 1 +
.../components/icons/providers/deepseek.svg | 1 +
.../icons/providers/fireworksai.svg | 1 +
.../components/icons/providers/gemini.svg | 1 +
.../components/icons/providers/groq.svg | 1 +
.../icons/providers/huggingface.svg | 1 +
.../components/icons/providers/index.ts | 31 +
.../components/icons/providers/mistral.svg | 1 +
.../components/icons/providers/moonshot.svg | 1 +
.../components/icons/providers/nscale.svg | 5 +
.../components/icons/providers/ollama.svg | 1 +
.../components/icons/providers/openai.svg | 1 +
.../components/icons/providers/openrouter.svg | 1 +
.../components/icons/providers/perplexity.svg | 1 +
.../components/icons/providers/qwen.svg | 1 +
.../components/icons/providers/recraft.svg | 1 +
.../components/icons/providers/replicate.svg | 1 +
.../components/icons/providers/sambanova.svg | 1 +
.../components/icons/providers/togetherai.svg | 1 +
.../components/icons/providers/vertexai.svg | 1 +
.../icons/providers/workersai-cloudflare.svg | 1 +
.../components/icons/providers/xai.svg | 1 +
.../components/icons/providers/xinference.svg | 1 +
.../components/icons/providers/zhipu.svg | 1 +
.../components/new-chat/model-selector.tsx | 6 +-
surfsense_web/lib/provider-icons.tsx | 163 +-
surfsense_web/next.config.ts | 34 +-
surfsense_web/package.json | 1 +
surfsense_web/pnpm-lock.yaml | 1934 ++++++++++++++++-
surfsense_web/svgr.d.ts | 6 +
38 files changed, 2105 insertions(+), 105 deletions(-)
create mode 100644 surfsense_web/components/icons/providers/ai21.svg
create mode 100644 surfsense_web/components/icons/providers/anthropic.svg
create mode 100644 surfsense_web/components/icons/providers/anyscale.svg
create mode 100644 surfsense_web/components/icons/providers/bedrock.svg
create mode 100644 surfsense_web/components/icons/providers/cerebras.svg
create mode 100644 surfsense_web/components/icons/providers/cohere.svg
create mode 100644 surfsense_web/components/icons/providers/cometapi.svg
create mode 100644 surfsense_web/components/icons/providers/dbrx.svg
create mode 100644 surfsense_web/components/icons/providers/deepinfra.svg
create mode 100644 surfsense_web/components/icons/providers/deepseek.svg
create mode 100644 surfsense_web/components/icons/providers/fireworksai.svg
create mode 100644 surfsense_web/components/icons/providers/gemini.svg
create mode 100644 surfsense_web/components/icons/providers/groq.svg
create mode 100644 surfsense_web/components/icons/providers/huggingface.svg
create mode 100644 surfsense_web/components/icons/providers/index.ts
create mode 100644 surfsense_web/components/icons/providers/mistral.svg
create mode 100644 surfsense_web/components/icons/providers/moonshot.svg
create mode 100644 surfsense_web/components/icons/providers/nscale.svg
create mode 100644 surfsense_web/components/icons/providers/ollama.svg
create mode 100644 surfsense_web/components/icons/providers/openai.svg
create mode 100644 surfsense_web/components/icons/providers/openrouter.svg
create mode 100644 surfsense_web/components/icons/providers/perplexity.svg
create mode 100644 surfsense_web/components/icons/providers/qwen.svg
create mode 100644 surfsense_web/components/icons/providers/recraft.svg
create mode 100644 surfsense_web/components/icons/providers/replicate.svg
create mode 100644 surfsense_web/components/icons/providers/sambanova.svg
create mode 100644 surfsense_web/components/icons/providers/togetherai.svg
create mode 100644 surfsense_web/components/icons/providers/vertexai.svg
create mode 100644 surfsense_web/components/icons/providers/workersai-cloudflare.svg
create mode 100644 surfsense_web/components/icons/providers/xai.svg
create mode 100644 surfsense_web/components/icons/providers/xinference.svg
create mode 100644 surfsense_web/components/icons/providers/zhipu.svg
create mode 100644 surfsense_web/svgr.d.ts
diff --git a/surfsense_web/components/icons/providers/ai21.svg b/surfsense_web/components/icons/providers/ai21.svg
new file mode 100644
index 000000000..b5e8bf4d1
--- /dev/null
+++ b/surfsense_web/components/icons/providers/ai21.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/anthropic.svg b/surfsense_web/components/icons/providers/anthropic.svg
new file mode 100644
index 000000000..35be6f954
--- /dev/null
+++ b/surfsense_web/components/icons/providers/anthropic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/anyscale.svg b/surfsense_web/components/icons/providers/anyscale.svg
new file mode 100644
index 000000000..3a551131e
--- /dev/null
+++ b/surfsense_web/components/icons/providers/anyscale.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/bedrock.svg b/surfsense_web/components/icons/providers/bedrock.svg
new file mode 100644
index 000000000..195aa6594
--- /dev/null
+++ b/surfsense_web/components/icons/providers/bedrock.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/cerebras.svg b/surfsense_web/components/icons/providers/cerebras.svg
new file mode 100644
index 000000000..92a9d5a2e
--- /dev/null
+++ b/surfsense_web/components/icons/providers/cerebras.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/cohere.svg b/surfsense_web/components/icons/providers/cohere.svg
new file mode 100644
index 000000000..a3e3ecd6c
--- /dev/null
+++ b/surfsense_web/components/icons/providers/cohere.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/cometapi.svg b/surfsense_web/components/icons/providers/cometapi.svg
new file mode 100644
index 000000000..9e59c1f56
--- /dev/null
+++ b/surfsense_web/components/icons/providers/cometapi.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/dbrx.svg b/surfsense_web/components/icons/providers/dbrx.svg
new file mode 100644
index 000000000..80c6c7ef7
--- /dev/null
+++ b/surfsense_web/components/icons/providers/dbrx.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/deepinfra.svg b/surfsense_web/components/icons/providers/deepinfra.svg
new file mode 100644
index 000000000..c4391ca86
--- /dev/null
+++ b/surfsense_web/components/icons/providers/deepinfra.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/deepseek.svg b/surfsense_web/components/icons/providers/deepseek.svg
new file mode 100644
index 000000000..518097be8
--- /dev/null
+++ b/surfsense_web/components/icons/providers/deepseek.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/fireworksai.svg b/surfsense_web/components/icons/providers/fireworksai.svg
new file mode 100644
index 000000000..9555f015b
--- /dev/null
+++ b/surfsense_web/components/icons/providers/fireworksai.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/gemini.svg b/surfsense_web/components/icons/providers/gemini.svg
new file mode 100644
index 000000000..0f09f0613
--- /dev/null
+++ b/surfsense_web/components/icons/providers/gemini.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/groq.svg b/surfsense_web/components/icons/providers/groq.svg
new file mode 100644
index 000000000..bd5d4fe0a
--- /dev/null
+++ b/surfsense_web/components/icons/providers/groq.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/huggingface.svg b/surfsense_web/components/icons/providers/huggingface.svg
new file mode 100644
index 000000000..6982b8942
--- /dev/null
+++ b/surfsense_web/components/icons/providers/huggingface.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/index.ts b/surfsense_web/components/icons/providers/index.ts
new file mode 100644
index 000000000..73c03a6cc
--- /dev/null
+++ b/surfsense_web/components/icons/providers/index.ts
@@ -0,0 +1,31 @@
+export { default as Ai21Icon } from "./ai21.svg";
+export { default as AnthropicIcon } from "./anthropic.svg";
+export { default as AnyscaleIcon } from "./anyscale.svg";
+export { default as BedrockIcon } from "./bedrock.svg";
+export { default as CerebrasIcon } from "./cerebras.svg";
+export { default as CloudflareIcon } from "./workersai-cloudflare.svg";
+export { default as CohereIcon } from "./cohere.svg";
+export { default as CometApiIcon } from "./cometapi.svg";
+export { default as DatabricksIcon } from "./dbrx.svg";
+export { default as DeepInfraIcon } from "./deepinfra.svg";
+export { default as DeepSeekIcon } from "./deepseek.svg";
+export { default as FireworksAiIcon } from "./fireworksai.svg";
+export { default as GeminiIcon } from "./gemini.svg";
+export { default as GroqIcon } from "./groq.svg";
+export { default as HuggingFaceIcon } from "./huggingface.svg";
+export { default as MistralIcon } from "./mistral.svg";
+export { default as MoonshotIcon } from "./moonshot.svg";
+export { default as NscaleIcon } from "./nscale.svg";
+export { default as OllamaIcon } from "./ollama.svg";
+export { default as OpenaiIcon } from "./openai.svg";
+export { default as OpenRouterIcon } from "./openrouter.svg";
+export { default as PerplexityIcon } from "./perplexity.svg";
+export { default as QwenIcon } from "./qwen.svg";
+export { default as RecraftIcon } from "./recraft.svg";
+export { default as ReplicateIcon } from "./replicate.svg";
+export { default as SambaNovaIcon } from "./sambanova.svg";
+export { default as TogetherAiIcon } from "./togetherai.svg";
+export { default as VertexAiIcon } from "./vertexai.svg";
+export { default as XaiIcon } from "./xai.svg";
+export { default as XinferenceIcon } from "./xinference.svg";
+export { default as ZhipuIcon } from "./zhipu.svg";
diff --git a/surfsense_web/components/icons/providers/mistral.svg b/surfsense_web/components/icons/providers/mistral.svg
new file mode 100644
index 000000000..8719b0952
--- /dev/null
+++ b/surfsense_web/components/icons/providers/mistral.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/moonshot.svg b/surfsense_web/components/icons/providers/moonshot.svg
new file mode 100644
index 000000000..79c885069
--- /dev/null
+++ b/surfsense_web/components/icons/providers/moonshot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/nscale.svg b/surfsense_web/components/icons/providers/nscale.svg
new file mode 100644
index 000000000..cfac91523
--- /dev/null
+++ b/surfsense_web/components/icons/providers/nscale.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/surfsense_web/components/icons/providers/ollama.svg b/surfsense_web/components/icons/providers/ollama.svg
new file mode 100644
index 000000000..abd5a67db
--- /dev/null
+++ b/surfsense_web/components/icons/providers/ollama.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/openai.svg b/surfsense_web/components/icons/providers/openai.svg
new file mode 100644
index 000000000..6d10e2cef
--- /dev/null
+++ b/surfsense_web/components/icons/providers/openai.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/openrouter.svg b/surfsense_web/components/icons/providers/openrouter.svg
new file mode 100644
index 000000000..4a5d19753
--- /dev/null
+++ b/surfsense_web/components/icons/providers/openrouter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/perplexity.svg b/surfsense_web/components/icons/providers/perplexity.svg
new file mode 100644
index 000000000..8e38646da
--- /dev/null
+++ b/surfsense_web/components/icons/providers/perplexity.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/qwen.svg b/surfsense_web/components/icons/providers/qwen.svg
new file mode 100644
index 000000000..dd128325e
--- /dev/null
+++ b/surfsense_web/components/icons/providers/qwen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/recraft.svg b/surfsense_web/components/icons/providers/recraft.svg
new file mode 100644
index 000000000..6860900c1
--- /dev/null
+++ b/surfsense_web/components/icons/providers/recraft.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/replicate.svg b/surfsense_web/components/icons/providers/replicate.svg
new file mode 100644
index 000000000..80c67741a
--- /dev/null
+++ b/surfsense_web/components/icons/providers/replicate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/sambanova.svg b/surfsense_web/components/icons/providers/sambanova.svg
new file mode 100644
index 000000000..46b6270fc
--- /dev/null
+++ b/surfsense_web/components/icons/providers/sambanova.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/togetherai.svg b/surfsense_web/components/icons/providers/togetherai.svg
new file mode 100644
index 000000000..1c6441d69
--- /dev/null
+++ b/surfsense_web/components/icons/providers/togetherai.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/vertexai.svg b/surfsense_web/components/icons/providers/vertexai.svg
new file mode 100644
index 000000000..45adce83b
--- /dev/null
+++ b/surfsense_web/components/icons/providers/vertexai.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/workersai-cloudflare.svg b/surfsense_web/components/icons/providers/workersai-cloudflare.svg
new file mode 100644
index 000000000..4894b1918
--- /dev/null
+++ b/surfsense_web/components/icons/providers/workersai-cloudflare.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/xai.svg b/surfsense_web/components/icons/providers/xai.svg
new file mode 100644
index 000000000..dee9b47e3
--- /dev/null
+++ b/surfsense_web/components/icons/providers/xai.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/xinference.svg b/surfsense_web/components/icons/providers/xinference.svg
new file mode 100644
index 000000000..deec84ca1
--- /dev/null
+++ b/surfsense_web/components/icons/providers/xinference.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/icons/providers/zhipu.svg b/surfsense_web/components/icons/providers/zhipu.svg
new file mode 100644
index 000000000..3d89b0ed9
--- /dev/null
+++ b/surfsense_web/components/icons/providers/zhipu.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx
index 3f5786a3a..988dc7209 100644
--- a/surfsense_web/components/new-chat/model-selector.tsx
+++ b/surfsense_web/components/new-chat/model-selector.tsx
@@ -6,9 +6,7 @@ import {
Check,
ChevronDown,
Edit3,
- Globe,
Plus,
- User,
} from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
@@ -231,7 +229,6 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
{filteredGlobalConfigs.length > 0 && (
-
Global Models
{filteredGlobalConfigs.map((config) => {
@@ -300,14 +297,13 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
)}
{filteredGlobalConfigs.length > 0 && filteredUserConfigs.length > 0 && (
-
+
)}
{/* User Configs Section */}
{filteredUserConfigs.length > 0 && (
-
Your Configurations
{filteredUserConfigs.map((config) => {
diff --git a/surfsense_web/lib/provider-icons.tsx b/surfsense_web/lib/provider-icons.tsx
index 62ed19f61..ce474c5a9 100644
--- a/surfsense_web/lib/provider-icons.tsx
+++ b/surfsense_web/lib/provider-icons.tsx
@@ -1,18 +1,39 @@
import {
Bot,
- Cloud,
- Cpu,
- Database,
- Globe,
- Layers,
- Server,
- Settings2,
Shuffle,
- Sparkles,
- Wand2,
- Zap,
} from "lucide-react";
import { cn } from "@/lib/utils";
+import { Ai21Icon } from "@/components/icons/providers";
+import { AnthropicIcon } from "@/components/icons/providers";
+import { AnyscaleIcon } from "@/components/icons/providers";
+import { BedrockIcon } from "@/components/icons/providers";
+import { CerebrasIcon } from "@/components/icons/providers";
+import { CloudflareIcon } from "@/components/icons/providers";
+import { CohereIcon } from "@/components/icons/providers";
+import { CometApiIcon } from "@/components/icons/providers";
+import { DatabricksIcon } from "@/components/icons/providers";
+import { DeepInfraIcon } from "@/components/icons/providers";
+import { DeepSeekIcon } from "@/components/icons/providers";
+import { FireworksAiIcon } from "@/components/icons/providers";
+import { GeminiIcon } from "@/components/icons/providers";
+import { GroqIcon } from "@/components/icons/providers";
+import { HuggingFaceIcon } from "@/components/icons/providers";
+import { MistralIcon } from "@/components/icons/providers";
+import { MoonshotIcon } from "@/components/icons/providers";
+import { NscaleIcon } from "@/components/icons/providers";
+import { OllamaIcon } from "@/components/icons/providers";
+import { OpenaiIcon } from "@/components/icons/providers";
+import { OpenRouterIcon } from "@/components/icons/providers";
+import { PerplexityIcon } from "@/components/icons/providers";
+import { QwenIcon } from "@/components/icons/providers";
+import { RecraftIcon } from "@/components/icons/providers";
+import { ReplicateIcon } from "@/components/icons/providers";
+import { SambaNovaIcon } from "@/components/icons/providers";
+import { TogetherAiIcon } from "@/components/icons/providers";
+import { VertexAiIcon } from "@/components/icons/providers";
+import { XaiIcon } from "@/components/icons/providers";
+import { XinferenceIcon } from "@/components/icons/providers";
+import { ZhipuIcon } from "@/components/icons/providers";
/**
* Returns a Lucide icon element for the given LLM / image-gen provider.
@@ -30,72 +51,74 @@ export function getProviderIcon(
}
switch (provider?.toUpperCase()) {
- case "OPENAI":
- return ;
- case "ANTHROPIC":
- return ;
- case "GOOGLE":
- return ;
- case "AZURE_OPENAI":
- return ;
- case "GROQ":
- return ;
- case "OLLAMA":
- return ;
- case "XAI":
- return ;
- case "MISTRAL":
- return ;
- case "DEEPSEEK":
- return ;
- case "COHERE":
- return ;
- case "BEDROCK":
- return ;
- case "VERTEX_AI":
- return ;
- case "OPENROUTER":
- return ;
- case "TOGETHER_AI":
- return ;
- case "FIREWORKS_AI":
- return ;
- case "PERPLEXITY":
- return ;
- case "CEREBRAS":
- return ;
- case "RECRAFT":
- return ;
- case "REPLICATE":
- return ;
- case "ALIBABA_QWEN":
- return ;
- case "MOONSHOT":
- return ;
- case "ZHIPU":
- return ;
- case "ANYSCALE":
- return ;
- case "DEEPINFRA":
- return ;
- case "SAMBANOVA":
- return ;
case "AI21":
- return ;
+ return ;
+ case "ALIBABA_QWEN":
+ return ;
+ case "ANTHROPIC":
+ return ;
+ case "ANYSCALE":
+ return ;
+ case "AZURE":
+ case "AZURE_OPENAI":
+ return ;
+ case "AWS_BEDROCK":
+ case "BEDROCK":
+ return ;
+ case "CEREBRAS":
+ return ;
case "CLOUDFLARE":
- return ;
- case "DATABRICKS":
- return ;
+ return ;
+ case "COHERE":
+ return ;
case "COMETAPI":
- return ;
- case "HUGGINGFACE":
- return ;
+ return ;
case "CUSTOM":
return ;
- case "XINFERENCE":
- return ;
+ case "DATABRICKS":
+ return ;
+ case "DEEPINFRA":
+ return ;
+ case "DEEPSEEK":
+ return ;
+ case "FIREWORKS_AI":
+ return ;
+ case "GOOGLE":
+ return ;
+ case "GROQ":
+ return ;
+ case "HUGGINGFACE":
+ return ;
+ case "MISTRAL":
+ return ;
+ case "MOONSHOT":
+ return ;
case "NSCALE":
- return ;
+ return ;
+ case "OLLAMA":
+ return ;
+ case "OPENAI":
+ return ;
+ case "OPENROUTER":
+ return ;
+ case "PERPLEXITY":
+ return ;
+ case "RECRAFT":
+ return ;
+ case "REPLICATE":
+ return ;
+ case "SAMBANOVA":
+ return ;
+ case "TOGETHER_AI":
+ return ;
+ case "VERTEX_AI":
+ return ;
+ case "XAI":
+ return ;
+ case "XINFERENCE":
+ return ;
+ case "ZHIPU":
+ return ;
default:
return ;
}
diff --git a/surfsense_web/next.config.ts b/surfsense_web/next.config.ts
index f7491b4d3..55a9296fd 100644
--- a/surfsense_web/next.config.ts
+++ b/surfsense_web/next.config.ts
@@ -23,12 +23,44 @@ const nextConfig: NextConfig = {
// Mark BlockNote server packages as external
serverExternalPackages: ["@blocknote/server-util"],
- // Configure webpack to handle blocknote packages
+ // Turbopack config (used during `next dev --turbopack`)
+ turbopack: {
+ rules: {
+ "*.svg": {
+ loaders: ["@svgr/webpack"],
+ as: "*.js",
+ },
+ },
+ },
+
+ // Configure webpack to handle blocknote packages + SVGR
webpack: (config, { isServer }) => {
if (isServer) {
// Don't bundle these packages on the server
config.externals = [...(config.externals || []), "@blocknote/server-util"];
}
+
+ // SVGR: import *.svg as React components
+ const fileLoaderRule = config.module.rules.find(
+ (rule: any) => rule.test?.test?.(".svg"),
+ );
+ config.module.rules.push(
+ // Re-apply the existing file loader for *.svg?url imports
+ {
+ ...fileLoaderRule,
+ test: /\.svg$/i,
+ resourceQuery: /url/, // e.g. import icon from './icon.svg?url'
+ },
+ // Convert all other *.svg imports to React components
+ {
+ test: /\.svg$/i,
+ issuer: fileLoaderRule.issuer,
+ resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] },
+ use: ["@svgr/webpack"],
+ },
+ );
+ fileLoaderRule.exclude = /\.svg$/i;
+
return config;
},
diff --git a/surfsense_web/package.json b/surfsense_web/package.json
index 52bd9a5a2..ba5045001 100644
--- a/surfsense_web/package.json
+++ b/surfsense_web/package.json
@@ -112,6 +112,7 @@
"devDependencies": {
"@biomejs/biome": "2.1.2",
"@eslint/eslintrc": "^3.3.1",
+ "@svgr/webpack": "^8.1.0",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/typography": "^0.5.16",
"@types/canvas-confetti": "^1.9.0",
diff --git a/surfsense_web/pnpm-lock.yaml b/surfsense_web/pnpm-lock.yaml
index 40c80964b..d806fe5f9 100644
--- a/surfsense_web/pnpm-lock.yaml
+++ b/surfsense_web/pnpm-lock.yaml
@@ -166,22 +166,22 @@ importers:
version: 1.4.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
fumadocs-core:
specifier: ^16.3.1
- version: 16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1)
+ version: 16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1)
fumadocs-mdx:
specifier: ^14.2.1
- version: 14.2.1(fumadocs-core@16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
+ version: 14.2.1(fumadocs-core@16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
fumadocs-ui:
specifier: ^16.3.1
- version: 16.3.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1)
+ version: 16.3.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1)
geist:
specifier: ^1.4.2
- version: 1.5.1(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
+ version: 1.5.1(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
jotai:
specifier: ^2.15.1
- version: 2.16.0(@types/react@19.2.7)(react@19.2.3)
+ version: 2.16.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.7)(react@19.2.3)
jotai-tanstack-query:
specifier: ^0.11.0
- version: 0.11.0(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(jotai@2.16.0(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)
+ version: 0.11.0(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(jotai@2.16.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3)
lucide-react:
specifier: ^0.477.0
version: 0.477.0(react@19.2.3)
@@ -190,10 +190,10 @@ importers:
version: 12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next:
specifier: ^16.1.0
- version: 16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ version: 16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-intl:
specifier: ^4.6.1
- version: 4.6.1(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
+ version: 4.6.1(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -276,6 +276,9 @@ importers:
'@eslint/eslintrc':
specifier: ^3.3.1
version: 3.3.3
+ '@svgr/webpack':
+ specifier: ^8.1.0
+ version: 8.1.0(typescript@5.9.3)
'@tailwindcss/postcss':
specifier: ^4.1.11
version: 4.1.18
@@ -433,10 +436,561 @@ packages:
react:
optional: true
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.0':
+ resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.0':
+ resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.1':
+ resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-annotate-as-pure@7.27.3':
+ resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.28.6':
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-create-class-features-plugin@7.28.6':
+ resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-create-regexp-features-plugin@7.28.5':
+ resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-define-polyfill-provider@0.6.6':
+ resolution: {integrity: sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-member-expression-to-functions@7.28.5':
+ resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.28.6':
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.6':
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-optimise-call-expression@7.27.1':
+ resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-plugin-utils@7.28.6':
+ resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-remap-async-to-generator@7.27.1':
+ resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-replace-supers@7.28.6':
+ resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
+ resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-wrap-function@7.28.6':
+ resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.28.6':
+ resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.0':
+ resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5':
+ resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1':
+ resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1':
+ resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1':
+ resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.13.0
+
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6':
+ resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2':
+ resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-assertions@7.28.6':
+ resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-attributes@7.28.6':
+ resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-jsx@7.28.6':
+ resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-typescript@7.28.6':
+ resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-unicode-sets-regex@7.18.6':
+ resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-arrow-functions@7.27.1':
+ resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-async-generator-functions@7.29.0':
+ resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-async-to-generator@7.28.6':
+ resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-block-scoped-functions@7.27.1':
+ resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-block-scoping@7.28.6':
+ resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-class-properties@7.28.6':
+ resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-class-static-block@7.28.6':
+ resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.12.0
+
+ '@babel/plugin-transform-classes@7.28.6':
+ resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-computed-properties@7.28.6':
+ resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-destructuring@7.28.5':
+ resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-dotall-regex@7.28.6':
+ resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-duplicate-keys@7.27.1':
+ resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0':
+ resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-dynamic-import@7.27.1':
+ resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-explicit-resource-management@7.28.6':
+ resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-exponentiation-operator@7.28.6':
+ resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-export-namespace-from@7.27.1':
+ resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-for-of@7.27.1':
+ resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-function-name@7.27.1':
+ resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-json-strings@7.28.6':
+ resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-literals@7.27.1':
+ resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-logical-assignment-operators@7.28.6':
+ resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-member-expression-literals@7.27.1':
+ resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-amd@7.27.1':
+ resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-commonjs@7.28.6':
+ resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-systemjs@7.29.0':
+ resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-umd@7.27.1':
+ resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-named-capturing-groups-regex@7.29.0':
+ resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-new-target@7.27.1':
+ resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.28.6':
+ resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-numeric-separator@7.28.6':
+ resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-object-rest-spread@7.28.6':
+ resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-object-super@7.27.1':
+ resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-optional-catch-binding@7.28.6':
+ resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-optional-chaining@7.28.6':
+ resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-parameters@7.27.7':
+ resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-private-methods@7.28.6':
+ resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-private-property-in-object@7.28.6':
+ resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-property-literals@7.27.1':
+ resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-constant-elements@7.27.1':
+ resolution: {integrity: sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-display-name@7.28.0':
+ resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-development@7.27.1':
+ resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx@7.28.6':
+ resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-pure-annotations@7.27.1':
+ resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-regenerator@7.29.0':
+ resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-regexp-modifiers@7.28.6':
+ resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-reserved-words@7.27.1':
+ resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-shorthand-properties@7.27.1':
+ resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-spread@7.28.6':
+ resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-sticky-regex@7.27.1':
+ resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-template-literals@7.27.1':
+ resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-typeof-symbol@7.27.1':
+ resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-typescript@7.28.6':
+ resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-escapes@7.27.1':
+ resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-property-regex@7.28.6':
+ resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-regex@7.27.1':
+ resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-sets-regex@7.28.6':
+ resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/preset-env@7.29.0':
+ resolution: {integrity: sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-modules@0.1.6-no-external-plugins':
+ resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0
+
+ '@babel/preset-react@7.28.5':
+ resolution: {integrity: sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-typescript@7.28.5':
+ resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
+ '@babel/template@7.28.6':
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.0':
+ resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.0':
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
+ engines: {node: '>=6.9.0'}
+
'@biomejs/biome@2.1.2':
resolution: {integrity: sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg==}
engines: {node: '>=14.21.3'}
@@ -2670,6 +3224,84 @@ packages:
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+ '@svgr/babel-plugin-add-jsx-attribute@8.0.0':
+ resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-remove-jsx-attribute@8.0.0':
+ resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0':
+ resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0':
+ resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-svg-dynamic-title@8.0.0':
+ resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-svg-em-dimensions@8.0.0':
+ resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-transform-react-native-svg@8.1.0':
+ resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-transform-svg-component@8.0.0':
+ resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-preset@8.1.0':
+ resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/core@8.1.0':
+ resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==}
+ engines: {node: '>=14'}
+
+ '@svgr/hast-util-to-babel-ast@8.0.0':
+ resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==}
+ engines: {node: '>=14'}
+
+ '@svgr/plugin-jsx@8.1.0':
+ resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@svgr/core': '*'
+
+ '@svgr/plugin-svgo@8.1.0':
+ resolution: {integrity: sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@svgr/core': '*'
+
+ '@svgr/webpack@8.1.0':
+ resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==}
+ engines: {node: '>=14'}
+
'@swc/core-darwin-arm64@1.15.7':
resolution: {integrity: sha512-+hNVUfezUid7LeSHqnhoC6Gh3BROABxjlDNInuZ/fie1RUxaEX4qzDwdTgozJELgHhvYxyPIg1ro8ibnKtgO4g==}
engines: {node: '>=10'}
@@ -2975,6 +3607,10 @@ packages:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
+ '@trysound/sax@0.2.0':
+ resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
+ engines: {node: '>=10.13.0'}
+
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -3431,6 +4067,21 @@ packages:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
+ babel-plugin-polyfill-corejs2@0.4.15:
+ resolution: {integrity: sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-polyfill-corejs3@0.14.0:
+ resolution: {integrity: sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-polyfill-regenerator@0.6.6:
+ resolution: {integrity: sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
@@ -3453,6 +4104,9 @@ packages:
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+ boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@@ -3463,6 +4117,11 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
+ browserslist@4.28.1:
+ resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -3485,6 +4144,10 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
+ camelcase@6.3.0:
+ resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
+ engines: {node: '>=10'}
+
caniuse-lite@1.0.30001761:
resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==}
@@ -3600,6 +4263,12 @@ packages:
confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ core-js-compat@3.48.0:
+ resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==}
+
core-js@3.47.0:
resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==}
@@ -3609,6 +4278,15 @@ packages:
cose-base@2.2.0:
resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==}
+ cosmiconfig@8.3.6:
+ resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
@@ -3621,11 +4299,30 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
+ css-select@5.2.2:
+ resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
+
+ css-tree@2.2.1:
+ resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+
+ css-tree@2.3.1:
+ resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ css-what@6.2.2:
+ resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
+ engines: {node: '>= 6'}
+
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
+ csso@5.0.5:
+ resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+
cssstyle@4.6.0:
resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
engines: {node: '>=18'}
@@ -3851,6 +4548,10 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+
define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
@@ -3892,9 +4593,25 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
+ dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
dompurify@3.3.1:
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
+ dot-case@3.0.4:
+ resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
+
dotenv@17.2.3:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'}
@@ -3999,6 +4716,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
+ electron-to-chromium@1.5.286:
+ resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==}
+
emblor@1.4.8:
resolution: {integrity: sha512-Vqtz4Gepa7CIkmplQ+kvJnsSZJ4sAyHvQqqX2iCmgoRo5iRQFxr+5FJkk6QuLVNH5vrbBZEYxg7sMZuDCnQ/PQ==}
peerDependencies:
@@ -4026,6 +4746,9 @@ packages:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
+ error-ex@1.3.4:
+ resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+
es-abstract@1.24.1:
resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==}
engines: {node: '>= 0.4'}
@@ -4084,6 +4807,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -4433,6 +5160,10 @@ packages:
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
engines: {node: '>= 0.4'}
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
get-east-asian-width@1.4.0:
resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
engines: {node: '>=18'}
@@ -4692,6 +5423,9 @@ packages:
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
engines: {node: '>= 0.4'}
+ is-arrayish@0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+
is-async-function@2.1.1:
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
engines: {node: '>= 0.4'}
@@ -4874,9 +5608,17 @@ packages:
canvas:
optional: true
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+ json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -4890,6 +5632,11 @@ packages:
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
hasBin: true
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
jsondiffpatch@0.6.0:
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -5005,6 +5752,9 @@ packages:
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
engines: {node: '>= 12.0.0'}
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
@@ -5021,6 +5771,9 @@ packages:
lodash-es@4.17.22:
resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==}
+ lodash.debounce@4.0.8:
+ resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
+
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -5034,12 +5787,18 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
+ lower-case@2.0.2:
+ resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
+
lowlight@1.20.0:
resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
lucide-react@0.477.0:
resolution: {integrity: sha512-yCf7aYxerFZAbd8jHJxjwe1j7jEMPptjnaOqdYeirFnEy85cNR3/L+o0I875CYFYya+eEVzZSbNuRk8BZPDpVw==}
peerDependencies:
@@ -5124,6 +5883,12 @@ packages:
mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+ mdn-data@2.0.28:
+ resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
+
+ mdn-data@2.0.30:
+ resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
+
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
@@ -5391,6 +6156,9 @@ packages:
sass:
optional: true
+ no-case@3.0.4:
+ resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
+
node-abi@3.85.0:
resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==}
engines: {node: '>=10'}
@@ -5398,10 +6166,16 @@ packages:
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+ node-releases@2.0.27:
+ resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
+
npm-to-yarn@3.0.1:
resolution: {integrity: sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
number-flow@0.5.8:
resolution: {integrity: sha512-FPr1DumWyGi5Nucoug14bC6xEz70A1TnhgSHhKyfqjgji2SOTz+iLJxKtv37N5JyJbteGYCm6NQ9p1O4KZ7iiA==}
@@ -5481,6 +6255,10 @@ packages:
parse-entities@4.0.2:
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+ parse-json@5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
@@ -5501,6 +6279,10 @@ packages:
path-to-regexp@8.3.0:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
+ path-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -5916,6 +6698,13 @@ packages:
refractor@3.6.0:
resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==}
+ regenerate-unicode-properties@10.2.2:
+ resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==}
+ engines: {node: '>=4'}
+
+ regenerate@1.4.2:
+ resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
+
regex-recursion@6.0.2:
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
@@ -5929,6 +6718,17 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
+ regexpu-core@6.4.0:
+ resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==}
+ engines: {node: '>=4'}
+
+ regjsgen@0.8.0:
+ resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==}
+
+ regjsparser@0.13.0:
+ resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==}
+ hasBin: true
+
rehype-format@5.0.1:
resolution: {integrity: sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==}
@@ -6144,6 +6944,9 @@ packages:
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
+ snake-case@3.0.4:
+ resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
+
sonner@2.0.7:
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
peerDependencies:
@@ -6258,6 +7061,14 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ svg-parser@2.0.4:
+ resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}
+
+ svgo@3.3.2:
+ resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
swr@2.3.8:
resolution: {integrity: sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==}
peerDependencies:
@@ -6397,6 +7208,22 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+ unicode-canonical-property-names-ecmascript@2.0.1:
+ resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
+ engines: {node: '>=4'}
+
+ unicode-match-property-ecmascript@2.0.0:
+ resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
+ engines: {node: '>=4'}
+
+ unicode-match-property-value-ecmascript@2.2.1:
+ resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==}
+ engines: {node: '>=4'}
+
+ unicode-property-aliases-ecmascript@2.2.0:
+ resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
+ engines: {node: '>=4'}
+
unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
@@ -6427,6 +7254,12 @@ packages:
unrs-resolver@1.11.1:
resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -6669,6 +7502,9 @@ packages:
peerDependencies:
yjs: ^13.0.0
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
yjs@13.6.28:
resolution: {integrity: sha512-EgnDOXs8+hBVm6mq3/S89Kiwzh5JRbn7w2wXwbrMRyKy/8dOFsLvuIfC+x19ZdtaDc0tA9rQmdZzbqqNHG44wA==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
@@ -6836,8 +7672,738 @@ snapshots:
'@types/react': 19.2.7
react: 19.2.3
+ '@babel/code-frame@7.29.0':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.0': {}
+
+ '@babel/core@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helpers': 7.28.6
+ '@babel/parser': 7.29.0
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.1':
+ dependencies:
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-annotate-as-pure@7.27.3':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/helper-compilation-targets@7.28.6':
+ dependencies:
+ '@babel/compat-data': 7.29.0
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.28.1
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-member-expression-to-functions': 7.28.5
+ '@babel/helper-optimise-call-expression': 7.27.1
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/traverse': 7.29.0
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ regexpu-core: 6.4.0
+ semver: 6.3.1
+
+ '@babel/helper-define-polyfill-provider@0.6.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ debug: 4.4.3
+ lodash.debounce: 4.0.8
+ resolve: 1.22.11
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-member-expression-to-functions@7.28.5':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-imports@7.28.6':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-optimise-call-expression@7.27.1':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/helper-plugin-utils@7.28.6': {}
+
+ '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-wrap-function': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-member-expression-to-functions': 7.28.5
+ '@babel/helper-optimise-call-expression': 7.27.1
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helper-wrap-function@7.28.6':
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helpers@7.28.6':
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+
+ '@babel/parser@7.29.0':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+
+ '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0)
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-globals': 7.28.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0)
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/template': 7.28.6
+
+ '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-modules-systemjs@7.29.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0)
+ '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0)
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-react-constant-elements@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/preset-env@7.29.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/compat-data': 7.29.0
+ '@babel/core': 7.29.0
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-validator-option': 7.27.1
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)
+ '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0)
+ '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0)
+ '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0)
+ '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0)
+ babel-plugin-polyfill-corejs2: 0.4.15(@babel/core@7.29.0)
+ babel-plugin-polyfill-corejs3: 0.14.0(@babel/core@7.29.0)
+ babel-plugin-polyfill-regenerator: 0.6.6(@babel/core@7.29.0)
+ core-js-compat: 3.48.0
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/types': 7.29.0
+ esutils: 2.0.3
+
+ '@babel/preset-react@7.28.5(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-validator-option': 7.27.1
+ '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-validator-option': 7.27.1
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/runtime@7.28.4': {}
+ '@babel/template@7.28.6':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+
+ '@babel/traverse@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.29.0
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.0':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
'@biomejs/biome@2.1.2':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.1.2
@@ -7434,9 +9000,9 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@fumadocs/ui@16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1)':
+ '@fumadocs/ui@16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1)':
dependencies:
- fumadocs-core: 16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1)
+ fumadocs-core: 16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1)
lodash.merge: 4.6.2
next-themes: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
postcss-selector-parser: 7.1.1
@@ -7445,7 +9011,7 @@ snapshots:
tailwind-merge: 3.4.0
optionalDependencies:
'@types/react': 19.2.7
- next: 16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ next: 16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
tailwindcss: 4.1.18
transitivePeerDependencies:
- '@mixedbread/sdk'
@@ -8927,6 +10493,99 @@ snapshots:
'@standard-schema/utils@0.3.0': {}
+ '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+
+ '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+
+ '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+
+ '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+
+ '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+
+ '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+
+ '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+
+ '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+
+ '@svgr/babel-preset@8.1.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.0)
+ '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.0)
+ '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.0)
+ '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.0)
+ '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.0)
+ '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.0)
+ '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.0)
+ '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.0)
+
+ '@svgr/core@8.1.0(typescript@5.9.3)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0)
+ camelcase: 6.3.0
+ cosmiconfig: 8.3.6(typescript@5.9.3)
+ snake-case: 3.0.4
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@svgr/hast-util-to-babel-ast@8.0.0':
+ dependencies:
+ '@babel/types': 7.29.0
+ entities: 4.5.0
+
+ '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0)
+ '@svgr/core': 8.1.0(typescript@5.9.3)
+ '@svgr/hast-util-to-babel-ast': 8.0.0
+ svg-parser: 2.0.4
+ transitivePeerDependencies:
+ - supports-color
+
+ '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3)':
+ dependencies:
+ '@svgr/core': 8.1.0(typescript@5.9.3)
+ cosmiconfig: 8.3.6(typescript@5.9.3)
+ deepmerge: 4.3.1
+ svgo: 3.3.2
+ transitivePeerDependencies:
+ - typescript
+
+ '@svgr/webpack@8.1.0(typescript@5.9.3)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/plugin-transform-react-constant-elements': 7.27.1(@babel/core@7.29.0)
+ '@babel/preset-env': 7.29.0(@babel/core@7.29.0)
+ '@babel/preset-react': 7.28.5(@babel/core@7.29.0)
+ '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0)
+ '@svgr/core': 8.1.0(typescript@5.9.3)
+ '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))
+ '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3)
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
'@swc/core-darwin-arm64@1.15.7':
optional: true
@@ -9200,6 +10859,8 @@ snapshots:
transitivePeerDependencies:
- '@floating-ui/dom'
+ '@trysound/sax@0.2.0': {}
+
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
@@ -9698,6 +11359,30 @@ snapshots:
axobject-query@4.1.0: {}
+ babel-plugin-polyfill-corejs2@0.4.15(@babel/core@7.29.0):
+ dependencies:
+ '@babel/compat-data': 7.29.0
+ '@babel/core': 7.29.0
+ '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0)
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-polyfill-corejs3@0.14.0(@babel/core@7.29.0):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0)
+ core-js-compat: 3.48.0
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-polyfill-regenerator@0.6.6(@babel/core@7.29.0):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
bail@2.0.2: {}
balanced-match@1.0.2: {}
@@ -9725,6 +11410,8 @@ snapshots:
readable-stream: 3.6.2
optional: true
+ boolbase@1.0.0: {}
+
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
@@ -9738,6 +11425,14 @@ snapshots:
dependencies:
fill-range: 7.1.1
+ browserslist@4.28.1:
+ dependencies:
+ baseline-browser-mapping: 2.9.11
+ caniuse-lite: 1.0.30001761
+ electron-to-chromium: 1.5.286
+ node-releases: 2.0.27
+ update-browserslist-db: 1.2.3(browserslist@4.28.1)
+
buffer-from@1.1.2: {}
buffer@5.7.1:
@@ -9765,6 +11460,8 @@ snapshots:
callsites@3.1.0: {}
+ camelcase@6.3.0: {}
+
caniuse-lite@1.0.30001761: {}
canvas-confetti@1.9.4: {}
@@ -9869,6 +11566,12 @@ snapshots:
confbox@0.1.8: {}
+ convert-source-map@2.0.0: {}
+
+ core-js-compat@3.48.0:
+ dependencies:
+ browserslist: 4.28.1
+
core-js@3.47.0: {}
cose-base@1.0.3:
@@ -9879,6 +11582,15 @@ snapshots:
dependencies:
layout-base: 2.0.1
+ cosmiconfig@8.3.6(typescript@5.9.3):
+ dependencies:
+ import-fresh: 3.3.1
+ js-yaml: 4.1.1
+ parse-json: 5.2.0
+ path-type: 4.0.0
+ optionalDependencies:
+ typescript: 5.9.3
+
crelt@1.0.6: {}
cross-env@7.0.3:
@@ -9891,8 +11603,32 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
+ css-select@5.2.2:
+ dependencies:
+ boolbase: 1.0.0
+ css-what: 6.2.2
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ nth-check: 2.1.1
+
+ css-tree@2.2.1:
+ dependencies:
+ mdn-data: 2.0.28
+ source-map-js: 1.2.1
+
+ css-tree@2.3.1:
+ dependencies:
+ mdn-data: 2.0.30
+ source-map-js: 1.2.1
+
+ css-what@6.2.2: {}
+
cssesc@3.0.0: {}
+ csso@5.0.5:
+ dependencies:
+ css-tree: 2.2.1
+
cssstyle@4.6.0:
dependencies:
'@asamuzakjp/css-color': 3.2.0
@@ -10139,6 +11875,8 @@ snapshots:
deep-is@0.1.4: {}
+ deepmerge@4.3.1: {}
+
define-data-property@1.1.4:
dependencies:
es-define-property: 1.0.1
@@ -10175,10 +11913,33 @@ snapshots:
dependencies:
esutils: 2.0.3
+ dom-serializer@2.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ entities: 4.5.0
+
+ domelementtype@2.3.0: {}
+
+ domhandler@5.0.3:
+ dependencies:
+ domelementtype: 2.3.0
+
dompurify@3.3.1:
optionalDependencies:
'@types/trusted-types': 2.0.7
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
+ dot-case@3.0.4:
+ dependencies:
+ no-case: 3.0.4
+ tslib: 2.8.1
+
dotenv@17.2.3: {}
drizzle-kit@0.31.8:
@@ -10206,6 +11967,8 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
+ electron-to-chromium@1.5.286: {}
+
emblor@1.4.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@radix-ui/react-dialog': 1.0.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -10240,6 +12003,10 @@ snapshots:
entities@6.0.1: {}
+ error-ex@1.3.4:
+ dependencies:
+ is-arrayish: 0.2.1
+
es-abstract@1.24.1:
dependencies:
array-buffer-byte-length: 1.0.2
@@ -10445,6 +12212,8 @@ snapshots:
'@esbuild/win32-ia32': 0.27.2
'@esbuild/win32-x64': 0.27.2
+ escalade@3.2.0: {}
+
escape-string-regexp@4.0.0: {}
escape-string-regexp@5.0.0: {}
@@ -10777,7 +12546,7 @@ snapshots:
fsevents@2.3.3:
optional: true
- fumadocs-core@16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1):
+ fumadocs-core@16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1):
dependencies:
'@formatjs/intl-localematcher': 0.7.2
'@orama/orama': 3.1.18
@@ -10800,21 +12569,21 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
lucide-react: 0.477.0(react@19.2.3)
- next: 16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ next: 16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
zod: 4.2.1
transitivePeerDependencies:
- supports-color
- fumadocs-mdx@14.2.1(fumadocs-core@16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)):
+ fumadocs-mdx@14.2.1(fumadocs-core@16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)):
dependencies:
'@mdx-js/mdx': 3.1.1
'@standard-schema/spec': 1.1.0
chokidar: 5.0.0
esbuild: 0.27.2
estree-util-value-to-estree: 3.5.0
- fumadocs-core: 16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1)
+ fumadocs-core: 16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1)
js-yaml: 4.1.1
mdast-util-to-markdown: 2.1.2
picocolors: 1.1.1
@@ -10828,15 +12597,15 @@ snapshots:
vfile: 6.0.3
zod: 4.2.1
optionalDependencies:
- next: 16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ next: 16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
vite: 7.3.1(@types/node@20.19.27)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)
transitivePeerDependencies:
- supports-color
- fumadocs-ui@16.3.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1):
+ fumadocs-ui@16.3.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1):
dependencies:
- '@fumadocs/ui': 16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1)
+ '@fumadocs/ui': 16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.2.1)
'@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -10848,7 +12617,7 @@ snapshots:
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.3)
'@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
class-variance-authority: 0.7.1
- fumadocs-core: 16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1)
+ fumadocs-core: 16.3.1(@types/react@19.2.7)(lucide-react@0.477.0(react@19.2.3))(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1)
next-themes: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -10883,12 +12652,14 @@ snapshots:
functions-have-names@1.2.3: {}
- geist@1.5.1(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)):
+ geist@1.5.1(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)):
dependencies:
- next: 16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ next: 16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
generator-function@2.0.1: {}
+ gensync@1.0.0-beta.2: {}
+
get-east-asian-width@1.4.0: {}
get-intrinsic@1.3.0:
@@ -11284,6 +13055,8 @@ snapshots:
call-bound: 1.0.4
get-intrinsic: 1.3.0
+ is-arrayish@0.2.1: {}
+
is-async-function@2.1.1:
dependencies:
async-function: 1.0.0
@@ -11419,16 +13192,18 @@ snapshots:
jiti@2.6.1: {}
- jotai-tanstack-query@0.11.0(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(jotai@2.16.0(@types/react@19.2.7)(react@19.2.3))(react@19.2.3):
+ jotai-tanstack-query@0.11.0(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(jotai@2.16.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.7)(react@19.2.3))(react@19.2.3):
dependencies:
'@tanstack/query-core': 5.90.12
- jotai: 2.16.0(@types/react@19.2.7)(react@19.2.3)
+ jotai: 2.16.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.7)(react@19.2.3)
optionalDependencies:
'@tanstack/react-query': 5.90.12(react@19.2.3)
react: 19.2.3
- jotai@2.16.0(@types/react@19.2.7)(react@19.2.3):
+ jotai@2.16.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.7)(react@19.2.3):
optionalDependencies:
+ '@babel/core': 7.29.0
+ '@babel/template': 7.28.6
'@types/react': 19.2.7
react: 19.2.3
@@ -11466,8 +13241,12 @@ snapshots:
- supports-color
- utf-8-validate
+ jsesc@3.1.0: {}
+
json-buffer@3.0.1: {}
+ json-parse-even-better-errors@2.3.1: {}
+
json-schema-traverse@0.4.1: {}
json-schema@0.4.0: {}
@@ -11478,6 +13257,8 @@ snapshots:
dependencies:
minimist: 1.2.8
+ json5@2.2.3: {}
+
jsondiffpatch@0.6.0:
dependencies:
'@types/diff-match-patch': 1.0.36
@@ -11577,6 +13358,8 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.30.2
lightningcss-win32-x64-msvc: 1.30.2
+ lines-and-columns@1.2.4: {}
+
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
@@ -11591,6 +13374,8 @@ snapshots:
lodash-es@4.17.22: {}
+ lodash.debounce@4.0.8: {}
+
lodash.merge@4.6.2: {}
long@5.3.2: {}
@@ -11601,6 +13386,10 @@ snapshots:
dependencies:
js-tokens: 4.0.0
+ lower-case@2.0.2:
+ dependencies:
+ tslib: 2.8.1
+
lowlight@1.20.0:
dependencies:
fault: 1.0.4
@@ -11608,6 +13397,10 @@ snapshots:
lru-cache@10.4.3: {}
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
lucide-react@0.477.0(react@19.2.3):
dependencies:
react: 19.2.3
@@ -11812,6 +13605,10 @@ snapshots:
dependencies:
'@types/mdast': 4.0.4
+ mdn-data@2.0.28: {}
+
+ mdn-data@2.0.30: {}
+
mdurl@2.0.0: {}
merge2@1.4.1: {}
@@ -12210,13 +14007,13 @@ snapshots:
next-intl-swc-plugin-extractor@4.6.1: {}
- next-intl@4.6.1(next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(typescript@5.9.3):
+ next-intl@4.6.1(next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(typescript@5.9.3):
dependencies:
'@formatjs/intl-localematcher': 0.5.10
'@parcel/watcher': 2.5.1
'@swc/core': 1.15.7
negotiator: 1.0.0
- next: 16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ next: 16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-intl-swc-plugin-extractor: 4.6.1
po-parser: 2.0.0
react: 19.2.3
@@ -12231,7 +14028,7 @@ snapshots:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
- next@16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ next@16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@next/env': 16.1.0
'@swc/helpers': 0.5.15
@@ -12240,7 +14037,7 @@ snapshots:
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
- styled-jsx: 5.1.6(react@19.2.3)
+ styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.3)
optionalDependencies:
'@next/swc-darwin-arm64': 16.1.0
'@next/swc-darwin-x64': 16.1.0
@@ -12256,6 +14053,11 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
+ no-case@3.0.4:
+ dependencies:
+ lower-case: 2.0.2
+ tslib: 2.8.1
+
node-abi@3.85.0:
dependencies:
semver: 7.7.3
@@ -12263,8 +14065,14 @@ snapshots:
node-addon-api@7.1.1: {}
+ node-releases@2.0.27: {}
+
npm-to-yarn@3.0.1: {}
+ nth-check@2.1.1:
+ dependencies:
+ boolbase: 1.0.0
+
number-flow@0.5.8:
dependencies:
esm-env: 1.2.2
@@ -12376,6 +14184,13 @@ snapshots:
is-decimal: 2.0.1
is-hexadecimal: 2.0.1
+ parse-json@5.2.0:
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ error-ex: 1.3.4
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+
parse5@7.3.0:
dependencies:
entities: 6.0.1
@@ -12390,6 +14205,8 @@ snapshots:
path-to-regexp@8.3.0: {}
+ path-type@4.0.0: {}
+
pathe@2.0.3: {}
pg-cloudflare@1.2.7:
@@ -12886,6 +14703,12 @@ snapshots:
parse-entities: 2.0.0
prismjs: 1.27.0
+ regenerate-unicode-properties@10.2.2:
+ dependencies:
+ regenerate: 1.4.2
+
+ regenerate@1.4.2: {}
+
regex-recursion@6.0.2:
dependencies:
regex-utilities: 2.3.0
@@ -12905,6 +14728,21 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
+ regexpu-core@6.4.0:
+ dependencies:
+ regenerate: 1.4.2
+ regenerate-unicode-properties: 10.2.2
+ regjsgen: 0.8.0
+ regjsparser: 0.13.0
+ unicode-match-property-ecmascript: 2.0.0
+ unicode-match-property-value-ecmascript: 2.2.1
+
+ regjsgen@0.8.0: {}
+
+ regjsparser@0.13.0:
+ dependencies:
+ jsesc: 3.1.0
+
rehype-format@5.0.1:
dependencies:
'@types/hast': 3.0.4
@@ -13272,6 +15110,11 @@ snapshots:
simple-concat: 1.0.1
optional: true
+ snake-case@3.0.4:
+ dependencies:
+ dot-case: 3.0.4
+ tslib: 2.8.1
+
sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
react: 19.2.3
@@ -13407,10 +15250,12 @@ snapshots:
dependencies:
inline-style-parser: 0.2.7
- styled-jsx@5.1.6(react@19.2.3):
+ styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.3):
dependencies:
client-only: 0.0.1
react: 19.2.3
+ optionalDependencies:
+ '@babel/core': 7.29.0
stylis@4.3.6: {}
@@ -13420,6 +15265,18 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ svg-parser@2.0.4: {}
+
+ svgo@3.3.2:
+ dependencies:
+ '@trysound/sax': 0.2.0
+ commander: 7.2.0
+ css-select: 5.2.2
+ css-tree: 2.3.1
+ css-what: 6.2.2
+ csso: 5.0.5
+ picocolors: 1.1.1
+
swr@2.3.8(react@19.2.3):
dependencies:
dequal: 2.0.3
@@ -13571,6 +15428,17 @@ snapshots:
undici-types@6.21.0: {}
+ unicode-canonical-property-names-ecmascript@2.0.1: {}
+
+ unicode-match-property-ecmascript@2.0.0:
+ dependencies:
+ unicode-canonical-property-names-ecmascript: 2.0.1
+ unicode-property-aliases-ecmascript: 2.2.0
+
+ unicode-match-property-value-ecmascript@2.2.1: {}
+
+ unicode-property-aliases-ecmascript@2.2.0: {}
+
unified@11.0.5:
dependencies:
'@types/unist': 3.0.3
@@ -13642,6 +15510,12 @@ snapshots:
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
'@unrs/resolver-binding-win32-x64-msvc': 1.11.1
+ update-browserslist-db@1.2.3(browserslist@4.28.1):
+ dependencies:
+ browserslist: 4.28.1
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
@@ -13848,6 +15722,8 @@ snapshots:
lib0: 0.2.115
yjs: 13.6.28
+ yallist@3.1.1: {}
+
yjs@13.6.28:
dependencies:
lib0: 0.2.115
diff --git a/surfsense_web/svgr.d.ts b/surfsense_web/svgr.d.ts
new file mode 100644
index 000000000..79922fb0f
--- /dev/null
+++ b/surfsense_web/svgr.d.ts
@@ -0,0 +1,6 @@
+declare module "*.svg" {
+ import type { FC, SVGProps } from "react";
+ const content: FC>;
+ export default content;
+}
+
From 312fedd6a2375d305c96987e53630ac78e5fd629 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 10 Feb 2026 17:20:42 +0530
Subject: [PATCH 24/35] refactor: remove ImageModelSelector component and
update ModelSelector and ChatHeader to handle LLM and image model
configurations more efficiently
---
.../components/new-chat/chat-header.tsx | 13 +-
.../new-chat/image-model-selector.tsx | 361 --------
.../components/new-chat/model-selector.tsx | 774 ++++++++++++------
surfsense_web/lib/provider-icons.tsx | 2 +-
4 files changed, 532 insertions(+), 618 deletions(-)
delete mode 100644 surfsense_web/components/new-chat/image-model-selector.tsx
diff --git a/surfsense_web/components/new-chat/chat-header.tsx b/surfsense_web/components/new-chat/chat-header.tsx
index 8a8fa11a0..2f1d9d845 100644
--- a/surfsense_web/components/new-chat/chat-header.tsx
+++ b/surfsense_web/components/new-chat/chat-header.tsx
@@ -8,7 +8,6 @@ import type {
NewLLMConfigPublic,
} from "@/contracts/types/new-llm-config.types";
import { ImageConfigSidebar } from "./image-config-sidebar";
-import { ImageModelSelector } from "./image-model-selector";
import { ModelConfigSidebar } from "./model-config-sidebar";
import { ModelSelector } from "./model-selector";
@@ -34,7 +33,7 @@ export function ChatHeader({ searchSpaceId }: ChatHeaderProps) {
const [imageSidebarMode, setImageSidebarMode] = useState<"create" | "edit" | "view">("view");
// LLM handlers
- const handleEditConfig = useCallback(
+ const handleEditLLMConfig = useCallback(
(config: NewLLMConfigPublic | GlobalNewLLMConfig, global: boolean) => {
setSelectedConfig(config);
setIsGlobal(global);
@@ -44,7 +43,7 @@ export function ChatHeader({ searchSpaceId }: ChatHeaderProps) {
[]
);
- const handleAddNew = useCallback(() => {
+ const handleAddNewLLM = useCallback(() => {
setSelectedConfig(null);
setIsGlobal(false);
setSidebarMode("create");
@@ -81,8 +80,12 @@ export function ChatHeader({ searchSpaceId }: ChatHeaderProps) {
return (
-
-
+
void;
- onEdit?: (config: ImageGenerationConfig | GlobalImageGenConfig, isGlobal: boolean) => void;
-}
-
-export function ImageModelSelector({ className, onAddNew, onEdit }: ImageModelSelectorProps) {
- const [open, setOpen] = useState(false);
- const [searchQuery, setSearchQuery] = useState("");
-
- const { data: globalConfigs, isLoading: globalLoading } = useAtomValue(globalImageGenConfigsAtom);
- const { data: userConfigs, isLoading: userLoading } = useAtomValue(imageGenConfigsAtom);
- const { data: preferences, isLoading: prefsLoading } = useAtomValue(llmPreferencesAtom);
- const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
- const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom);
-
- const isLoading = globalLoading || userLoading || prefsLoading;
-
- const currentConfig = useMemo(() => {
- if (!preferences) return null;
- const id = preferences.image_generation_config_id;
- if (id === null || id === undefined) return null;
- const globalMatch = globalConfigs?.find((c) => c.id === id);
- if (globalMatch) return globalMatch;
- return userConfigs?.find((c) => c.id === id) ?? null;
- }, [preferences, globalConfigs, userConfigs]);
-
- const isCurrentAutoMode = useMemo(() => {
- return currentConfig && "is_auto_mode" in currentConfig && currentConfig.is_auto_mode;
- }, [currentConfig]);
-
- const filteredGlobal = useMemo(() => {
- if (!globalConfigs) return [];
- if (!searchQuery) return globalConfigs;
- const q = searchQuery.toLowerCase();
- return globalConfigs.filter(
- (c) =>
- c.name.toLowerCase().includes(q) ||
- c.model_name.toLowerCase().includes(q) ||
- c.provider.toLowerCase().includes(q)
- );
- }, [globalConfigs, searchQuery]);
-
- const filteredUser = useMemo(() => {
- if (!userConfigs) return [];
- if (!searchQuery) return userConfigs;
- const q = searchQuery.toLowerCase();
- return userConfigs.filter(
- (c) =>
- c.name.toLowerCase().includes(q) ||
- c.model_name.toLowerCase().includes(q) ||
- c.provider.toLowerCase().includes(q)
- );
- }, [userConfigs, searchQuery]);
-
- const totalModels = (globalConfigs?.length ?? 0) + (userConfigs?.length ?? 0);
-
- const handleSelect = useCallback(
- async (configId: number) => {
- if (currentConfig?.id === configId) {
- setOpen(false);
- return;
- }
- if (!searchSpaceId) {
- toast.error("No search space selected");
- return;
- }
- try {
- await updatePreferences({
- search_space_id: Number(searchSpaceId),
- data: { image_generation_config_id: configId },
- });
- toast.success("Image model updated");
- setOpen(false);
- } catch {
- toast.error("Failed to switch image model");
- }
- },
- [currentConfig, searchSpaceId, updatePreferences]
- );
-
- // Don't render if no configs at all
- if (!isLoading && totalModels === 0) {
- return (
-
-
- Add Image Model
-
- );
- }
-
- return (
-
-
-
- {isLoading ? (
-
- ) : currentConfig ? (
- <>
- {isCurrentAutoMode ? (
-
- ) : (
-
- )}
-
- {currentConfig.name}
-
- {isCurrentAutoMode ? (
-
- Auto
-
- ) : (
-
- Image
-
- )}
- >
- ) : (
- <>
-
- Image Model
- >
- )}
-
-
-
-
-
-
- {totalModels > 3 && (
-
-
-
- )}
-
-
-
-
-
No image models found
-
-
-
- {/* Global Image Gen Configs */}
- {filteredGlobal.length > 0 && (
-
-
-
- Global Image Models
-
- {filteredGlobal.map((config) => {
- const isSelected = currentConfig?.id === config.id;
- const isAuto = "is_auto_mode" in config && config.is_auto_mode;
- return (
- handleSelect(config.id)}
- className={cn(
- "mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
- isSelected && "bg-accent/80",
- isAuto && "border border-violet-200 dark:border-violet-800/50"
- )}
- >
-
-
- {isAuto ? (
-
- ) : (
-
- )}
-
-
-
- {config.name}
- {isAuto && (
-
- Recommended
-
- )}
- {isSelected && }
-
-
- {isAuto ? "Auto load balancing" : config.model_name}
-
-
- {onEdit && (
-
{
- e.stopPropagation();
- setOpen(false);
- onEdit(config, true);
- }}
- />
- )}
-
-
- );
- })}
-
- )}
-
- {/* User Image Gen Configs */}
- {filteredUser.length > 0 && (
- <>
- {filteredGlobal.length > 0 && }
-
-
-
- Your Image Models
-
- {filteredUser.map((config) => {
- const isSelected = currentConfig?.id === config.id;
- return (
- handleSelect(config.id)}
- className={cn(
- "mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
- isSelected && "bg-accent/80"
- )}
- >
-
-
-
-
-
-
- {config.name}
- {isSelected && }
-
-
- {config.model_name}
-
-
- {onEdit && (
-
{
- e.stopPropagation();
- setOpen(false);
- onEdit(config, false);
- }}
- >
-
-
- )}
-
-
- );
- })}
-
- >
- )}
-
- {/* Add New */}
- {onAddNew && (
-
-
{
- setOpen(false);
- onAddNew();
- }}
- >
-
- Add Image Model
-
-
- )}
-
-
-
-
- );
-}
diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx
index 988dc7209..d27594ee6 100644
--- a/surfsense_web/components/new-chat/model-selector.tsx
+++ b/surfsense_web/components/new-chat/model-selector.tsx
@@ -6,10 +6,16 @@ import {
Check,
ChevronDown,
Edit3,
+ ImageIcon,
Plus,
+ Zap,
} from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
+import {
+ globalImageGenConfigsAtom,
+ imageGenConfigsAtom,
+} from "@/atoms/image-gen-config/image-gen-config-query.atoms";
import { updateLLMPreferencesMutationAtom } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms";
import {
globalNewLLMConfigsAtom,
@@ -30,102 +36,150 @@ import {
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Spinner } from "@/components/ui/spinner";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type {
+ GlobalImageGenConfig,
GlobalNewLLMConfig,
+ ImageGenerationConfig,
NewLLMConfigPublic,
} from "@/contracts/types/new-llm-config.types";
-import { cn } from "@/lib/utils";
import { getProviderIcon } from "@/lib/provider-icons";
+import { cn } from "@/lib/utils";
interface ModelSelectorProps {
- onEdit: (config: NewLLMConfigPublic | GlobalNewLLMConfig, isGlobal: boolean) => void;
- onAddNew: () => void;
+ onEditLLM: (config: NewLLMConfigPublic | GlobalNewLLMConfig, isGlobal: boolean) => void;
+ onAddNewLLM: () => void;
+ onEditImage?: (config: ImageGenerationConfig | GlobalImageGenConfig, isGlobal: boolean) => void;
+ onAddNewImage?: () => void;
className?: string;
}
-export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProps) {
+export function ModelSelector({
+ onEditLLM,
+ onAddNewLLM,
+ onEditImage,
+ onAddNewImage,
+ className,
+}: ModelSelectorProps) {
const [open, setOpen] = useState(false);
- const [searchQuery, setSearchQuery] = useState("");
+ const [activeTab, setActiveTab] = useState<"llm" | "image">("llm");
+ const [llmSearchQuery, setLlmSearchQuery] = useState("");
+ const [imageSearchQuery, setImageSearchQuery] = useState("");
- // Fetch configs
- const { data: userConfigs, isLoading: userConfigsLoading } = useAtomValue(newLLMConfigsAtom);
- const { data: globalConfigs, isLoading: globalConfigsLoading } =
+ // LLM data
+ const { data: llmUserConfigs, isLoading: llmUserLoading } = useAtomValue(newLLMConfigsAtom);
+ const { data: llmGlobalConfigs, isLoading: llmGlobalLoading } =
useAtomValue(globalNewLLMConfigsAtom);
- const { data: preferences, isLoading: preferencesLoading } = useAtomValue(llmPreferencesAtom);
+ const { data: preferences, isLoading: prefsLoading } = useAtomValue(llmPreferencesAtom);
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom);
- const isLoading = userConfigsLoading || globalConfigsLoading || preferencesLoading;
+ // Image data
+ const { data: imageGlobalConfigs, isLoading: imageGlobalLoading } =
+ useAtomValue(globalImageGenConfigsAtom);
+ const { data: imageUserConfigs, isLoading: imageUserLoading } =
+ useAtomValue(imageGenConfigsAtom);
- // Get current agent LLM config
- const currentConfig = useMemo(() => {
+ const isLoading = llmUserLoading || llmGlobalLoading || prefsLoading || imageGlobalLoading || imageUserLoading;
+
+ // ─── LLM current config ───
+ const currentLLMConfig = useMemo(() => {
if (!preferences) return null;
-
const agentLlmId = preferences.agent_llm_id;
if (agentLlmId === null || agentLlmId === undefined) return null;
-
- // Check if it's Auto mode (ID 0) or global config (negative ID)
if (agentLlmId <= 0) {
- return globalConfigs?.find((c) => c.id === agentLlmId) ?? null;
+ return llmGlobalConfigs?.find((c) => c.id === agentLlmId) ?? null;
}
- // Otherwise, check user configs
- return userConfigs?.find((c) => c.id === agentLlmId) ?? null;
- }, [preferences, globalConfigs, userConfigs]);
+ return llmUserConfigs?.find((c) => c.id === agentLlmId) ?? null;
+ }, [preferences, llmGlobalConfigs, llmUserConfigs]);
- // Check if current config is Auto mode
- const isCurrentAutoMode = useMemo(() => {
- return currentConfig && "is_auto_mode" in currentConfig && currentConfig.is_auto_mode;
- }, [currentConfig]);
+ const isLLMAutoMode = useMemo(() => {
+ return currentLLMConfig && "is_auto_mode" in currentLLMConfig && currentLLMConfig.is_auto_mode;
+ }, [currentLLMConfig]);
- // Filter configs based on search
- const filteredGlobalConfigs = useMemo(() => {
- if (!globalConfigs) return [];
- if (!searchQuery) return globalConfigs;
- const query = searchQuery.toLowerCase();
- return globalConfigs.filter(
+ // ─── Image current config ───
+ const currentImageConfig = useMemo(() => {
+ if (!preferences) return null;
+ const id = preferences.image_generation_config_id;
+ if (id === null || id === undefined) return null;
+ const globalMatch = imageGlobalConfigs?.find((c) => c.id === id);
+ if (globalMatch) return globalMatch;
+ return imageUserConfigs?.find((c) => c.id === id) ?? null;
+ }, [preferences, imageGlobalConfigs, imageUserConfigs]);
+
+ const isImageAutoMode = useMemo(() => {
+ return currentImageConfig && "is_auto_mode" in currentImageConfig && currentImageConfig.is_auto_mode;
+ }, [currentImageConfig]);
+
+ // ─── LLM filtering ───
+ const filteredLLMGlobal = useMemo(() => {
+ if (!llmGlobalConfigs) return [];
+ if (!llmSearchQuery) return llmGlobalConfigs;
+ const q = llmSearchQuery.toLowerCase();
+ return llmGlobalConfigs.filter(
(c) =>
- c.name.toLowerCase().includes(query) ||
- c.model_name.toLowerCase().includes(query) ||
- c.provider.toLowerCase().includes(query)
+ c.name.toLowerCase().includes(q) ||
+ c.model_name.toLowerCase().includes(q) ||
+ c.provider.toLowerCase().includes(q)
);
- }, [globalConfigs, searchQuery]);
+ }, [llmGlobalConfigs, llmSearchQuery]);
- const filteredUserConfigs = useMemo(() => {
- if (!userConfigs) return [];
- if (!searchQuery) return userConfigs;
- const query = searchQuery.toLowerCase();
- return userConfigs.filter(
+ const filteredLLMUser = useMemo(() => {
+ if (!llmUserConfigs) return [];
+ if (!llmSearchQuery) return llmUserConfigs;
+ const q = llmSearchQuery.toLowerCase();
+ return llmUserConfigs.filter(
(c) =>
- c.name.toLowerCase().includes(query) ||
- c.model_name.toLowerCase().includes(query) ||
- c.provider.toLowerCase().includes(query)
+ c.name.toLowerCase().includes(q) ||
+ c.model_name.toLowerCase().includes(q) ||
+ c.provider.toLowerCase().includes(q)
);
- }, [userConfigs, searchQuery]);
+ }, [llmUserConfigs, llmSearchQuery]);
- // Total model count for conditional search display
- const totalModels = useMemo(() => {
- return (globalConfigs?.length ?? 0) + (userConfigs?.length ?? 0);
- }, [globalConfigs, userConfigs]);
+ const totalLLMModels = (llmGlobalConfigs?.length ?? 0) + (llmUserConfigs?.length ?? 0);
- const handleSelectConfig = useCallback(
+ // ─── Image filtering ───
+ const filteredImageGlobal = useMemo(() => {
+ if (!imageGlobalConfigs) return [];
+ if (!imageSearchQuery) return imageGlobalConfigs;
+ const q = imageSearchQuery.toLowerCase();
+ return imageGlobalConfigs.filter(
+ (c) =>
+ c.name.toLowerCase().includes(q) ||
+ c.model_name.toLowerCase().includes(q) ||
+ c.provider.toLowerCase().includes(q)
+ );
+ }, [imageGlobalConfigs, imageSearchQuery]);
+
+ const filteredImageUser = useMemo(() => {
+ if (!imageUserConfigs) return [];
+ if (!imageSearchQuery) return imageUserConfigs;
+ const q = imageSearchQuery.toLowerCase();
+ return imageUserConfigs.filter(
+ (c) =>
+ c.name.toLowerCase().includes(q) ||
+ c.model_name.toLowerCase().includes(q) ||
+ c.provider.toLowerCase().includes(q)
+ );
+ }, [imageUserConfigs, imageSearchQuery]);
+
+ const totalImageModels = (imageGlobalConfigs?.length ?? 0) + (imageUserConfigs?.length ?? 0);
+
+ // ─── Handlers ───
+ const handleSelectLLM = useCallback(
async (config: NewLLMConfigPublic | GlobalNewLLMConfig) => {
- // If already selected, just close
- if (currentConfig?.id === config.id) {
+ if (currentLLMConfig?.id === config.id) {
setOpen(false);
return;
}
-
if (!searchSpaceId) {
toast.error("No search space selected");
return;
}
-
try {
await updatePreferences({
search_space_id: Number(searchSpaceId),
- data: {
- agent_llm_id: config.id,
- },
+ data: { agent_llm_id: config.id },
});
toast.success(`Switched to ${config.name}`);
setOpen(false);
@@ -134,16 +188,40 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
toast.error("Failed to switch model");
}
},
- [currentConfig, searchSpaceId, updatePreferences]
+ [currentLLMConfig, searchSpaceId, updatePreferences]
);
- const handleEditConfig = useCallback(
+ const handleEditLLMConfig = useCallback(
(e: React.MouseEvent, config: NewLLMConfigPublic | GlobalNewLLMConfig, isGlobal: boolean) => {
e.stopPropagation();
- onEdit(config, isGlobal);
+ onEditLLM(config, isGlobal);
setOpen(false);
},
- [onEdit]
+ [onEditLLM]
+ );
+
+ const handleSelectImage = useCallback(
+ async (configId: number) => {
+ if (currentImageConfig?.id === configId) {
+ setOpen(false);
+ return;
+ }
+ if (!searchSpaceId) {
+ toast.error("No search space selected");
+ return;
+ }
+ try {
+ await updatePreferences({
+ search_space_id: Number(searchSpaceId),
+ data: { image_generation_config_id: configId },
+ });
+ toast.success("Image model updated");
+ setOpen(false);
+ } catch {
+ toast.error("Failed to switch image model");
+ }
+ },
+ [currentImageConfig, searchSpaceId, updatePreferences]
);
return (
@@ -161,30 +239,37 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
Loading
>
- ) : currentConfig ? (
- <>
- {getProviderIcon(currentConfig.provider, { isAutoMode: isCurrentAutoMode ?? false })}
-
- {currentConfig.name}
-
- {isCurrentAutoMode ? (
-
- Balanced
-
- ) : (
-
- {currentConfig.model_name.split("/").pop()?.slice(0, 10) ||
- currentConfig.model_name.slice(0, 10)}
-
- )}
- >
) : (
<>
-
- Select Model
+ {/* LLM section */}
+ {currentLLMConfig ? (
+ <>
+ {getProviderIcon(currentLLMConfig.provider, { isAutoMode: isLLMAutoMode ?? false })}
+
+ {currentLLMConfig.name}
+
+ >
+ ) : (
+ <>
+
+ Select Model
+ >
+ )}
+
+ {/* Divider */}
+
+
+ {/* Image section */}
+ {currentImageConfig ? (
+ <>
+ {getProviderIcon(currentImageConfig.provider, { isAutoMode: isImageAutoMode ?? false })}
+
+ {currentImageConfig.name}
+
+ >
+ ) : (
+
+ )}
>
)}
- setActiveTab(v as "llm" | "image")}
+ className="w-full"
>
- {totalModels > 3 && (
-
-
-
- )}
-
-
-
-
-
-
No models found
-
Try a different search term
-
-
-
- {/* Global Configs Section */}
- {filteredGlobalConfigs.length > 0 && (
-
-
- Global Models
-
- {filteredGlobalConfigs.map((config) => {
- const isSelected = currentConfig?.id === config.id;
- const isAutoMode = "is_auto_mode" in config && config.is_auto_mode;
- return (
- handleSelectConfig(config)}
- className={cn(
- "mx-2 rounded-lg mb-1 cursor-pointer group transition-all",
- "hover:bg-accent/50",
- isSelected && "bg-accent/80",
- isAutoMode && "border border-violet-200 dark:border-violet-800/50"
- )}
- >
-
-
-
- {getProviderIcon(config.provider, { isAutoMode })}
-
-
-
- {config.name}
- {isAutoMode && (
-
- Recommended
-
- )}
- {isSelected && }
-
-
-
- {isAutoMode ? "Auto load balancing" : config.model_name}
-
- {!isAutoMode && config.citations_enabled && (
-
- Citations
-
- )}
-
-
-
- {!isAutoMode && (
-
handleEditConfig(e, config, true)}
- >
-
-
- )}
-
-
- );
- })}
-
- )}
-
- {filteredGlobalConfigs.length > 0 && filteredUserConfigs.length > 0 && (
-
- )}
-
- {/* User Configs Section */}
- {filteredUserConfigs.length > 0 && (
-
-
- Your Configurations
-
- {filteredUserConfigs.map((config) => {
- const isSelected = currentConfig?.id === config.id;
- return (
- handleSelectConfig(config)}
- className={cn(
- "mx-2 rounded-lg mb-1 cursor-pointer group transition-all",
- "hover:bg-accent/50",
- isSelected && "bg-accent/80"
- )}
- >
-
-
-
{getProviderIcon(config.provider)}
-
-
- {config.name}
- {isSelected && }
-
-
-
- {config.model_name}
-
- {config.citations_enabled && (
-
- Citations
-
- )}
-
-
-
-
handleEditConfig(e, config, false)}
- >
-
-
-
-
- );
- })}
-
- )}
-
- {/* Add New Config Button */}
-
-
{
- setOpen(false);
- onAddNew();
- }}
+
+
+
-
- Add New Configuration
-
-
-
-
+
+ LLM
+
+
+
+ Image
+
+
+
+
+ {/* ─── LLM Tab ─── */}
+
+
+ {totalLLMModels > 3 && (
+
+
+
+ )}
+
+
+
+
+
+
No models found
+
Try a different search term
+
+
+
+ {/* Global LLM Configs */}
+ {filteredLLMGlobal.length > 0 && (
+
+
+ Global Models
+
+ {filteredLLMGlobal.map((config) => {
+ const isSelected = currentLLMConfig?.id === config.id;
+ const isAutoMode = "is_auto_mode" in config && config.is_auto_mode;
+ return (
+ handleSelectLLM(config)}
+ className={cn(
+ "mx-2 rounded-lg mb-1 cursor-pointer group transition-all",
+ "hover:bg-accent/50",
+ isSelected && "bg-accent/80",
+ isAutoMode && "border border-violet-800"
+ )}
+ >
+
+
+
+ {getProviderIcon(config.provider, { isAutoMode })}
+
+
+
+ {config.name}
+ {isAutoMode && (
+
+ Recommended
+
+ )}
+ {isSelected && }
+
+
+
+ {isAutoMode ? "Auto load balancing" : config.model_name}
+
+ {!isAutoMode && config.citations_enabled && (
+
+ Citations
+
+ )}
+
+
+
+ {!isAutoMode && (
+
handleEditLLMConfig(e, config, true)}
+ >
+
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+ {filteredLLMGlobal.length > 0 && filteredLLMUser.length > 0 && (
+
+ )}
+
+ {/* User LLM Configs */}
+ {filteredLLMUser.length > 0 && (
+
+
+ Your Configurations
+
+ {filteredLLMUser.map((config) => {
+ const isSelected = currentLLMConfig?.id === config.id;
+ return (
+ handleSelectLLM(config)}
+ className={cn(
+ "mx-2 rounded-lg mb-1 cursor-pointer group transition-all",
+ "hover:bg-accent/50",
+ isSelected && "bg-accent/80"
+ )}
+ >
+
+
+
{getProviderIcon(config.provider)}
+
+
+ {config.name}
+ {isSelected && }
+
+
+
+ {config.model_name}
+
+ {config.citations_enabled && (
+
+ Citations
+
+ )}
+
+
+
+
handleEditLLMConfig(e, config, false)}
+ >
+
+
+
+
+ );
+ })}
+
+ )}
+
+ {/* Add New LLM Config */}
+
+
{
+ setOpen(false);
+ onAddNewLLM();
+ }}
+ >
+
+ Add New Configuration
+
+
+
+
+
+
+ {/* ─── Image Tab ─── */}
+
+
+ {totalImageModels > 3 && (
+
+
+
+ )}
+
+
+
+
+
No image models found
+
+
+
+ {/* Global Image Configs */}
+ {filteredImageGlobal.length > 0 && (
+
+
+ Global Image Models
+
+ {filteredImageGlobal.map((config) => {
+ const isSelected = currentImageConfig?.id === config.id;
+ const isAuto = "is_auto_mode" in config && config.is_auto_mode;
+ return (
+ handleSelectImage(config.id)}
+ className={cn(
+ "mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
+ isSelected && "bg-accent/80",
+ isAuto && "border border-violet-800"
+ )}
+ >
+
+
+ {getProviderIcon(config.provider, { isAutoMode: isAuto })}
+
+
+
+ {config.name}
+ {isAuto && (
+
+ Recommended
+
+ )}
+ {isSelected && }
+
+
+ {isAuto ? "Auto load balancing" : config.model_name}
+
+
+ {onEditImage && !isAuto && (
+
{
+ e.stopPropagation();
+ setOpen(false);
+ onEditImage(config, true);
+ }}
+ >
+
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+ {/* User Image Configs */}
+ {filteredImageUser.length > 0 && (
+ <>
+ {filteredImageGlobal.length > 0 && }
+
+
+ Your Image Models
+
+ {filteredImageUser.map((config) => {
+ const isSelected = currentImageConfig?.id === config.id;
+ return (
+ handleSelectImage(config.id)}
+ className={cn(
+ "mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
+ isSelected && "bg-accent/80"
+ )}
+ >
+
+
+ {getProviderIcon(config.provider)}
+
+
+
+ {config.name}
+ {isSelected && }
+
+
+ {config.model_name}
+
+
+ {onEditImage && (
+
{
+ e.stopPropagation();
+ setOpen(false);
+ onEditImage(config, false);
+ }}
+ >
+
+
+ )}
+
+
+ );
+ })}
+
+ >
+ )}
+
+ {/* Add New Image Config */}
+ {onAddNewImage && (
+
+
{
+ setOpen(false);
+ onAddNewImage();
+ }}
+ >
+
+ Add Image Model
+
+
+ )}
+
+
+
+
);
diff --git a/surfsense_web/lib/provider-icons.tsx b/surfsense_web/lib/provider-icons.tsx
index ce474c5a9..4a32f0df0 100644
--- a/surfsense_web/lib/provider-icons.tsx
+++ b/surfsense_web/lib/provider-icons.tsx
@@ -47,7 +47,7 @@ export function getProviderIcon(
}: { isAutoMode?: boolean; className?: string } = {}
) {
if (isAutoMode || provider?.toUpperCase() === "AUTO") {
- return ;
+ return ;
}
switch (provider?.toUpperCase()) {
From 7557f5d2be666061e882d748dcafd39623af95a7 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 10 Feb 2026 18:02:04 +0530
Subject: [PATCH 25/35] feat: re-add image model preference management to
ImageModelManager with selection and saving functionality
---
.../settings/image-model-manager.tsx | 179 +++++++++++++++++-
1 file changed, 176 insertions(+), 3 deletions(-)
diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx
index 0170fcf8c..37a1636c7 100644
--- a/surfsense_web/components/settings/image-model-manager.tsx
+++ b/surfsense_web/components/settings/image-model-manager.tsx
@@ -6,16 +6,18 @@ import {
Check,
ChevronsUpDown,
Edit3,
+ ImageIcon,
Key,
Plus,
RefreshCw,
+ Shuffle,
Info,
Trash2,
Wand2,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
-import { useCallback, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import {
@@ -42,7 +44,7 @@ import {
} from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Command,
CommandEmpty,
@@ -129,7 +131,7 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
} = useAtomValue(imageGenConfigsAtom);
const { data: globalConfigs = [], isFetching: globalLoading } =
useAtomValue(globalImageGenConfigsAtom);
- const { isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom);
+ const { data: preferences = {}, isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom);
// Members for user resolution
const { data: members } = useAtomValue(membersAtom);
@@ -168,6 +170,18 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
const [editingConfig, setEditingConfig] = useState(null);
const [configToDelete, setConfigToDelete] = useState(null);
+ // Preference state
+ const [selectedPrefId, setSelectedPrefId] = useState(
+ preferences.image_generation_config_id ?? ""
+ );
+ const [hasPrefChanges, setHasPrefChanges] = useState(false);
+ const [isSavingPref, setIsSavingPref] = useState(false);
+
+ useEffect(() => {
+ setSelectedPrefId(preferences.image_generation_config_id ?? "");
+ setHasPrefChanges(false);
+ }, [preferences]);
+
const isSubmitting = isCreating || isUpdating;
const isLoading = configsLoading || globalLoading || prefsLoading;
const errors = [createError, updateError, deleteError, fetchError].filter(Boolean) as Error[];
@@ -277,6 +291,39 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
setIsDialogOpen(true);
};
+ const handlePrefChange = (value: string) => {
+ const newVal = value === "unassigned" ? "" : parseInt(value);
+ setSelectedPrefId(newVal);
+ setHasPrefChanges(newVal !== (preferences.image_generation_config_id ?? ""));
+ };
+
+ const handleSavePref = async () => {
+ setIsSavingPref(true);
+ try {
+ await updatePreferences({
+ search_space_id: searchSpaceId,
+ data: {
+ image_generation_config_id:
+ typeof selectedPrefId === "string"
+ ? selectedPrefId
+ ? parseInt(selectedPrefId)
+ : undefined
+ : selectedPrefId,
+ },
+ });
+ setHasPrefChanges(false);
+ toast.success("Image generation model preference saved!");
+ } catch {
+ toast.error("Failed to save preference");
+ } finally {
+ setIsSavingPref(false);
+ }
+ };
+
+ const allConfigs = [
+ ...globalConfigs.map((c) => ({ ...c, _source: "global" as const })),
+ ...(userConfigs ?? []).map((c) => ({ ...c, _source: "user" as const })),
+ ];
const selectedProvider = IMAGE_GEN_PROVIDERS.find((p) => p.value === formData.provider);
const suggestedModels = getImageGenModelsByProvider(formData.provider);
@@ -356,9 +403,135 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)}
+ {/* Active Preference Card */}
+ {!isLoading && allConfigs.length > 0 && (
+
+
+
+
+
+
+
+
+ Active Image Model
+
+ Select which model to use for image generation
+
+
+
+
+
+
+
+
+
+
+
+ Unassigned
+
+ {globalConfigs.length > 0 && (
+ <>
+
+ Global
+
+ {globalConfigs.map((c) => {
+ const isAuto = "is_auto_mode" in c && c.is_auto_mode;
+ return (
+
+
+ {isAuto ? (
+
+
+ AUTO
+
+ ) : (
+
+ {c.provider}
+
+ )}
+ {c.name}
+
+
+ );
+ })}
+ >
+ )}
+ {(userConfigs?.length ?? 0) > 0 && (
+ <>
+
+ Your Models
+
+ {userConfigs?.map((c) => (
+
+
+
+ {c.provider}
+
+ {c.name}
+ ({c.model_name})
+
+
+ ))}
+ >
+ )}
+
+
+ {hasPrefChanges && (
+
+
+ {isSavingPref ? "Saving..." : "Save"}
+
+ {
+ setSelectedPrefId(preferences.image_generation_config_id ?? "");
+ setHasPrefChanges(false);
+ }}
+ className="text-xs h-8"
+ >
+ Reset
+
+
+ )}
+
+
+
+ )}
+
{/* Loading Skeleton */}
{isLoading && (
+ {/* Active Preference Skeleton */}
+
+
+
+
+
+
+
+
+
{/* Your Image Models Section Skeleton */}
From 4d7132c16cfcc50ec3aaed4acac00aa264a246f9 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 10 Feb 2026 18:09:27 +0530
Subject: [PATCH 26/35] feat: update LLM role manager to include image
generation model preferences and improve loading/error handling
---
.../settings/image-model-manager.tsx | 203 +-----------------
.../components/settings/llm-role-manager.tsx | 124 +++++++----
2 files changed, 94 insertions(+), 233 deletions(-)
diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx
index 37a1636c7..f2d010e10 100644
--- a/surfsense_web/components/settings/image-model-manager.tsx
+++ b/surfsense_web/components/settings/image-model-manager.tsx
@@ -6,18 +6,16 @@ import {
Check,
ChevronsUpDown,
Edit3,
- ImageIcon,
Key,
Plus,
RefreshCw,
- Shuffle,
Info,
Trash2,
Wand2,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import {
@@ -30,7 +28,6 @@ import {
imageGenConfigsAtom,
} from "@/atoms/image-gen-config/image-gen-config-query.atoms";
import { updateLLMPreferencesMutationAtom } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms";
-import { llmPreferencesAtom } from "@/atoms/new-llm-config/new-llm-config-query.atoms";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
AlertDialog,
@@ -42,9 +39,8 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
-import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Card, CardContent } from "@/components/ui/card";
import {
Command,
CommandEmpty,
@@ -131,7 +127,6 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
} = useAtomValue(imageGenConfigsAtom);
const { data: globalConfigs = [], isFetching: globalLoading } =
useAtomValue(globalImageGenConfigsAtom);
- const { data: preferences = {}, isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom);
// Members for user resolution
const { data: members } = useAtomValue(membersAtom);
@@ -170,20 +165,8 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
const [editingConfig, setEditingConfig] = useState(null);
const [configToDelete, setConfigToDelete] = useState(null);
- // Preference state
- const [selectedPrefId, setSelectedPrefId] = useState(
- preferences.image_generation_config_id ?? ""
- );
- const [hasPrefChanges, setHasPrefChanges] = useState(false);
- const [isSavingPref, setIsSavingPref] = useState(false);
-
- useEffect(() => {
- setSelectedPrefId(preferences.image_generation_config_id ?? "");
- setHasPrefChanges(false);
- }, [preferences]);
-
const isSubmitting = isCreating || isUpdating;
- const isLoading = configsLoading || globalLoading || prefsLoading;
+ const isLoading = configsLoading || globalLoading;
const errors = [createError, updateError, deleteError, fetchError].filter(Boolean) as Error[];
// Form state for create/edit dialog
@@ -291,40 +274,6 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
setIsDialogOpen(true);
};
- const handlePrefChange = (value: string) => {
- const newVal = value === "unassigned" ? "" : parseInt(value);
- setSelectedPrefId(newVal);
- setHasPrefChanges(newVal !== (preferences.image_generation_config_id ?? ""));
- };
-
- const handleSavePref = async () => {
- setIsSavingPref(true);
- try {
- await updatePreferences({
- search_space_id: searchSpaceId,
- data: {
- image_generation_config_id:
- typeof selectedPrefId === "string"
- ? selectedPrefId
- ? parseInt(selectedPrefId)
- : undefined
- : selectedPrefId,
- },
- });
- setHasPrefChanges(false);
- toast.success("Image generation model preference saved!");
- } catch {
- toast.error("Failed to save preference");
- } finally {
- setIsSavingPref(false);
- }
- };
-
- const allConfigs = [
- ...globalConfigs.map((c) => ({ ...c, _source: "global" as const })),
- ...(userConfigs ?? []).map((c) => ({ ...c, _source: "user" as const })),
- ];
-
const selectedProvider = IMAGE_GEN_PROVIDERS.find((p) => p.value === formData.provider);
const suggestedModels = getImageGenModelsByProvider(formData.provider);
@@ -342,6 +291,14 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
Refresh
+ {canCreate && (
+
+ Add Image Model
+
+ )}
{/* Errors */}
@@ -403,135 +360,9 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)}
- {/* Active Preference Card */}
- {!isLoading && allConfigs.length > 0 && (
-
-
-
-
-
-
-
-
- Active Image Model
-
- Select which model to use for image generation
-
-
-
-
-
-
-
-
-
-
-
- Unassigned
-
- {globalConfigs.length > 0 && (
- <>
-
- Global
-
- {globalConfigs.map((c) => {
- const isAuto = "is_auto_mode" in c && c.is_auto_mode;
- return (
-
-
- {isAuto ? (
-
-
- AUTO
-
- ) : (
-
- {c.provider}
-
- )}
- {c.name}
-
-
- );
- })}
- >
- )}
- {(userConfigs?.length ?? 0) > 0 && (
- <>
-
- Your Models
-
- {userConfigs?.map((c) => (
-
-
-
- {c.provider}
-
- {c.name}
- ({c.model_name})
-
-
- ))}
- >
- )}
-
-
- {hasPrefChanges && (
-
-
- {isSavingPref ? "Saving..." : "Save"}
-
- {
- setSelectedPrefId(preferences.image_generation_config_id ?? "");
- setHasPrefChanges(false);
- }}
- className="text-xs h-8"
- >
- Reset
-
-
- )}
-
-
-
- )}
-
{/* Loading Skeleton */}
{isLoading && (
- {/* Active Preference Skeleton */}
-
-
-
-
-
-
-
-
-
{/* Your Image Models Section Skeleton */}
@@ -573,18 +404,6 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{/* User Configs */}
{!isLoading && (
-
-
Your Image Models
- {canCreate && (
-
- Add Image Model
-
- )}
-
-
{(userConfigs?.length ?? 0) === 0 ? (
diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx
index c634ffd49..6e44b8958 100644
--- a/surfsense_web/components/settings/llm-role-manager.tsx
+++ b/surfsense_web/components/settings/llm-role-manager.tsx
@@ -7,6 +7,7 @@ import {
CheckCircle,
CircleDashed,
FileText,
+ ImageIcon,
RefreshCw,
RotateCcw,
Save,
@@ -15,6 +16,10 @@ import {
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
+import {
+ globalImageGenConfigsAtom,
+ imageGenConfigsAtom,
+} from "@/atoms/image-gen-config/image-gen-config-query.atoms";
import { updateLLMPreferencesMutationAtom } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms";
import {
globalNewLLMConfigsAtom,
@@ -46,6 +51,8 @@ const ROLE_DESCRIPTIONS = {
description: "Primary LLM for chat interactions and agent operations",
color: "text-blue-600 dark:text-blue-400",
bgColor: "bg-blue-500/10",
+ prefKey: "agent_llm_id" as const,
+ configType: "llm" as const,
},
document_summary: {
icon: FileText,
@@ -53,6 +60,17 @@ const ROLE_DESCRIPTIONS = {
description: "Handles document summarization and research synthesis",
color: "text-purple-600 dark:text-purple-400",
bgColor: "bg-purple-500/10",
+ prefKey: "document_summary_llm_id" as const,
+ configType: "llm" as const,
+ },
+ image_generation: {
+ icon: ImageIcon,
+ title: "Image Generation Model",
+ description: "Model used for AI image generation (DALL-E, GPT Image, etc.)",
+ color: "text-teal-600 dark:text-teal-400",
+ bgColor: "bg-teal-500/10",
+ prefKey: "image_generation_config_id" as const,
+ configType: "image" as const,
},
};
@@ -61,6 +79,7 @@ interface LLMRoleManagerProps {
}
export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
+ // LLM configs
const {
data: newLLMConfigs = [],
isFetching: configsLoading,
@@ -72,6 +91,20 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
isFetching: globalConfigsLoading,
error: globalConfigsError,
} = useAtomValue(globalNewLLMConfigsAtom);
+
+ // Image gen configs
+ const {
+ data: userImageConfigs = [],
+ isFetching: imageConfigsLoading,
+ error: imageConfigsError,
+ } = useAtomValue(imageGenConfigsAtom);
+ const {
+ data: globalImageConfigs = [],
+ isFetching: globalImageConfigsLoading,
+ error: globalImageConfigsError,
+ } = useAtomValue(globalImageGenConfigsAtom);
+
+ // Preferences
const {
data: preferences = {},
isFetching: preferencesLoading,
@@ -83,6 +116,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
const [assignments, setAssignments] = useState({
agent_llm_id: preferences.agent_llm_id ?? "",
document_summary_llm_id: preferences.document_summary_llm_id ?? "",
+ image_generation_config_id: preferences.image_generation_config_id ?? "",
});
const [hasChanges, setHasChanges] = useState(false);
@@ -92,15 +126,16 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
const newAssignments = {
agent_llm_id: preferences.agent_llm_id ?? "",
document_summary_llm_id: preferences.document_summary_llm_id ?? "",
+ image_generation_config_id: preferences.image_generation_config_id ?? "",
};
setAssignments(newAssignments);
setHasChanges(false);
}, [preferences]);
- const handleRoleAssignment = (role: string, configId: string) => {
+ const handleRoleAssignment = (prefKey: string, configId: string) => {
const newAssignments = {
...assignments,
- [role]: configId === "unassigned" ? "" : parseInt(configId),
+ [prefKey]: configId === "unassigned" ? "" : parseInt(configId),
};
setAssignments(newAssignments);
@@ -108,6 +143,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
const currentPrefs = {
agent_llm_id: preferences.agent_llm_id ?? "",
document_summary_llm_id: preferences.document_summary_llm_id ?? "",
+ image_generation_config_id: preferences.image_generation_config_id ?? "",
};
const hasChangesNow = Object.keys(newAssignments).some(
@@ -122,19 +158,13 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
const handleSave = async () => {
setIsSaving(true);
+ const toNumericOrUndefined = (val: string | number) =>
+ typeof val === "string" ? (val ? parseInt(val) : undefined) : val;
+
const numericAssignments = {
- agent_llm_id:
- typeof assignments.agent_llm_id === "string"
- ? assignments.agent_llm_id
- ? parseInt(assignments.agent_llm_id)
- : undefined
- : assignments.agent_llm_id,
- document_summary_llm_id:
- typeof assignments.document_summary_llm_id === "string"
- ? assignments.document_summary_llm_id
- ? parseInt(assignments.document_summary_llm_id)
- : undefined
- : assignments.document_summary_llm_id,
+ agent_llm_id: toNumericOrUndefined(assignments.agent_llm_id),
+ document_summary_llm_id: toNumericOrUndefined(assignments.document_summary_llm_id),
+ image_generation_config_id: toNumericOrUndefined(assignments.image_generation_config_id),
};
await updatePreferences({
@@ -143,7 +173,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
});
setHasChanges(false);
- toast.success("LLM role assignments saved successfully!");
+ toast.success("Role assignments saved successfully!");
setIsSaving(false);
};
@@ -152,6 +182,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
setAssignments({
agent_llm_id: preferences.agent_llm_id ?? "",
document_summary_llm_id: preferences.document_summary_llm_id ?? "",
+ image_generation_config_id: preferences.image_generation_config_id ?? "",
});
setHasChanges(false);
};
@@ -162,16 +193,26 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
assignments.agent_llm_id !== undefined &&
assignments.document_summary_llm_id !== "" &&
assignments.document_summary_llm_id !== null &&
- assignments.document_summary_llm_id !== undefined;
+ assignments.document_summary_llm_id !== undefined &&
+ assignments.image_generation_config_id !== "" &&
+ assignments.image_generation_config_id !== null &&
+ assignments.image_generation_config_id !== undefined;
- // Combine global and custom configs
- const allConfigs = [
+ // Combine global and custom LLM configs
+ const allLLMConfigs = [
...globalConfigs.map((config) => ({ ...config, is_global: true })),
...newLLMConfigs.filter((config) => config.id && config.id.toString().trim() !== ""),
];
- const isLoading = configsLoading || preferencesLoading || globalConfigsLoading;
- const hasError = configsError || preferencesError || globalConfigsError;
+ // Combine global and custom image gen configs
+ const allImageConfigs = [
+ ...globalImageConfigs.map((config) => ({ ...config, is_global: true })),
+ ...(userImageConfigs ?? []).filter((config) => config.id && config.id.toString().trim() !== ""),
+ ];
+
+ const isLoading = configsLoading || preferencesLoading || globalConfigsLoading || imageConfigsLoading || globalImageConfigsLoading;
+ const hasError = configsError || preferencesError || globalConfigsError || imageConfigsError || globalImageConfigsError;
+ const hasAnyConfigs = allLLMConfigs.length > 0 || allImageConfigs.length > 0;
return (
@@ -223,7 +264,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{/* Loading Skeleton */}
{isLoading && (
- {["skeleton-a", "skeleton-b"].map((key) => (
+ {["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
{/* Header: icon + title + status */}
@@ -260,18 +301,18 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
)}
{/* No configs warning */}
- {!isLoading && !hasError && allConfigs.length === 0 && (
+ {!isLoading && !hasError && !hasAnyConfigs && (
- No LLM configurations found. Please add at least one LLM provider in the
- Agent Configs tab before assigning roles.
+ No configurations found. Please add at least one LLM provider or image model
+ in the respective settings tabs before assigning roles.
)}
{/* Role Assignment Cards */}
- {!isLoading && !hasError && allConfigs.length > 0 && (
+ {!isLoading && !hasError && hasAnyConfigs && (
{Object.entries(ROLE_DESCRIPTIONS).map(([key, role], index) => {
const IconComponent = role.icon;
+ const isImageRole = role.configType === "image";
const currentAssignment =
- assignments[`${key}_llm_id` as keyof typeof assignments];
- const assignedConfig = allConfigs.find(
+ assignments[role.prefKey as keyof typeof assignments];
+
+ // Pick the right config lists based on role type
+ const roleGlobalConfigs = isImageRole ? globalImageConfigs : globalConfigs;
+ const roleUserConfigs = isImageRole
+ ? (userImageConfigs ?? []).filter((c) => c.id && c.id.toString().trim() !== "")
+ : newLLMConfigs.filter((c) => c.id && c.id.toString().trim() !== "");
+ const roleAllConfigs = isImageRole ? allImageConfigs : allLLMConfigs;
+
+ const assignedConfig = roleAllConfigs.find(
(config) => config.id === currentAssignment
);
const isAssigned =
@@ -340,7 +390,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
- handleRoleAssignment(`${key}_llm_id`, value)
+ handleRoleAssignment(role.prefKey, value)
}
>
@@ -357,12 +407,12 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{/* Global Configurations */}
- {globalConfigs.length > 0 && (
+ {roleGlobalConfigs.length > 0 && (
Global Configurations
- {globalConfigs.map((config) => {
+ {roleGlobalConfigs.map((config) => {
const isAuto =
"is_auto_mode" in config &&
config.is_auto_mode;
@@ -412,20 +462,12 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
)}
{/* Custom Configurations */}
- {newLLMConfigs.length > 0 && (
+ {roleUserConfigs.length > 0 && (
Your Configurations
- {newLLMConfigs
- .filter(
- (config) =>
- config.id &&
- config.id
- .toString()
- .trim() !== ""
- )
- .map((config) => (
+ {roleUserConfigs.map((config) => (
) : (
-
+
From b44b4497a6a2d6f6573a6ec9bcd40c2d9107e60a Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 10 Feb 2026 19:04:42 +0530
Subject: [PATCH 27/35] refactor: remove unused onDelete and onSettings props
from MobileSidebar component
---
surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx | 4 ----
1 file changed, 4 deletions(-)
diff --git a/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx
index 377bf65f5..567236498 100644
--- a/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx
@@ -113,10 +113,6 @@ export function MobileSidebar({
isShared={space.memberCount > 1}
isOwner={space.isOwner}
onClick={() => handleSearchSpaceSelect(space.id)}
- onDelete={onSearchSpaceDelete ? () => onSearchSpaceDelete(space) : undefined}
- onSettings={
- onSearchSpaceSettings ? () => onSearchSpaceSettings(space) : undefined
- }
size="md"
disableTooltip
/>
From f1ce17bde4e67d8d3ab3f8928e46bc91c74cd8b8 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 10 Feb 2026 19:06:21 +0530
Subject: [PATCH 28/35] chore: ran linting
---
...96_add_user_id_to_llm_and_image_configs.py | 1 -
.../app/services/public_chat_service.py | 4 +-
.../connector_indexers/discord_indexer.py | 4 +-
.../tasks/connector_indexers/slack_indexer.py | 40 ++---
.../(manage)/components/RowActions.tsx | 6 +-
.../components/new-chat/model-selector.tsx | 53 +++---
.../public-chat-snapshot-row.tsx | 96 +++++------
.../public-chat-snapshots-manager.tsx | 14 +-
.../settings/image-model-manager.tsx | 123 +++++++-------
.../components/settings/llm-role-manager.tsx | 152 ++++++++----------
.../settings/model-config-manager.tsx | 108 ++++++-------
.../contracts/types/new-llm-config.types.ts | 4 +-
surfsense_web/lib/provider-icons.tsx | 11 +-
surfsense_web/next.config.ts | 6 +-
surfsense_web/svgr.d.ts | 1 -
15 files changed, 285 insertions(+), 338 deletions(-)
diff --git a/surfsense_backend/alembic/versions/96_add_user_id_to_llm_and_image_configs.py b/surfsense_backend/alembic/versions/96_add_user_id_to_llm_and_image_configs.py
index 7f600a9e3..06983f510 100644
--- a/surfsense_backend/alembic/versions/96_add_user_id_to_llm_and_image_configs.py
+++ b/surfsense_backend/alembic/versions/96_add_user_id_to_llm_and_image_configs.py
@@ -141,4 +141,3 @@ def downgrade() -> None:
ALTER TABLE image_generation_configs DROP COLUMN IF EXISTS user_id;
"""
)
-
diff --git a/surfsense_backend/app/services/public_chat_service.py b/surfsense_backend/app/services/public_chat_service.py
index 9088ed748..ba2dd0079 100644
--- a/surfsense_backend/app/services/public_chat_service.py
+++ b/surfsense_backend/app/services/public_chat_service.py
@@ -439,7 +439,9 @@ async def list_snapshots_for_search_space(
"message_count": len(s.message_ids) if s.message_ids else 0,
"thread_id": s.thread_id,
"thread_title": thread_titles.get(s.thread_id, "Untitled"),
- "created_by_user_id": str(s.created_by_user_id) if s.created_by_user_id else None,
+ "created_by_user_id": str(s.created_by_user_id)
+ if s.created_by_user_id
+ else None,
}
for s in snapshots
]
diff --git a/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py b/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py
index 98b798452..8769d03c5 100644
--- a/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py
+++ b/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py
@@ -651,9 +651,7 @@ async def index_discord_messages(
# PHASE 2: Process each batch document one by one
# Each document transitions: pending → processing → ready/failed
# =======================================================================
- logger.info(
- f"Phase 2: Processing {len(batches_to_process)} batch documents"
- )
+ logger.info(f"Phase 2: Processing {len(batches_to_process)} batch documents")
for item in batches_to_process:
# Send heartbeat periodically
diff --git a/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py b/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py
index decdc7b37..01771d2ac 100644
--- a/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py
+++ b/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py
@@ -357,9 +357,7 @@ async def index_slack_messages(
# Group messages into batches of SLACK_BATCH_SIZE
# Each batch becomes a single document with conversation context
# =======================================================
- for batch_start in range(
- 0, len(formatted_messages), SLACK_BATCH_SIZE
- ):
+ for batch_start in range(0, len(formatted_messages), SLACK_BATCH_SIZE):
batch = formatted_messages[
batch_start : batch_start + SLACK_BATCH_SIZE
]
@@ -377,9 +375,7 @@ async def index_slack_messages(
# channel_id + first message ts + last message ts
first_msg_ts = batch[0].get("timestamp", "")
last_msg_ts = batch[-1].get("timestamp", "")
- unique_identifier = (
- f"{channel_id}_{first_msg_ts}_{last_msg_ts}"
- )
+ unique_identifier = f"{channel_id}_{first_msg_ts}_{last_msg_ts}"
unique_identifier_hash = generate_unique_identifier_hash(
DocumentType.SLACK_CONNECTOR,
unique_identifier,
@@ -392,10 +388,8 @@ async def index_slack_messages(
)
# Check if document with this unique identifier already exists
- existing_document = (
- await check_document_by_unique_identifier(
- session, unique_identifier_hash
- )
+ existing_document = await check_document_by_unique_identifier(
+ session, unique_identifier_hash
)
if existing_document:
@@ -405,9 +399,7 @@ async def index_slack_messages(
if not DocumentStatus.is_state(
existing_document.status, DocumentStatus.READY
):
- existing_document.status = (
- DocumentStatus.ready()
- )
+ existing_document.status = DocumentStatus.ready()
documents_skipped += 1
continue
@@ -440,10 +432,8 @@ async def index_slack_messages(
# Document doesn't exist by unique_identifier_hash
# Check if a document with the same content_hash exists (from another connector)
with session.no_autoflush:
- duplicate_by_content = (
- await check_duplicate_document_by_hash(
- session, content_hash
- )
+ duplicate_by_content = await check_duplicate_document_by_hash(
+ session, content_hash
)
if duplicate_by_content:
@@ -496,12 +486,8 @@ async def index_slack_messages(
"channel_id": channel_id,
"first_message_ts": first_msg_ts,
"last_message_ts": last_msg_ts,
- "first_message_time": batch[0].get(
- "datetime", "Unknown"
- ),
- "last_message_time": batch[-1].get(
- "datetime", "Unknown"
- ),
+ "first_message_time": batch[0].get("datetime", "Unknown"),
+ "last_message_time": batch[-1].get("datetime", "Unknown"),
"message_count": len(batch),
"start_date": start_date_str,
"end_date": end_date_str,
@@ -538,9 +524,7 @@ async def index_slack_messages(
# PHASE 2: Process each batch document one by one
# Each document transitions: pending → processing → ready/failed
# =======================================================================
- logger.info(
- f"Phase 2: Processing {len(batches_to_process)} batch documents"
- )
+ logger.info(f"Phase 2: Processing {len(batches_to_process)} batch documents")
for item in batches_to_process:
# Send heartbeat periodically
@@ -621,9 +605,7 @@ async def index_slack_messages(
)
try:
await session.commit()
- logger.info(
- "Successfully committed all Slack document changes to database"
- )
+ logger.info("Successfully committed all Slack document changes to database")
except Exception as e:
# Handle any remaining integrity errors gracefully (race conditions, etc.)
if (
diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx
index ffb763c6b..137c02f27 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx
@@ -51,8 +51,7 @@ export function RowActions({
document.status?.state === "pending" || document.status?.state === "processing";
// FILE documents that failed processing cannot be edited
- const isFileFailed =
- document.document_type === "FILE" && document.status?.state === "failed";
+ const isFileFailed = document.document_type === "FILE" && document.status?.state === "failed";
// SURFSENSE_DOCS are system-managed and should not show delete at all
const shouldShowDelete = !NON_DELETABLE_DOCUMENT_TYPES.includes(
@@ -212,7 +211,8 @@ export function RowActions({
Delete document?
- This action cannot be undone. This will permanently delete this document from your search space.
+ This action cannot be undone. This will permanently delete this document from your
+ search space.
diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx
index d27594ee6..01a926a5a 100644
--- a/surfsense_web/components/new-chat/model-selector.tsx
+++ b/surfsense_web/components/new-chat/model-selector.tsx
@@ -1,15 +1,7 @@
"use client";
import { useAtomValue } from "jotai";
-import {
- Bot,
- Check,
- ChevronDown,
- Edit3,
- ImageIcon,
- Plus,
- Zap,
-} from "lucide-react";
+import { Bot, Check, ChevronDown, Edit3, ImageIcon, Plus, Zap } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import {
@@ -77,10 +69,10 @@ export function ModelSelector({
// Image data
const { data: imageGlobalConfigs, isLoading: imageGlobalLoading } =
useAtomValue(globalImageGenConfigsAtom);
- const { data: imageUserConfigs, isLoading: imageUserLoading } =
- useAtomValue(imageGenConfigsAtom);
+ const { data: imageUserConfigs, isLoading: imageUserLoading } = useAtomValue(imageGenConfigsAtom);
- const isLoading = llmUserLoading || llmGlobalLoading || prefsLoading || imageGlobalLoading || imageUserLoading;
+ const isLoading =
+ llmUserLoading || llmGlobalLoading || prefsLoading || imageGlobalLoading || imageUserLoading;
// ─── LLM current config ───
const currentLLMConfig = useMemo(() => {
@@ -108,7 +100,9 @@ export function ModelSelector({
}, [preferences, imageGlobalConfigs, imageUserConfigs]);
const isImageAutoMode = useMemo(() => {
- return currentImageConfig && "is_auto_mode" in currentImageConfig && currentImageConfig.is_auto_mode;
+ return (
+ currentImageConfig && "is_auto_mode" in currentImageConfig && currentImageConfig.is_auto_mode
+ );
}, [currentImageConfig]);
// ─── LLM filtering ───
@@ -244,7 +238,9 @@ export function ModelSelector({
{/* LLM section */}
{currentLLMConfig ? (
<>
- {getProviderIcon(currentLLMConfig.provider, { isAutoMode: isLLMAutoMode ?? false })}
+ {getProviderIcon(currentLLMConfig.provider, {
+ isAutoMode: isLLMAutoMode ?? false,
+ })}
{currentLLMConfig.name}
@@ -262,7 +258,9 @@ export function ModelSelector({
{/* Image section */}
{currentImageConfig ? (
<>
- {getProviderIcon(currentImageConfig.provider, { isAutoMode: isImageAutoMode ?? false })}
+ {getProviderIcon(currentImageConfig.provider, {
+ isAutoMode: isImageAutoMode ?? false,
+ })}
{currentImageConfig.name}
@@ -373,7 +371,9 @@ export function ModelSelector({
Recommended
)}
- {isSelected && }
+ {isSelected && (
+
+ )}
@@ -436,7 +436,9 @@ export function ModelSelector({
{config.name}
- {isSelected && }
+ {isSelected && (
+
+ )}
@@ -489,7 +491,10 @@ export function ModelSelector({
{/* ─── Image Tab ─── */}
-
+
{totalImageModels > 3 && (
0 && (
<>
- {filteredImageGlobal.length > 0 && }
+ {filteredImageGlobal.length > 0 && (
+
+ )}
Your Image Models
@@ -591,13 +598,13 @@ export function ModelSelector({
)}
>
-
- {getProviderIcon(config.provider)}
-
+
{getProviderIcon(config.provider)}
{config.name}
- {isSelected && }
+ {isSelected && (
+
+ )}
{config.model_name}
diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx
index 1e7de9f23..568c52ded 100644
--- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx
+++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx
@@ -50,9 +50,7 @@ export function PublicChatSnapshotRow({
day: "numeric",
});
- const member = snapshot.created_by_user_id
- ? memberMap.get(snapshot.created_by_user_id)
- : null;
+ const member = snapshot.created_by_user_id ? memberMap.get(snapshot.created_by_user_id) : null;
return (
@@ -77,11 +75,7 @@ export function PublicChatSnapshotRow({
asChild
className="h-7 w-7 text-muted-foreground hover:text-foreground"
>
-
+
@@ -110,51 +104,49 @@ export function PublicChatSnapshotRow({
- {/* Message count badge */}
-
-
-
- {snapshot.message_count} messages
-
-
+ {/* Message count badge */}
+
+
+
+ {snapshot.message_count} messages
+
+
- {/* Public URL – selectable fallback for manual copy */}
-
-
- {snapshot.public_url}
-
-
-
-
-
- {copied ? (
-
- ) : (
-
- )}
-
-
- {copied ? "Copied!" : "Copy link"}
-
-
-
+ {/* Public URL – selectable fallback for manual copy */}
+
+
+ {snapshot.public_url}
+
+
+
+
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+ {copied ? "Copied!" : "Copy link"}
+
+
+
- {/* Footer: Date + Creator */}
+ {/* Footer: Date + Creator */}
-
- {formattedDate}
-
+ {formattedDate}
{member && (
<>
·
@@ -182,9 +174,7 @@ export function PublicChatSnapshotRow({
-
- {member.email || member.name}
-
+
{member.email || member.name}
>
diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx
index 5b872404d..24d801409 100644
--- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx
+++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx
@@ -97,13 +97,13 @@ export function PublicChatSnapshotsManager({
- {/* Message count badge */}
-
-
-
- {/* URL skeleton */}
-
- {/* Footer: Date + Creator */}
+ {/* Message count badge */}
+
+
+
+ {/* URL skeleton */}
+
+ {/* Footer: Date + Creator */}
diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx
index f2d010e10..ef1a20068 100644
--- a/surfsense_web/components/settings/image-model-manager.tsx
+++ b/surfsense_web/components/settings/image-model-manager.tsx
@@ -348,16 +348,16 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{/* Global info */}
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && (
-
-
-
-
- {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length} global
- image model(s)
- {" "}
- available from your administrator.
-
-
+
+
+
+
+ {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length} global
+ image model(s)
+ {" "}
+ available from your administrator.
+
+
)}
{/* Loading Skeleton */}
@@ -417,7 +417,11 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
: "No image models have been added to this space yet. Contact a space owner to add one."}
{canCreate && (
-
+
Add First Image Model
@@ -457,43 +461,43 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)}
{(canUpdate || canDelete) && (
-
- {canUpdate && (
-
-
-
- openEditDialog(config)}
- className="h-7 w-7 text-muted-foreground hover:text-foreground"
- >
-
-
-
- Edit
-
-
- )}
- {canDelete && (
-
-
-
- setConfigToDelete(config)}
- className="h-7 w-7 text-muted-foreground hover:text-destructive"
- >
-
-
-
- Delete
-
-
- )}
-
- )}
+
+ {canUpdate && (
+
+
+
+ openEditDialog(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-foreground"
+ >
+
+
+
+ Edit
+
+
+ )}
+ {canDelete && (
+
+
+
+ setConfigToDelete(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
+ >
+
+
+
+ Delete
+
+
+ )}
+
+ )}
{/* Provider + Model */}
@@ -507,14 +511,11 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{/* Footer: Date + Creator */}
- {new Date(config.created_at).toLocaleDateString(
- undefined,
- {
- year: "numeric",
- month: "short",
- day: "numeric",
- }
- )}
+ {new Date(config.created_at).toLocaleDateString(undefined, {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ })}
{member && (
<>
@@ -574,13 +575,11 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
}}
>
e.preventDefault()}
- >
+ className="max-w-lg max-h-[90vh] overflow-y-auto"
+ onOpenAutoFocus={(e) => e.preventDefault()}
+ >
-
- {editingConfig ? "Edit Image Model" : "Add Image Model"}
-
+ {editingConfig ? "Edit Image Model" : "Add Image Model"}
{editingConfig
? "Update your image generation model"
diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx
index 6e44b8958..cdc84d400 100644
--- a/surfsense_web/components/settings/llm-role-manager.tsx
+++ b/surfsense_web/components/settings/llm-role-manager.tsx
@@ -210,8 +210,18 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
...(userImageConfigs ?? []).filter((config) => config.id && config.id.toString().trim() !== ""),
];
- const isLoading = configsLoading || preferencesLoading || globalConfigsLoading || imageConfigsLoading || globalImageConfigsLoading;
- const hasError = configsError || preferencesError || globalConfigsError || imageConfigsError || globalImageConfigsError;
+ const isLoading =
+ configsLoading ||
+ preferencesLoading ||
+ globalConfigsLoading ||
+ imageConfigsLoading ||
+ globalImageConfigsLoading;
+ const hasError =
+ configsError ||
+ preferencesError ||
+ globalConfigsError ||
+ imageConfigsError ||
+ globalImageConfigsError;
const hasAnyConfigs = allLLMConfigs.length > 0 || allImageConfigs.length > 0;
return (
@@ -253,8 +263,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{(configsError?.message ?? "Failed to load LLM configurations") ||
(preferencesError?.message ?? "Failed to load preferences") ||
- (globalConfigsError?.message ??
- "Failed to load global configurations")}
+ (globalConfigsError?.message ?? "Failed to load global configurations")}
@@ -305,8 +314,8 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
- No configurations found. Please add at least one LLM provider or image model
- in the respective settings tabs before assigning roles.
+ No configurations found. Please add at least one LLM provider or image model in the
+ respective settings tabs before assigning roles.
)}
@@ -322,8 +331,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{Object.entries(ROLE_DESCRIPTIONS).map(([key, role], index) => {
const IconComponent = role.icon;
const isImageRole = role.configType === "image";
- const currentAssignment =
- assignments[role.prefKey as keyof typeof assignments];
+ const currentAssignment = assignments[role.prefKey as keyof typeof assignments];
// Pick the right config lists based on role type
const roleGlobalConfigs = isImageRole ? globalImageConfigs : globalConfigs;
@@ -332,17 +340,13 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
: newLLMConfigs.filter((c) => c.id && c.id.toString().trim() !== "");
const roleAllConfigs = isImageRole ? allImageConfigs : allLLMConfigs;
- const assignedConfig = roleAllConfigs.find(
- (config) => config.id === currentAssignment
- );
+ const assignedConfig = roleAllConfigs.find((config) => config.id === currentAssignment);
const isAssigned =
currentAssignment !== "" &&
currentAssignment !== null &&
currentAssignment !== undefined;
const isAutoMode =
- assignedConfig &&
- "is_auto_mode" in assignedConfig &&
- assignedConfig.is_auto_mode;
+ assignedConfig && "is_auto_mode" in assignedConfig && assignedConfig.is_auto_mode;
return (
-
+
-
- {role.title}
-
+
{role.title}
{role.description}
@@ -389,9 +389,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
- handleRoleAssignment(role.prefKey, value)
- }
+ onValueChange={(value) => handleRoleAssignment(role.prefKey, value)}
>
@@ -401,9 +399,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
value="unassigned"
className="text-xs md:text-sm py-1.5 md:py-2"
>
-
- Unassigned
-
+ Unassigned
{/* Global Configurations */}
@@ -413,9 +409,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
Global Configurations
{roleGlobalConfigs.map((config) => {
- const isAuto =
- "is_auto_mode" in config &&
- config.is_auto_mode;
+ const isAuto = "is_auto_mode" in config && config.is_auto_mode;
return (
) : (
- getProviderIcon(config.provider, { className: "size-3 md:size-3.5 shrink-0" })
+ getProviderIcon(config.provider, {
+ className: "size-3 md:size-3.5 shrink-0",
+ })
)}
{config.name}
{!isAuto && (
- (
- {
- config.model_name
- }
- )
+ ({config.model_name})
)}
{isAuto && (
@@ -468,42 +460,40 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
Your Configurations
{roleUserConfigs.map((config) => (
-
-
- {getProviderIcon(config.provider, { className: "size-3 md:size-3.5 shrink-0" })}
-
- {config.name}
-
-
- (
- {
- config.model_name
- }
- )
-
-
-
- ))}
+
+
+ {getProviderIcon(config.provider, {
+ className: "size-3 md:size-3.5 shrink-0",
+ })}
+
+ {config.name}
+
+
+ ({config.model_name})
+
+
+
+ ))}
)}
- {/* Assigned Config Summary */}
- {assignedConfig && (
-
+ {/* Assigned Config Summary */}
+ {assignedConfig && (
+
{isAutoMode ? (
-
- {assignedConfig.name}
-
- {"is_global" in assignedConfig &&
- assignedConfig.is_global && (
-
- 🌐 Global
-
- )}
+ {assignedConfig.name}
+ {"is_global" in assignedConfig && assignedConfig.is_global && (
+
+ 🌐 Global
+
+ )}
- {getProviderIcon(assignedConfig.provider, { className: "size-3 shrink-0" })}
+ {getProviderIcon(assignedConfig.provider, {
+ className: "size-3 shrink-0",
+ })}
{assignedConfig.model_name}
@@ -552,9 +538,9 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
)}
-
- )}
-
+
+ )}
+
);
@@ -572,9 +558,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
transition={{ duration: 0.2 }}
className="flex items-center justify-between gap-3 rounded-lg border border-border bg-muted/50 p-3 md:p-4"
>
-
- You have unsaved changes
-
+
You have unsaved changes
{(canUpdate || canDelete) && (
-
- {canUpdate && (
-
-
-
- openEditDialog(config)}
- className="h-7 w-7 text-muted-foreground hover:text-foreground"
- >
-
-
-
- Edit
-
-
- )}
- {canDelete && (
-
-
-
- setConfigToDelete(config)}
- className="h-7 w-7 text-muted-foreground hover:text-destructive"
- >
-
-
-
- Delete
-
-
- )}
-
- )}
+
+ {canUpdate && (
+
+
+
+ openEditDialog(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-foreground"
+ >
+
+
+
+ Edit
+
+
+ )}
+ {canDelete && (
+
+
+
+ setConfigToDelete(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
+ >
+
+
+
+ Delete
+
+
+ )}
+
+ )}
{/* Provider + Model */}
@@ -453,14 +450,11 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
{/* Footer: Date + Creator */}
- {new Date(config.created_at).toLocaleDateString(
- undefined,
- {
- year: "numeric",
- month: "short",
- day: "numeric",
- }
- )}
+ {new Date(config.created_at).toLocaleDateString(undefined, {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ })}
{member && (
<>
diff --git a/surfsense_web/contracts/types/new-llm-config.types.ts b/surfsense_web/contracts/types/new-llm-config.types.ts
index 7b3fca8b0..0885fa7f5 100644
--- a/surfsense_web/contracts/types/new-llm-config.types.ts
+++ b/surfsense_web/contracts/types/new-llm-config.types.ts
@@ -218,7 +218,9 @@ export const getImageGenConfigsResponse = z.array(imageGenerationConfig);
export const updateImageGenConfigRequest = z.object({
id: z.number(),
- data: imageGenerationConfig.omit({ id: true, created_at: true, search_space_id: true, user_id: true }).partial(),
+ data: imageGenerationConfig
+ .omit({ id: true, created_at: true, search_space_id: true, user_id: true })
+ .partial(),
});
export const updateImageGenConfigResponse = imageGenerationConfig;
diff --git a/surfsense_web/lib/provider-icons.tsx b/surfsense_web/lib/provider-icons.tsx
index 4a32f0df0..11cef5bce 100644
--- a/surfsense_web/lib/provider-icons.tsx
+++ b/surfsense_web/lib/provider-icons.tsx
@@ -1,7 +1,4 @@
-import {
- Bot,
- Shuffle,
-} from "lucide-react";
+import { Bot, Shuffle } from "lucide-react";
import { cn } from "@/lib/utils";
import { Ai21Icon } from "@/components/icons/providers";
import { AnthropicIcon } from "@/components/icons/providers";
@@ -41,10 +38,7 @@ import { ZhipuIcon } from "@/components/icons/providers";
*/
export function getProviderIcon(
provider: string,
- {
- isAutoMode,
- className = "size-4",
- }: { isAutoMode?: boolean; className?: string } = {}
+ { isAutoMode, className = "size-4" }: { isAutoMode?: boolean; className?: string } = {}
) {
if (isAutoMode || provider?.toUpperCase() === "AUTO") {
return
;
@@ -123,4 +117,3 @@ export function getProviderIcon(
return
;
}
}
-
diff --git a/surfsense_web/next.config.ts b/surfsense_web/next.config.ts
index 55a9296fd..3278b9f3d 100644
--- a/surfsense_web/next.config.ts
+++ b/surfsense_web/next.config.ts
@@ -41,9 +41,7 @@ const nextConfig: NextConfig = {
}
// SVGR: import *.svg as React components
- const fileLoaderRule = config.module.rules.find(
- (rule: any) => rule.test?.test?.(".svg"),
- );
+ const fileLoaderRule = config.module.rules.find((rule: any) => rule.test?.test?.(".svg"));
config.module.rules.push(
// Re-apply the existing file loader for *.svg?url imports
{
@@ -57,7 +55,7 @@ const nextConfig: NextConfig = {
issuer: fileLoaderRule.issuer,
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] },
use: ["@svgr/webpack"],
- },
+ }
);
fileLoaderRule.exclude = /\.svg$/i;
diff --git a/surfsense_web/svgr.d.ts b/surfsense_web/svgr.d.ts
index 79922fb0f..ada7f47c5 100644
--- a/surfsense_web/svgr.d.ts
+++ b/surfsense_web/svgr.d.ts
@@ -3,4 +3,3 @@ declare module "*.svg" {
const content: FC
>;
export default content;
}
-
From bb4ed3cac69b5ba868231c0eac8b684524032f3d Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 10 Feb 2026 23:04:15 +0530
Subject: [PATCH 29/35] refactor: update configuration names from "Auto (Load
Balanced)" to "Auto (Fastest)" across multiple components for consistency and
clarity
---
surfsense_backend/app/agents/new_chat/llm_config.py | 2 +-
surfsense_backend/app/routes/image_generation_routes.py | 4 ++--
surfsense_backend/app/routes/new_llm_config_routes.py | 2 +-
surfsense_backend/app/routes/search_spaces_routes.py | 4 ++--
surfsense_backend/app/services/llm_service.py | 2 +-
.../components/new-chat/image-config-sidebar.tsx | 2 +-
.../components/new-chat/model-config-sidebar.tsx | 4 ++--
surfsense_web/components/new-chat/model-selector.tsx | 8 ++++----
surfsense_web/components/settings/llm-role-manager.tsx | 2 +-
9 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/surfsense_backend/app/agents/new_chat/llm_config.py b/surfsense_backend/app/agents/new_chat/llm_config.py
index 12b389a2d..8339e9eff 100644
--- a/surfsense_backend/app/agents/new_chat/llm_config.py
+++ b/surfsense_backend/app/agents/new_chat/llm_config.py
@@ -108,7 +108,7 @@ class AgentConfig:
use_default_system_instructions=True,
citations_enabled=True,
config_id=AUTO_MODE_ID,
- config_name="Auto (Load Balanced)",
+ config_name="Auto (Fastest)",
is_auto_mode=True,
)
diff --git a/surfsense_backend/app/routes/image_generation_routes.py b/surfsense_backend/app/routes/image_generation_routes.py
index a8963e181..97a3559b9 100644
--- a/surfsense_backend/app/routes/image_generation_routes.py
+++ b/surfsense_backend/app/routes/image_generation_routes.py
@@ -69,7 +69,7 @@ def _get_global_image_gen_config(config_id: int) -> dict | None:
if config_id == IMAGE_GEN_AUTO_MODE_ID:
return {
"id": IMAGE_GEN_AUTO_MODE_ID,
- "name": "Auto (Load Balanced)",
+ "name": "Auto (Fastest)",
"provider": "AUTO",
"model_name": "auto",
"is_auto_mode": True,
@@ -215,7 +215,7 @@ async def get_global_image_gen_configs(
safe_configs.append(
{
"id": 0,
- "name": "Auto (Load Balanced)",
+ "name": "Auto (Fastest)",
"description": "Automatically routes across available image generation providers.",
"provider": "AUTO",
"custom_provider": None,
diff --git a/surfsense_backend/app/routes/new_llm_config_routes.py b/surfsense_backend/app/routes/new_llm_config_routes.py
index f90e86594..f784bd273 100644
--- a/surfsense_backend/app/routes/new_llm_config_routes.py
+++ b/surfsense_backend/app/routes/new_llm_config_routes.py
@@ -64,7 +64,7 @@ async def get_global_new_llm_configs(
safe_configs.append(
{
"id": 0,
- "name": "Auto (Load Balanced)",
+ "name": "Auto (Fastest)",
"description": "Automatically routes requests across available LLM providers for optimal performance and rate limit handling. Recommended for most users.",
"provider": "AUTO",
"custom_provider": None,
diff --git a/surfsense_backend/app/routes/search_spaces_routes.py b/surfsense_backend/app/routes/search_spaces_routes.py
index fd84c0f45..d115c31e2 100644
--- a/surfsense_backend/app/routes/search_spaces_routes.py
+++ b/surfsense_backend/app/routes/search_spaces_routes.py
@@ -324,7 +324,7 @@ async def _get_llm_config_by_id(
if config_id == 0:
return {
"id": 0,
- "name": "Auto (Load Balanced)",
+ "name": "Auto (Fastest)",
"description": "Automatically routes requests across available LLM providers for optimal performance and rate limit handling",
"provider": "AUTO",
"custom_provider": None,
@@ -402,7 +402,7 @@ async def _get_image_gen_config_by_id(
if config_id == 0:
return {
"id": 0,
- "name": "Auto (Load Balanced)",
+ "name": "Auto (Fastest)",
"description": "Automatically routes requests across available image generation providers",
"provider": "AUTO",
"model_name": "auto",
diff --git a/surfsense_backend/app/services/llm_service.py b/surfsense_backend/app/services/llm_service.py
index e21259990..5af804fed 100644
--- a/surfsense_backend/app/services/llm_service.py
+++ b/surfsense_backend/app/services/llm_service.py
@@ -41,7 +41,7 @@ def get_global_llm_config(llm_config_id: int) -> dict | None:
if llm_config_id == AUTO_MODE_ID:
return {
"id": AUTO_MODE_ID,
- "name": "Auto (Load Balanced)",
+ "name": "Auto (Fastest)",
"description": "Automatically routes requests across available LLM providers for optimal performance and rate limit handling",
"provider": "AUTO",
"model_name": "auto",
diff --git a/surfsense_web/components/new-chat/image-config-sidebar.tsx b/surfsense_web/components/new-chat/image-config-sidebar.tsx
index be84b0b22..60fa60fa4 100644
--- a/surfsense_web/components/new-chat/image-config-sidebar.tsx
+++ b/surfsense_web/components/new-chat/image-config-sidebar.tsx
@@ -129,7 +129,7 @@ export function ImageConfigSidebar({
const getTitle = () => {
if (mode === "create") return "Add Image Model";
- if (isAutoMode) return "Auto Mode (Load Balanced)";
+ if (isAutoMode) return "Auto Mode (Fastest)";
if (isGlobal) return "View Global Image Model";
return "Edit Image Model";
};
diff --git a/surfsense_web/components/new-chat/model-config-sidebar.tsx b/surfsense_web/components/new-chat/model-config-sidebar.tsx
index 3e731c164..90fb95c88 100644
--- a/surfsense_web/components/new-chat/model-config-sidebar.tsx
+++ b/surfsense_web/components/new-chat/model-config-sidebar.tsx
@@ -68,7 +68,7 @@ export function ModelConfigSidebar({
// Get title based on mode
const getTitle = () => {
if (mode === "create") return "Add New Configuration";
- if (isAutoMode) return "Auto Mode (Load Balanced)";
+ if (isAutoMode) return "Auto Mode (Fastest)";
if (isGlobal) return "View Global Configuration";
return "Edit Configuration";
};
@@ -307,7 +307,7 @@ export function ModelConfigSidebar({
- Automatic Load Balancing
+ Automatic (Fastest)
Distributes requests across all configured LLM providers
diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx
index 01a926a5a..98911fe38 100644
--- a/surfsense_web/components/new-chat/model-selector.tsx
+++ b/surfsense_web/components/new-chat/model-selector.tsx
@@ -293,14 +293,14 @@ export function ModelSelector({
LLM
Image
@@ -377,7 +377,7 @@ export function ModelSelector({
- {isAutoMode ? "Auto load balancing" : config.model_name}
+ {isAutoMode ? "Auto Mode" : config.model_name}
{!isAutoMode && config.citations_enabled && (
}
- {isAuto ? "Auto load balancing" : config.model_name}
+ {isAuto ? "Auto Mode" : config.model_name}
{onEditImage && !isAuto && (
diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx
index cdc84d400..8c6fbf4cf 100644
--- a/surfsense_web/components/settings/llm-role-manager.tsx
+++ b/surfsense_web/components/settings/llm-role-manager.tsx
@@ -503,7 +503,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
/>
- Auto Load Balanced
+ Auto Mode
Routes across all available providers
From 68bc12691c54e873b491d4d82e1e55fc35c8ff18 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 10 Feb 2026 23:04:27 +0530
Subject: [PATCH 30/35] refactor: improve Alert component styling in
ImageModelManager for better layout and readability
---
.../settings/image-model-manager.tsx | 20 +++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx
index ef1a20068..25f4e892e 100644
--- a/surfsense_web/components/settings/image-model-manager.tsx
+++ b/surfsense_web/components/settings/image-model-manager.tsx
@@ -348,16 +348,16 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{/* Global info */}
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && (
-
-
-
-
- {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length} global
- image model(s)
- {" "}
- available from your administrator.
-
-
+
+
+
+
+ {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length} global
+ image model(s)
+ {" "}
+ available from your administrator.
+
+
)}
{/* Loading Skeleton */}
From cc356010589aa0c86cd15a4482fa44a6716384d3 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 10 Feb 2026 23:06:22 +0530
Subject: [PATCH 31/35] chore: ran linting
---
.../settings/image-model-manager.tsx | 20 +++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx
index 25f4e892e..1e1354718 100644
--- a/surfsense_web/components/settings/image-model-manager.tsx
+++ b/surfsense_web/components/settings/image-model-manager.tsx
@@ -348,16 +348,16 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{/* Global info */}
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && (
-
-
-
-
- {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length} global
- image model(s)
- {" "}
- available from your administrator.
-
-
+
+
+
+
+ {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length} global
+ image model(s)
+ {" "}
+ available from your administrator.
+
+
)}
{/* Loading Skeleton */}
From c41a0849458ec414e4a10fba1c367ed951b3bd40 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Wed, 11 Feb 2026 03:48:46 +0530
Subject: [PATCH 32/35] chore: remove migration script for adding user_id to
new_llm_configs and image_generation_configs
---
...figs.py => 98_add_user_id_to_llm_and_image_configs.py} | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
rename surfsense_backend/alembic/versions/{96_add_user_id_to_llm_and_image_configs.py => 98_add_user_id_to_llm_and_image_configs.py} (97%)
diff --git a/surfsense_backend/alembic/versions/96_add_user_id_to_llm_and_image_configs.py b/surfsense_backend/alembic/versions/98_add_user_id_to_llm_and_image_configs.py
similarity index 97%
rename from surfsense_backend/alembic/versions/96_add_user_id_to_llm_and_image_configs.py
rename to surfsense_backend/alembic/versions/98_add_user_id_to_llm_and_image_configs.py
index 06983f510..07a287231 100644
--- a/surfsense_backend/alembic/versions/96_add_user_id_to_llm_and_image_configs.py
+++ b/surfsense_backend/alembic/versions/98_add_user_id_to_llm_and_image_configs.py
@@ -1,7 +1,7 @@
"""Add user_id to new_llm_configs and image_generation_configs
-Revision ID: 96
-Revises: 95
+Revision ID: 98
+Revises: 97
"""
from collections.abc import Sequence
@@ -9,8 +9,8 @@ from collections.abc import Sequence
from alembic import op
# revision identifiers, used by Alembic.
-revision: str = "96"
-down_revision: str | None = "95"
+revision: str = "98"
+down_revision: str | None = "97"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
From e1da6a61a40f97938231b8e8dc6f00d76ee8402d Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Tue, 10 Feb 2026 20:34:04 -0800
Subject: [PATCH 33/35] hotpatch(cloud): added SERVICE_ROLE and CELERY_QUEUES
for task seperation
---
surfsense_backend/Dockerfile | 17 +++
surfsense_backend/app/celery_app.py | 33 ++++
.../scripts/docker/entrypoint.sh | 143 +++++++++++++-----
3 files changed, 157 insertions(+), 36 deletions(-)
diff --git a/surfsense_backend/Dockerfile b/surfsense_backend/Dockerfile
index 9ce6467b3..b7b96b6cb 100644
--- a/surfsense_backend/Dockerfile
+++ b/surfsense_backend/Dockerfile
@@ -74,6 +74,23 @@ RUN dos2unix /app/scripts/docker/entrypoint.sh && chmod +x /app/scripts/docker/e
ENV PYTHONPATH=/app
ENV UVICORN_LOOP=asyncio
+# SERVICE_ROLE controls which process this container runs:
+# api – FastAPI backend only (runs migrations on startup)
+# worker – Celery worker only
+# beat – Celery beat scheduler only
+# all – All three (legacy / dev default)
+ENV SERVICE_ROLE=all
+
+# Celery worker tuning (only used when SERVICE_ROLE=worker or all)
+ENV CELERY_MAX_WORKERS=10
+ENV CELERY_MIN_WORKERS=2
+ENV CELERY_MAX_TASKS_PER_CHILD=50
+# CELERY_QUEUES: comma-separated queues to consume (empty = all queues)
+# "surfsense" – fast tasks only (file uploads, podcasts, etc.)
+# "surfsense.connectors" – slow connector indexing tasks only
+# "" – both queues (default, for single-worker setups)
+ENV CELERY_QUEUES=""
+
# Run
EXPOSE 8000-8001
CMD ["/app/scripts/docker/entrypoint.sh"]
\ No newline at end of file
diff --git a/surfsense_backend/app/celery_app.py b/surfsense_backend/app/celery_app.py
index af406eab7..477e5369f 100644
--- a/surfsense_backend/app/celery_app.py
+++ b/surfsense_backend/app/celery_app.py
@@ -86,6 +86,11 @@ celery_app = Celery(
],
)
+# ── Queue names ──────────────────────────────────────────────
+# Default queue : fast, user-facing tasks (file upload, podcast, reindex, …)
+# Connectors queue: slow, long-running indexing tasks (Notion, Gmail, web crawl, …)
+CONNECTORS_QUEUE = f"{CELERY_TASK_DEFAULT_QUEUE}.connectors"
+
# Celery configuration
celery_app.conf.update(
# Task settings
@@ -114,6 +119,34 @@ celery_app.conf.update(
broker_connection_retry_on_startup=True,
# Beat scheduler settings
beat_max_loop_interval=60, # Check every minute
+ # ── Task routing ─────────────────────────────────────────
+ # Route slow connector/indexing tasks to a dedicated queue so they
+ # never block fast user-facing tasks (file uploads, podcasts, etc.)
+ task_routes={
+ # Connector indexing tasks → connectors queue
+ "index_slack_messages": {"queue": CONNECTORS_QUEUE},
+ "index_notion_pages": {"queue": CONNECTORS_QUEUE},
+ "index_github_repos": {"queue": CONNECTORS_QUEUE},
+ "index_linear_issues": {"queue": CONNECTORS_QUEUE},
+ "index_jira_issues": {"queue": CONNECTORS_QUEUE},
+ "index_confluence_pages": {"queue": CONNECTORS_QUEUE},
+ "index_clickup_tasks": {"queue": CONNECTORS_QUEUE},
+ "index_google_calendar_events": {"queue": CONNECTORS_QUEUE},
+ "index_airtable_records": {"queue": CONNECTORS_QUEUE},
+ "index_google_gmail_messages": {"queue": CONNECTORS_QUEUE},
+ "index_google_drive_files": {"queue": CONNECTORS_QUEUE},
+ "index_discord_messages": {"queue": CONNECTORS_QUEUE},
+ "index_teams_messages": {"queue": CONNECTORS_QUEUE},
+ "index_luma_events": {"queue": CONNECTORS_QUEUE},
+ "index_elasticsearch_documents": {"queue": CONNECTORS_QUEUE},
+ "index_crawled_urls": {"queue": CONNECTORS_QUEUE},
+ "index_bookstack_pages": {"queue": CONNECTORS_QUEUE},
+ "index_obsidian_vault": {"queue": CONNECTORS_QUEUE},
+ "index_composio_connector": {"queue": CONNECTORS_QUEUE},
+ "delete_connector_with_documents": {"queue": CONNECTORS_QUEUE},
+ # Everything else (document processing, podcasts, reindexing,
+ # schedule checker, cleanup) stays on the default fast queue.
+ },
)
# Configure Celery Beat schedule
diff --git a/surfsense_backend/scripts/docker/entrypoint.sh b/surfsense_backend/scripts/docker/entrypoint.sh
index f721e4e85..ce0f1ce13 100644
--- a/surfsense_backend/scripts/docker/entrypoint.sh
+++ b/surfsense_backend/scripts/docker/entrypoint.sh
@@ -1,58 +1,129 @@
#!/bin/bash
set -e
-# Function to handle shutdown gracefully
+# ─────────────────────────────────────────────────────────────
+# SERVICE_ROLE controls which process(es) this container runs.
+#
+# api – FastAPI backend only (runs migrations on startup)
+# worker – Celery worker only
+# beat – Celery beat scheduler only
+# all – All three in one container (legacy / dev default)
+#
+# Set SERVICE_ROLE as an environment variable in Coolify for
+# each service deployment.
+# ─────────────────────────────────────────────────────────────
+SERVICE_ROLE="${SERVICE_ROLE:-all}"
+echo "Starting SurfSense with SERVICE_ROLE=${SERVICE_ROLE}"
+
+# ── Autoscale defaults (override via env) ────────────────────
+# CELERY_MAX_WORKERS – max concurrent worker processes
+# CELERY_MIN_WORKERS – min workers kept warm
+# CELERY_QUEUES – comma-separated queues to consume
+# (empty = all queues for backward compat)
+CELERY_MAX_WORKERS="${CELERY_MAX_WORKERS:-10}"
+CELERY_MIN_WORKERS="${CELERY_MIN_WORKERS:-2}"
+CELERY_MAX_TASKS_PER_CHILD="${CELERY_MAX_TASKS_PER_CHILD:-50}"
+CELERY_QUEUES="${CELERY_QUEUES:-}"
+
+# ── Graceful shutdown ────────────────────────────────────────
+PIDS=()
+
cleanup() {
echo "Shutting down services..."
- kill -TERM "$backend_pid" "$celery_worker_pid" "$celery_beat_pid" 2>/dev/null || true
- wait "$backend_pid" "$celery_worker_pid" "$celery_beat_pid" 2>/dev/null || true
+ for pid in "${PIDS[@]}"; do
+ kill -TERM "$pid" 2>/dev/null || true
+ done
+ for pid in "${PIDS[@]}"; do
+ wait "$pid" 2>/dev/null || true
+ done
exit 0
}
trap cleanup SIGTERM SIGINT
-# Run database migrations with safeguards
-echo "Running database migrations..."
-# Wait for database to be ready (max 30 seconds)
-for i in {1..30}; do
- if python -c "from app.db import engine; import asyncio; asyncio.run(engine.dispose())" 2>/dev/null; then
- echo "Database is ready."
- break
+# ── Database migrations (only for api / all) ─────────────────
+run_migrations() {
+ echo "Running database migrations..."
+ for i in {1..30}; do
+ if python -c "from app.db import engine; import asyncio; asyncio.run(engine.dispose())" 2>/dev/null; then
+ echo "Database is ready."
+ break
+ fi
+ echo "Waiting for database... ($i/30)"
+ sleep 1
+ done
+
+ if timeout 60 alembic upgrade head 2>&1; then
+ echo "Migrations completed successfully."
+ else
+ echo "WARNING: Migration failed or timed out. Continuing anyway..."
+ echo "You may need to run migrations manually: alembic upgrade head"
fi
- echo "Waiting for database... ($i/30)"
- sleep 1
-done
+}
-# Run migrations with timeout (60 seconds max)
-if timeout 60 alembic upgrade head 2>&1; then
- echo "Migrations completed successfully."
-else
- echo "WARNING: Migration failed or timed out. Continuing anyway..."
- echo "You may need to run migrations manually: alembic upgrade head"
-fi
+# ── Service starters ─────────────────────────────────────────
+start_api() {
+ echo "Starting FastAPI Backend..."
+ python main.py &
+ PIDS+=($!)
+ echo " FastAPI PID=${PIDS[-1]}"
+}
-echo "Starting FastAPI Backend..."
-python main.py &
-backend_pid=$!
+start_worker() {
+ QUEUE_ARGS=""
+ if [ -n "${CELERY_QUEUES}" ]; then
+ QUEUE_ARGS="--queues=${CELERY_QUEUES}"
+ fi
-# Wait a bit for backend to initialize
-sleep 5
+ echo "Starting Celery Worker (autoscale=${CELERY_MAX_WORKERS},${CELERY_MIN_WORKERS}, max-tasks-per-child=${CELERY_MAX_TASKS_PER_CHILD}, queues=${CELERY_QUEUES:-all})..."
+ celery -A app.celery_app worker \
+ --loglevel=info \
+ --autoscale="${CELERY_MAX_WORKERS},${CELERY_MIN_WORKERS}" \
+ --max-tasks-per-child="${CELERY_MAX_TASKS_PER_CHILD}" \
+ --prefetch-multiplier=1 \
+ -Ofair \
+ ${QUEUE_ARGS} &
+ PIDS+=($!)
+ echo " Celery Worker PID=${PIDS[-1]}"
+}
-echo "Starting Celery Worker..."
-celery -A app.celery_app worker --loglevel=info --autoscale=128,4 &
-celery_worker_pid=$!
+start_beat() {
+ echo "Starting Celery Beat..."
+ celery -A app.celery_app beat --loglevel=info &
+ PIDS+=($!)
+ echo " Celery Beat PID=${PIDS[-1]}"
+}
-# Wait a bit for worker to initialize
-sleep 3
+# ── Main: run based on role ──────────────────────────────────
+case "${SERVICE_ROLE}" in
+ api)
+ run_migrations
+ start_api
+ ;;
+ worker)
+ start_worker
+ ;;
+ beat)
+ start_beat
+ ;;
+ all)
+ run_migrations
+ start_api
+ sleep 5
+ start_worker
+ sleep 3
+ start_beat
+ ;;
+ *)
+ echo "ERROR: Unknown SERVICE_ROLE '${SERVICE_ROLE}'. Use: api, worker, beat, or all"
+ exit 1
+ ;;
+esac
-echo "Starting Celery Beat..."
-celery -A app.celery_app beat --loglevel=info &
-celery_beat_pid=$!
-
-echo "All services started. PIDs: Backend=$backend_pid, Worker=$celery_worker_pid, Beat=$celery_beat_pid"
+echo "All requested services started. PIDs: ${PIDS[*]}"
# Wait for any process to exit
wait -n
-# If we get here, one process exited, so exit with its status
+# If we get here, one process exited unexpectedly
exit $?
From 725c03ab5d2f015a6613757942de89745f83919a Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Tue, 10 Feb 2026 20:51:43 -0800
Subject: [PATCH 34/35] refactor: wrapped titles in Balancer
---
.../components/homepage/hero-section.tsx | 24 +++++++++----------
1 file changed, 11 insertions(+), 13 deletions(-)
diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx
index ef9a529c3..13e1f8eed 100644
--- a/surfsense_web/components/homepage/hero-section.tsx
+++ b/surfsense_web/components/homepage/hero-section.tsx
@@ -85,21 +85,19 @@ export function HeroSection() {
/>
-
- {isNotebookLMVariant ? (
-
-
- NotebookLM with Superpowers
-
+ {isNotebookLMVariant ? (
+
+
+ NotebookLM with Superpowers
- ) : (
-
-
- NotebookLM for Teams
-
+
+ ) : (
+
+
+ NotebookLM for Teams
- )}
-
+
+ )}
{/* // TODO:aCTUAL DESCRITION */}
From f7ccd8ff91c6d4ae37784d26548a323ccd39168b Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Tue, 10 Feb 2026 21:31:43 -0800
Subject: [PATCH 35/35] chore: linting
---
.../components/homepage/hero-section.tsx | 2 +-
.../components/homepage/integrations.tsx | 2 +-
.../components/icons/providers/index.ts | 2 +-
.../settings/image-model-manager.tsx | 6 +-
.../components/settings/llm-role-manager.tsx | 2 +-
.../settings/model-config-manager.tsx | 4 +-
surfsense_web/lib/provider-icons.tsx | 64 ++++++++++---------
7 files changed, 42 insertions(+), 40 deletions(-)
diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx
index 13e1f8eed..b1e8d02bf 100644
--- a/surfsense_web/components/homepage/hero-section.tsx
+++ b/surfsense_web/components/homepage/hero-section.tsx
@@ -85,7 +85,7 @@ export function HeroSection() {
/>
- {isNotebookLMVariant ? (
+ {isNotebookLMVariant ? (
NotebookLM with Superpowers
diff --git a/surfsense_web/components/homepage/integrations.tsx b/surfsense_web/components/homepage/integrations.tsx
index 662387de5..1a4044140 100644
--- a/surfsense_web/components/homepage/integrations.tsx
+++ b/surfsense_web/components/homepage/integrations.tsx
@@ -1,7 +1,7 @@
"use client";
-import type React from "react";
import Image from "next/image";
+import type React from "react";
interface Integration {
name: string;
diff --git a/surfsense_web/components/icons/providers/index.ts b/surfsense_web/components/icons/providers/index.ts
index 73c03a6cc..0a9bdcc66 100644
--- a/surfsense_web/components/icons/providers/index.ts
+++ b/surfsense_web/components/icons/providers/index.ts
@@ -3,7 +3,6 @@ export { default as AnthropicIcon } from "./anthropic.svg";
export { default as AnyscaleIcon } from "./anyscale.svg";
export { default as BedrockIcon } from "./bedrock.svg";
export { default as CerebrasIcon } from "./cerebras.svg";
-export { default as CloudflareIcon } from "./workersai-cloudflare.svg";
export { default as CohereIcon } from "./cohere.svg";
export { default as CometApiIcon } from "./cometapi.svg";
export { default as DatabricksIcon } from "./dbrx.svg";
@@ -26,6 +25,7 @@ export { default as ReplicateIcon } from "./replicate.svg";
export { default as SambaNovaIcon } from "./sambanova.svg";
export { default as TogetherAiIcon } from "./togetherai.svg";
export { default as VertexAiIcon } from "./vertexai.svg";
+export { default as CloudflareIcon } from "./workersai-cloudflare.svg";
export { default as XaiIcon } from "./xai.svg";
export { default as XinferenceIcon } from "./xinference.svg";
export { default as ZhipuIcon } from "./zhipu.svg";
diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx
index 80828f825..cf7bb5671 100644
--- a/surfsense_web/components/settings/image-model-manager.tsx
+++ b/surfsense_web/components/settings/image-model-manager.tsx
@@ -6,10 +6,10 @@ import {
Check,
ChevronsUpDown,
Edit3,
+ Info,
Key,
Plus,
RefreshCw,
- Info,
Trash2,
Wand2,
} from "lucide-react";
@@ -17,7 +17,6 @@ import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
-import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import {
createImageGenConfigMutationAtom,
deleteImageGenConfigMutationAtom,
@@ -27,6 +26,7 @@ import {
globalImageGenConfigsAtom,
imageGenConfigsAtom,
} from "@/atoms/image-gen-config/image-gen-config-query.atoms";
+import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import { updateLLMPreferencesMutationAtom } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
@@ -75,8 +75,8 @@ import {
IMAGE_GEN_PROVIDERS,
} from "@/contracts/enums/image-gen-providers";
import type { ImageGenerationConfig } from "@/contracts/types/new-llm-config.types";
-import { cn } from "@/lib/utils";
import { getProviderIcon } from "@/lib/provider-icons";
+import { cn } from "@/lib/utils";
interface ImageModelManagerProps {
searchSpaceId: number;
diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx
index 8c6fbf4cf..051df855b 100644
--- a/surfsense_web/components/settings/llm-role-manager.tsx
+++ b/surfsense_web/components/settings/llm-role-manager.tsx
@@ -41,8 +41,8 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
-import { cn } from "@/lib/utils";
import { getProviderIcon } from "@/lib/provider-icons";
+import { cn } from "@/lib/utils";
const ROLE_DESCRIPTIONS = {
agent: {
diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx
index 7f228ff56..ba3445aad 100644
--- a/surfsense_web/components/settings/model-config-manager.tsx
+++ b/surfsense_web/components/settings/model-config-manager.tsx
@@ -5,10 +5,10 @@ import {
AlertCircle,
Edit3,
FileText,
+ Info,
MessageSquareQuote,
Plus,
RefreshCw,
- Info,
Trash2,
Wand2,
} from "lucide-react";
@@ -51,8 +51,8 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import type { NewLLMConfig } from "@/contracts/types/new-llm-config.types";
-import { cn } from "@/lib/utils";
import { getProviderIcon } from "@/lib/provider-icons";
+import { cn } from "@/lib/utils";
interface ModelConfigManagerProps {
searchSpaceId: number;
diff --git a/surfsense_web/lib/provider-icons.tsx b/surfsense_web/lib/provider-icons.tsx
index 11cef5bce..b7f49fe33 100644
--- a/surfsense_web/lib/provider-icons.tsx
+++ b/surfsense_web/lib/provider-icons.tsx
@@ -1,36 +1,38 @@
import { Bot, Shuffle } from "lucide-react";
+import {
+ Ai21Icon,
+ AnthropicIcon,
+ AnyscaleIcon,
+ BedrockIcon,
+ CerebrasIcon,
+ CloudflareIcon,
+ CohereIcon,
+ CometApiIcon,
+ DatabricksIcon,
+ DeepInfraIcon,
+ DeepSeekIcon,
+ FireworksAiIcon,
+ GeminiIcon,
+ GroqIcon,
+ HuggingFaceIcon,
+ MistralIcon,
+ MoonshotIcon,
+ NscaleIcon,
+ OllamaIcon,
+ OpenaiIcon,
+ OpenRouterIcon,
+ PerplexityIcon,
+ QwenIcon,
+ RecraftIcon,
+ ReplicateIcon,
+ SambaNovaIcon,
+ TogetherAiIcon,
+ VertexAiIcon,
+ XaiIcon,
+ XinferenceIcon,
+ ZhipuIcon,
+} from "@/components/icons/providers";
import { cn } from "@/lib/utils";
-import { Ai21Icon } from "@/components/icons/providers";
-import { AnthropicIcon } from "@/components/icons/providers";
-import { AnyscaleIcon } from "@/components/icons/providers";
-import { BedrockIcon } from "@/components/icons/providers";
-import { CerebrasIcon } from "@/components/icons/providers";
-import { CloudflareIcon } from "@/components/icons/providers";
-import { CohereIcon } from "@/components/icons/providers";
-import { CometApiIcon } from "@/components/icons/providers";
-import { DatabricksIcon } from "@/components/icons/providers";
-import { DeepInfraIcon } from "@/components/icons/providers";
-import { DeepSeekIcon } from "@/components/icons/providers";
-import { FireworksAiIcon } from "@/components/icons/providers";
-import { GeminiIcon } from "@/components/icons/providers";
-import { GroqIcon } from "@/components/icons/providers";
-import { HuggingFaceIcon } from "@/components/icons/providers";
-import { MistralIcon } from "@/components/icons/providers";
-import { MoonshotIcon } from "@/components/icons/providers";
-import { NscaleIcon } from "@/components/icons/providers";
-import { OllamaIcon } from "@/components/icons/providers";
-import { OpenaiIcon } from "@/components/icons/providers";
-import { OpenRouterIcon } from "@/components/icons/providers";
-import { PerplexityIcon } from "@/components/icons/providers";
-import { QwenIcon } from "@/components/icons/providers";
-import { RecraftIcon } from "@/components/icons/providers";
-import { ReplicateIcon } from "@/components/icons/providers";
-import { SambaNovaIcon } from "@/components/icons/providers";
-import { TogetherAiIcon } from "@/components/icons/providers";
-import { VertexAiIcon } from "@/components/icons/providers";
-import { XaiIcon } from "@/components/icons/providers";
-import { XinferenceIcon } from "@/components/icons/providers";
-import { ZhipuIcon } from "@/components/icons/providers";
/**
* Returns a Lucide icon element for the given LLM / image-gen provider.