diff --git a/surfsense_backend/alembic/versions/129_obsidian_plugin_vault_identity.py b/surfsense_backend/alembic/versions/129_obsidian_plugin_vault_identity.py index e716dfff1..0c0e3dbe5 100644 --- a/surfsense_backend/alembic/versions/129_obsidian_plugin_vault_identity.py +++ b/surfsense_backend/alembic/versions/129_obsidian_plugin_vault_identity.py @@ -91,8 +91,7 @@ def downgrade() -> None: ) conn.execute( sa.text( - "DROP INDEX IF EXISTS " - "search_source_connectors_obsidian_plugin_vault_uniq" + "DROP INDEX IF EXISTS search_source_connectors_obsidian_plugin_vault_uniq" ) ) conn.execute( diff --git a/surfsense_backend/app/routes/obsidian_plugin_routes.py b/surfsense_backend/app/routes/obsidian_plugin_routes.py index 096058d8a..8069f8265 100644 --- a/surfsense_backend/app/routes/obsidian_plugin_routes.py +++ b/surfsense_backend/app/routes/obsidian_plugin_routes.py @@ -129,7 +129,9 @@ async def _finish_obsidian_sync_notification( ): """Mark the rolling Obsidian sync inbox item complete or failed.""" handler = NotificationService.connector_indexing - connector_name = notification.notification_metadata.get("connector_name", "Obsidian") + connector_name = notification.notification_metadata.get( + "connector_name", "Obsidian" + ) if failed > 0 and indexed == 0: title = f"Failed: {connector_name}" message = ( @@ -273,9 +275,7 @@ async def _find_by_fingerprint( return (await session.execute(stmt)).scalars().first() -def _build_config( - payload: ConnectRequest, *, now_iso: str -) -> dict[str, object]: +def _build_config(payload: ConnectRequest, *, now_iso: str) -> dict[str, object]: return { "vault_id": payload.vault_id, "vault_name": payload.vault_name, @@ -456,9 +456,7 @@ async def obsidian_sync( session, connector=connector, payload=note, user_id=str(user.id) ) indexed += 1 - items.append( - SyncAckItem(path=note.path, status="ok", document_id=doc.id) - ) + items.append(SyncAckItem(path=note.path, status="ok", document_id=doc.id)) except HTTPException: raise except Exception as exc: @@ -597,9 +595,7 @@ async def obsidian_delete_notes( path, payload.vault_id, ) - items.append( - DeleteAckItem(path=path, status="error", error=str(exc)[:300]) - ) + items.append(DeleteAckItem(path=path, status="error", error=str(exc)[:300])) return DeleteAck( vault_id=payload.vault_id, @@ -616,9 +612,7 @@ async def obsidian_manifest( session: AsyncSession = Depends(get_async_session), ) -> ManifestResponse: """Return ``{path: {hash, mtime}}`` for the plugin's onload reconcile diff.""" - connector = await _resolve_vault_connector( - session, user=user, vault_id=vault_id - ) + connector = await _resolve_vault_connector(session, user=user, vault_id=vault_id) return await get_manifest(session, connector=connector, vault_id=vault_id) @@ -633,9 +627,7 @@ async def obsidian_stats( ``files_synced`` excludes tombstones so it matches ``/manifest``; ``last_sync_at`` includes them so deletes advance the freshness signal. """ - connector = await _resolve_vault_connector( - session, user=user, vault_id=vault_id - ) + connector = await _resolve_vault_connector(session, user=user, vault_id=vault_id) is_active = Document.document_metadata["deleted_at"].as_string().is_(None) diff --git a/surfsense_backend/app/schemas/obsidian_plugin.py b/surfsense_backend/app/schemas/obsidian_plugin.py index fac44bc3d..745886ef6 100644 --- a/surfsense_backend/app/schemas/obsidian_plugin.py +++ b/surfsense_backend/app/schemas/obsidian_plugin.py @@ -24,10 +24,14 @@ class _PluginBase(BaseModel): class NotePayload(_PluginBase): """One Obsidian note as pushed by the plugin (the source of truth).""" - vault_id: str = Field(..., description="Stable plugin-generated UUID for this vault") + vault_id: str = Field( + ..., description="Stable plugin-generated UUID for this vault" + ) path: str = Field(..., description="Vault-relative path, e.g. 'notes/foo.md'") name: str = Field(..., description="File stem (no extension)") - extension: str = Field(default="md", description="File extension without leading dot") + extension: str = Field( + default="md", description="File extension without leading dot" + ) content: str = Field(default="", description="Raw markdown body (post-frontmatter)") frontmatter: dict[str, Any] = Field(default_factory=dict) @@ -38,7 +42,9 @@ class NotePayload(_PluginBase): embeds: list[str] = Field(default_factory=list) aliases: list[str] = Field(default_factory=list) - content_hash: str = Field(..., description="Plugin-computed SHA-256 of the raw content") + content_hash: str = Field( + ..., description="Plugin-computed SHA-256 of the raw content" + ) size: int | None = Field( default=None, ge=0, diff --git a/surfsense_backend/app/services/obsidian_plugin_indexer.py b/surfsense_backend/app/services/obsidian_plugin_indexer.py index ea62f16d8..5afdbf886 100644 --- a/surfsense_backend/app/services/obsidian_plugin_indexer.py +++ b/surfsense_backend/app/services/obsidian_plugin_indexer.py @@ -126,9 +126,7 @@ def _build_document_string(payload: NotePayload, vault_name: str) -> str: existing search relevance heuristics keep working unchanged. """ tags_line = ", ".join(payload.tags) if payload.tags else "None" - links_line = ( - ", ".join(payload.resolved_links) if payload.resolved_links else "None" - ) + links_line = ", ".join(payload.resolved_links) if payload.resolved_links else "None" return ( "\n" f"Title: {payload.name}\n" @@ -235,9 +233,7 @@ async def upsert_note( if not prepared: if existing is not None: return existing - raise RuntimeError( - f"Indexing pipeline rejected obsidian note {payload.path}" - ) + raise RuntimeError(f"Indexing pipeline rejected obsidian note {payload.path}") document = prepared[0] diff --git a/surfsense_backend/tests/integration/test_obsidian_plugin_routes.py b/surfsense_backend/tests/integration/test_obsidian_plugin_routes.py index 449e1473d..1dd7e2a23 100644 --- a/surfsense_backend/tests/integration/test_obsidian_plugin_routes.py +++ b/surfsense_backend/tests/integration/test_obsidian_plugin_routes.py @@ -111,9 +111,7 @@ async def race_user_and_space(async_engine): # connectors test creates documents, so we wipe them too. The # CASCADE on user_id catches anything we missed. await cleanup.execute( - text( - 'DELETE FROM search_source_connectors WHERE user_id = :uid' - ), + text("DELETE FROM search_source_connectors WHERE user_id = :uid"), {"uid": user_id}, ) await cleanup.execute( @@ -156,9 +154,7 @@ class TestConnectRace: ) await obsidian_connect(payload, user=fresh_user, session=s) - results = await asyncio.gather( - _call("a"), _call("b"), return_exceptions=True - ) + results = await asyncio.gather(_call("a"), _call("b"), return_exceptions=True) for r in results: assert not isinstance(r, Exception), f"Connect raised: {r!r}" @@ -430,9 +426,7 @@ class TestWireContractSmoke: assert {it.status for it in rename_resp.items} == {"ok", "missing"} # snake_case fields are deliberate — the plugin decoder maps them # to camelCase explicitly. - assert all( - it.old_path and it.new_path for it in rename_resp.items - ) + assert all(it.old_path and it.new_path for it in rename_resp.items) # 4. /notes DELETE async def _delete(*args, **kwargs) -> bool: diff --git a/surfsense_backend/tests/unit/test_error_contract.py b/surfsense_backend/tests/unit/test_error_contract.py index 81ec08b2d..ec8021290 100644 --- a/surfsense_backend/tests/unit/test_error_contract.py +++ b/surfsense_backend/tests/unit/test_error_contract.py @@ -202,9 +202,7 @@ class TestHTTPExceptionHandler: # Intentional 503s (e.g. feature flag off) must surface the developer # message so the frontend can render actionable copy. body = _assert_envelope(client.get("/http-503"), 503) - assert ( - body["error"]["message"] == "Page purchases are temporarily unavailable." - ) + assert body["error"]["message"] == "Page purchases are temporarily unavailable." assert body["error"]["message"] != GENERIC_5XX_MESSAGE def test_502_preserves_detail(self, client): diff --git a/surfsense_web/app/api/v1/[...path]/route.ts b/surfsense_web/app/api/v1/[...path]/route.ts index 82c8e2a5d..418bf1a33 100644 --- a/surfsense_web/app/api/v1/[...path]/route.ts +++ b/surfsense_web/app/api/v1/[...path]/route.ts @@ -33,10 +33,7 @@ function toClientHeaders(headers: Headers) { return nextHeaders; } -async function proxy( - request: NextRequest, - context: { params: Promise<{ path?: string[] }> } -) { +async function proxy(request: NextRequest, context: { params: Promise<{ path?: string[] }> }) { const params = await context.params; const path = params.path?.join("/") || ""; const upstreamUrl = new URL(`${getBackendBaseUrl()}/api/v1/${path}`); @@ -62,4 +59,12 @@ async function proxy( }); } -export { proxy as GET, proxy as POST, proxy as PUT, proxy as PATCH, proxy as DELETE, proxy as OPTIONS, proxy as HEAD }; +export { + proxy as GET, + proxy as POST, + proxy as PUT, + proxy as PATCH, + proxy as DELETE, + proxy as OPTIONS, + proxy as HEAD, +}; diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx index 3600d30db..c34d9c0ca 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx @@ -3,7 +3,7 @@ import { Check, Copy, Info } from "lucide-react"; import { useTranslations } from "next-intl"; import { useCallback, useRef, useState } from "react"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { useApiKey } from "@/hooks/use-api-key"; diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopContent.tsx index 3175268d2..63ca9f5df 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopContent.tsx @@ -200,8 +200,8 @@ export function DesktopContent() { Launch on Startup - Automatically start SurfSense when you sign in to your computer so global - shortcuts and folder sync are always available. + Automatically start SurfSense when you sign in to your computer so global shortcuts and + folder sync are always available. @@ -232,8 +232,7 @@ export function DesktopContent() { Start minimized to tray

- Skip the main window on boot — SurfSense lives in the system tray until you need - it. + Skip the main window on boot — SurfSense lives in the system tray until you need it.

new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - ); + ].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); }, [pagesQuery.data, tokensQuery.data]); if (isLoading) { diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx index 689684c51..ecbb09fae 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx @@ -4,17 +4,16 @@ import { Check, Copy, Info } from "lucide-react"; import { type FC, useCallback, useRef, useState } from "react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; +import { EnumConnectorName } from "@/contracts/enums/connector"; import { useApiKey } from "@/hooks/use-api-key"; import { copyToClipboard as copyToClipboardUtil } from "@/lib/utils"; -import { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorBenefits } from "../connector-benefits"; import type { ConnectFormProps } from "../index"; const PLUGIN_RELEASES_URL = "https://github.com/MODSetter/SurfSense/releases?q=obsidian&expanded=true"; -const BACKEND_URL = - process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? "https://surfsense.com"; +const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? "https://surfsense.com"; /** * Obsidian connect form for the plugin-only architecture. @@ -32,9 +31,7 @@ const BACKEND_URL = export const ObsidianConnectForm: FC = ({ onBack }) => { const { apiKey, isLoading, copied, copyToClipboard } = useApiKey(); const [copiedUrl, setCopiedUrl] = useState(false); - const urlCopyTimerRef = useRef | undefined>( - undefined - ); + const urlCopyTimerRef = useRef | undefined>(undefined); const copyServerUrl = useCallback(async () => { const ok = await copyToClipboardUtil(BACKEND_URL); @@ -59,9 +56,8 @@ export const ObsidianConnectForm: FC = ({ onBack }) => { Plugin-based sync - SurfSense now syncs Obsidian via an official plugin that runs inside - Obsidian itself. Works on desktop and mobile, in cloud and self-hosted - deployments. + SurfSense now syncs Obsidian via an official plugin that runs inside Obsidian itself. + Works on desktop and mobile, in cloud and self-hosted deployments. @@ -76,10 +72,9 @@ export const ObsidianConnectForm: FC = ({ onBack }) => {

Install the plugin

- Grab the latest SurfSense plugin release. Once it's in the community - store, you'll also be able to install it from{" "} - Settings → Community plugins{" "} - inside Obsidian. + Grab the latest SurfSense plugin release. Once it's in the community store, you'll + also be able to install it from{" "} + Settings → Community plugins inside Obsidian.

= ({ onBack }) => { rel="noopener noreferrer" className="inline-flex" > - @@ -104,9 +104,9 @@ export const ObsidianConnectForm: FC = ({ onBack }) => {

Copy your API key

- Paste this into the plugin's API token{" "} - setting. The token expires after 24 hours. Long-lived personal access - tokens are coming in a future release. + Paste this into the plugin's API token setting. + The token expires after 24 hours. Long-lived personal access tokens are coming in a + future release.

{isLoading ? ( @@ -151,9 +151,9 @@ export const ObsidianConnectForm: FC = ({ onBack }) => {

Point the plugin at this server

- For SurfSense Cloud, use the default surfsense.com. - If you are self-hosting, set the plugin's{" "} - Server URL to your frontend domain. + For SurfSense Cloud, use the default{" "} + surfsense.com. If you are self-hosting, set the + plugin's Server URL to your frontend domain.

@@ -168,10 +168,9 @@ export const ObsidianConnectForm: FC = ({ onBack }) => {

Pick this search space

- In the plugin's Search space{" "} - setting, choose the search space you want this vault to sync into. - The connector will appear here automatically once the plugin makes - its first sync. + In the plugin's Search space setting, choose the + search space you want this vault to sync into. The connector will appear here + automatically once the plugin makes its first sync.

@@ -183,11 +182,9 @@ export const ObsidianConnectForm: FC = ({ onBack }) => { What you get with Obsidian integration:
    - {getConnectorBenefits(EnumConnectorName.OBSIDIAN_CONNECTOR)?.map( - (benefit) => ( -
  • {benefit}
  • - ) - )} + {getConnectorBenefits(EnumConnectorName.OBSIDIAN_CONNECTOR)?.map((benefit) => ( +
  • {benefit}
  • + ))}
)} diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/obsidian-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/obsidian-config.tsx index a9b98b76c..52b18fa09 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/obsidian-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/obsidian-config.tsx @@ -117,9 +117,7 @@ const PluginStats: FC<{ config: Record }> = ({ config }) => { label: "Files synced", value: placeholder ?? - (typeof stats?.files_synced === "number" - ? stats.files_synced.toLocaleString() - : "—"), + (typeof stats?.files_synced === "number" ? stats.files_synced.toLocaleString() : "—"), }, ]; }, [config.vault_name, stats, statsError]); @@ -139,10 +137,7 @@ const PluginStats: FC<{ config: Record }> = ({ config }) => {

Vault status

{tileRows.map((stat) => ( -
+
{stat.label}
@@ -160,8 +155,8 @@ const UnknownConnectorState: FC = () => ( Unrecognized config - This connector has neither plugin metadata nor a legacy marker. It may predate migration — - you can safely delete it and re-install the SurfSense Obsidian plugin to resume syncing. + This connector has neither plugin metadata nor a legacy marker. It may predate migration — you + can safely delete it and re-install the SurfSense Obsidian plugin to resume syncing. ); diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index c897489ff..154ff247a 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -349,12 +349,7 @@ export const AUTO_INDEX_CONNECTOR_TYPES = new Set(Object.keys(AUTO_INDEX // `lib/posthog/events.ts` or per-connector tracking code. // ============================================================================ -export type ConnectorTelemetryGroup = - | "oauth" - | "composio" - | "crawler" - | "other" - | "unknown"; +export type ConnectorTelemetryGroup = "oauth" | "composio" | "crawler" | "other" | "unknown"; export interface ConnectorTelemetryMeta { connector_type: string; @@ -363,45 +358,44 @@ export interface ConnectorTelemetryMeta { is_oauth: boolean; } -const CONNECTOR_TELEMETRY_REGISTRY: ReadonlyMap = - (() => { - const map = new Map(); +const CONNECTOR_TELEMETRY_REGISTRY: ReadonlyMap = (() => { + const map = new Map(); - for (const c of OAUTH_CONNECTORS) { - map.set(c.connectorType, { - connector_type: c.connectorType, - connector_title: c.title, - connector_group: "oauth", - is_oauth: true, - }); - } - for (const c of COMPOSIO_CONNECTORS) { - map.set(c.connectorType, { - connector_type: c.connectorType, - connector_title: c.title, - connector_group: "composio", - is_oauth: true, - }); - } - for (const c of CRAWLERS) { - map.set(c.connectorType, { - connector_type: c.connectorType, - connector_title: c.title, - connector_group: "crawler", - is_oauth: false, - }); - } - for (const c of OTHER_CONNECTORS) { - map.set(c.connectorType, { - connector_type: c.connectorType, - connector_title: c.title, - connector_group: "other", - is_oauth: false, - }); - } + for (const c of OAUTH_CONNECTORS) { + map.set(c.connectorType, { + connector_type: c.connectorType, + connector_title: c.title, + connector_group: "oauth", + is_oauth: true, + }); + } + for (const c of COMPOSIO_CONNECTORS) { + map.set(c.connectorType, { + connector_type: c.connectorType, + connector_title: c.title, + connector_group: "composio", + is_oauth: true, + }); + } + for (const c of CRAWLERS) { + map.set(c.connectorType, { + connector_type: c.connectorType, + connector_title: c.title, + connector_group: "crawler", + is_oauth: false, + }); + } + for (const c of OTHER_CONNECTORS) { + map.set(c.connectorType, { + connector_type: c.connectorType, + connector_title: c.title, + connector_group: "other", + is_oauth: false, + }); + } - return map; - })(); + return map; +})(); /** * Returns telemetry metadata for a connector_type, or a minimal "unknown" diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index e00a69939..317973eba 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -350,11 +350,7 @@ export const useConnectorDialog = () => { // Set connecting state immediately to disable button and show spinner setConnectingId(connector.id); - trackConnectorSetupStarted( - Number(searchSpaceId), - connector.connectorType, - "oauth_click" - ); + trackConnectorSetupStarted(Number(searchSpaceId), connector.connectorType, "oauth_click"); try { // Check if authEndpoint already has query parameters @@ -478,11 +474,7 @@ export const useConnectorDialog = () => { (connectorType: string) => { if (!searchSpaceId) return; - trackConnectorSetupStarted( - Number(searchSpaceId), - connectorType, - "non_oauth_click" - ); + trackConnectorSetupStarted(Number(searchSpaceId), connectorType, "non_oauth_click"); setConnectingConnectorType(connectorType); }, diff --git a/surfsense_web/components/free-chat/free-chat-page.tsx b/surfsense_web/components/free-chat/free-chat-page.tsx index b389a8489..deac1fd00 100644 --- a/surfsense_web/components/free-chat/free-chat-page.tsx +++ b/surfsense_web/components/free-chat/free-chat-page.tsx @@ -210,8 +210,7 @@ export function FreeChatPage() { trackAnonymousChatMessageSent({ modelSlug, messageLength: userQuery.trim().length, - hasUploadedDoc: - anonMode.isAnonymous && anonMode.uploadedDoc !== null ? true : false, + hasUploadedDoc: anonMode.isAnonymous && anonMode.uploadedDoc !== null ? true : false, surface: "free_chat_page", }); diff --git a/surfsense_web/components/homepage/features-bento-grid.tsx b/surfsense_web/components/homepage/features-bento-grid.tsx index 835ccd2c2..7406223de 100644 --- a/surfsense_web/components/homepage/features-bento-grid.tsx +++ b/surfsense_web/components/homepage/features-bento-grid.tsx @@ -426,15 +426,50 @@ const AiSortIllustration = () => ( AI File Sorting illustration showing automatic folder organization {/* Scattered documents on the left */} - - - + + + {/* AI sparkle / magic in the center */} - - + + @@ -442,51 +477,208 @@ const AiSortIllustration = () => ( {/* Animated sorting arrows */} - + - + - + - + {/* Organized folder tree on the right */} {/* Root folder */} - - - + + + {/* Subfolder 1 */} - - - - - + + + + + {/* Subfolder 2 */} - - - - - + + + + + {/* Subfolder 3 */} - - - - - + + + + + {/* Sparkle accents */} @@ -495,10 +687,22 @@ const AiSortIllustration = () => ( - + - + diff --git a/surfsense_web/components/sources/DocumentUploadTab.tsx b/surfsense_web/components/sources/DocumentUploadTab.tsx index 65fa117f7..5a324fea9 100644 --- a/surfsense_web/components/sources/DocumentUploadTab.tsx +++ b/surfsense_web/components/sources/DocumentUploadTab.tsx @@ -546,35 +546,35 @@ export function DocumentUploadTab({ ) ) : ( -
{ - if (!isElectron) fileInputRef.current?.click(); - }} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); +
{ if (!isElectron) fileInputRef.current?.click(); - } - }} - > - -
-

- {isElectron ? t("select_files_or_folder") : t("tap_select_files_or_folder")} -

-

{t("file_size_limit")}

-
-
e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + if (!isElectron) fileInputRef.current?.click(); + } + }} > - {renderBrowseButton({ fullWidth: true })} -
-
+ +
+

+ {isElectron ? t("select_files_or_folder") : t("tap_select_files_or_folder")} +

+

{t("file_size_limit")}

+
+
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > + {renderBrowseButton({ fullWidth: true })} +
+
)}