mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-08 22:22:17 +02:00
chore: ran linting
This commit is contained in:
parent
4a75603d4f
commit
3eb4d55ef5
17 changed files with 369 additions and 201 deletions
|
|
@ -91,8 +91,7 @@ def downgrade() -> None:
|
||||||
)
|
)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
sa.text(
|
sa.text(
|
||||||
"DROP INDEX IF EXISTS "
|
"DROP INDEX IF EXISTS search_source_connectors_obsidian_plugin_vault_uniq"
|
||||||
"search_source_connectors_obsidian_plugin_vault_uniq"
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,9 @@ async def _finish_obsidian_sync_notification(
|
||||||
):
|
):
|
||||||
"""Mark the rolling Obsidian sync inbox item complete or failed."""
|
"""Mark the rolling Obsidian sync inbox item complete or failed."""
|
||||||
handler = NotificationService.connector_indexing
|
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:
|
if failed > 0 and indexed == 0:
|
||||||
title = f"Failed: {connector_name}"
|
title = f"Failed: {connector_name}"
|
||||||
message = (
|
message = (
|
||||||
|
|
@ -273,9 +275,7 @@ async def _find_by_fingerprint(
|
||||||
return (await session.execute(stmt)).scalars().first()
|
return (await session.execute(stmt)).scalars().first()
|
||||||
|
|
||||||
|
|
||||||
def _build_config(
|
def _build_config(payload: ConnectRequest, *, now_iso: str) -> dict[str, object]:
|
||||||
payload: ConnectRequest, *, now_iso: str
|
|
||||||
) -> dict[str, object]:
|
|
||||||
return {
|
return {
|
||||||
"vault_id": payload.vault_id,
|
"vault_id": payload.vault_id,
|
||||||
"vault_name": payload.vault_name,
|
"vault_name": payload.vault_name,
|
||||||
|
|
@ -456,9 +456,7 @@ async def obsidian_sync(
|
||||||
session, connector=connector, payload=note, user_id=str(user.id)
|
session, connector=connector, payload=note, user_id=str(user.id)
|
||||||
)
|
)
|
||||||
indexed += 1
|
indexed += 1
|
||||||
items.append(
|
items.append(SyncAckItem(path=note.path, status="ok", document_id=doc.id))
|
||||||
SyncAckItem(path=note.path, status="ok", document_id=doc.id)
|
|
||||||
)
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
@ -597,9 +595,7 @@ async def obsidian_delete_notes(
|
||||||
path,
|
path,
|
||||||
payload.vault_id,
|
payload.vault_id,
|
||||||
)
|
)
|
||||||
items.append(
|
items.append(DeleteAckItem(path=path, status="error", error=str(exc)[:300]))
|
||||||
DeleteAckItem(path=path, status="error", error=str(exc)[:300])
|
|
||||||
)
|
|
||||||
|
|
||||||
return DeleteAck(
|
return DeleteAck(
|
||||||
vault_id=payload.vault_id,
|
vault_id=payload.vault_id,
|
||||||
|
|
@ -616,9 +612,7 @@ async def obsidian_manifest(
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
) -> ManifestResponse:
|
) -> ManifestResponse:
|
||||||
"""Return ``{path: {hash, mtime}}`` for the plugin's onload reconcile diff."""
|
"""Return ``{path: {hash, mtime}}`` for the plugin's onload reconcile diff."""
|
||||||
connector = await _resolve_vault_connector(
|
connector = await _resolve_vault_connector(session, user=user, vault_id=vault_id)
|
||||||
session, user=user, vault_id=vault_id
|
|
||||||
)
|
|
||||||
return await get_manifest(session, connector=connector, 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``;
|
``files_synced`` excludes tombstones so it matches ``/manifest``;
|
||||||
``last_sync_at`` includes them so deletes advance the freshness signal.
|
``last_sync_at`` includes them so deletes advance the freshness signal.
|
||||||
"""
|
"""
|
||||||
connector = await _resolve_vault_connector(
|
connector = await _resolve_vault_connector(session, user=user, vault_id=vault_id)
|
||||||
session, user=user, vault_id=vault_id
|
|
||||||
)
|
|
||||||
|
|
||||||
is_active = Document.document_metadata["deleted_at"].as_string().is_(None)
|
is_active = Document.document_metadata["deleted_at"].as_string().is_(None)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,14 @@ class _PluginBase(BaseModel):
|
||||||
class NotePayload(_PluginBase):
|
class NotePayload(_PluginBase):
|
||||||
"""One Obsidian note as pushed by the plugin (the source of truth)."""
|
"""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'")
|
path: str = Field(..., description="Vault-relative path, e.g. 'notes/foo.md'")
|
||||||
name: str = Field(..., description="File stem (no extension)")
|
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)")
|
content: str = Field(default="", description="Raw markdown body (post-frontmatter)")
|
||||||
|
|
||||||
frontmatter: dict[str, Any] = Field(default_factory=dict)
|
frontmatter: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
@ -38,7 +42,9 @@ class NotePayload(_PluginBase):
|
||||||
embeds: list[str] = Field(default_factory=list)
|
embeds: list[str] = Field(default_factory=list)
|
||||||
aliases: 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(
|
size: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
ge=0,
|
ge=0,
|
||||||
|
|
|
||||||
|
|
@ -126,9 +126,7 @@ def _build_document_string(payload: NotePayload, vault_name: str) -> str:
|
||||||
existing search relevance heuristics keep working unchanged.
|
existing search relevance heuristics keep working unchanged.
|
||||||
"""
|
"""
|
||||||
tags_line = ", ".join(payload.tags) if payload.tags else "None"
|
tags_line = ", ".join(payload.tags) if payload.tags else "None"
|
||||||
links_line = (
|
links_line = ", ".join(payload.resolved_links) if payload.resolved_links else "None"
|
||||||
", ".join(payload.resolved_links) if payload.resolved_links else "None"
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
"<METADATA>\n"
|
"<METADATA>\n"
|
||||||
f"Title: {payload.name}\n"
|
f"Title: {payload.name}\n"
|
||||||
|
|
@ -235,9 +233,7 @@ async def upsert_note(
|
||||||
if not prepared:
|
if not prepared:
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
return existing
|
return existing
|
||||||
raise RuntimeError(
|
raise RuntimeError(f"Indexing pipeline rejected obsidian note {payload.path}")
|
||||||
f"Indexing pipeline rejected obsidian note {payload.path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
document = prepared[0]
|
document = prepared[0]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,9 +111,7 @@ async def race_user_and_space(async_engine):
|
||||||
# connectors test creates documents, so we wipe them too. The
|
# connectors test creates documents, so we wipe them too. The
|
||||||
# CASCADE on user_id catches anything we missed.
|
# CASCADE on user_id catches anything we missed.
|
||||||
await cleanup.execute(
|
await cleanup.execute(
|
||||||
text(
|
text("DELETE FROM search_source_connectors WHERE user_id = :uid"),
|
||||||
'DELETE FROM search_source_connectors WHERE user_id = :uid'
|
|
||||||
),
|
|
||||||
{"uid": user_id},
|
{"uid": user_id},
|
||||||
)
|
)
|
||||||
await cleanup.execute(
|
await cleanup.execute(
|
||||||
|
|
@ -156,9 +154,7 @@ class TestConnectRace:
|
||||||
)
|
)
|
||||||
await obsidian_connect(payload, user=fresh_user, session=s)
|
await obsidian_connect(payload, user=fresh_user, session=s)
|
||||||
|
|
||||||
results = await asyncio.gather(
|
results = await asyncio.gather(_call("a"), _call("b"), return_exceptions=True)
|
||||||
_call("a"), _call("b"), return_exceptions=True
|
|
||||||
)
|
|
||||||
for r in results:
|
for r in results:
|
||||||
assert not isinstance(r, Exception), f"Connect raised: {r!r}"
|
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"}
|
assert {it.status for it in rename_resp.items} == {"ok", "missing"}
|
||||||
# snake_case fields are deliberate — the plugin decoder maps them
|
# snake_case fields are deliberate — the plugin decoder maps them
|
||||||
# to camelCase explicitly.
|
# to camelCase explicitly.
|
||||||
assert all(
|
assert all(it.old_path and it.new_path for it in rename_resp.items)
|
||||||
it.old_path and it.new_path for it in rename_resp.items
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. /notes DELETE
|
# 4. /notes DELETE
|
||||||
async def _delete(*args, **kwargs) -> bool:
|
async def _delete(*args, **kwargs) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -202,9 +202,7 @@ class TestHTTPExceptionHandler:
|
||||||
# Intentional 503s (e.g. feature flag off) must surface the developer
|
# Intentional 503s (e.g. feature flag off) must surface the developer
|
||||||
# message so the frontend can render actionable copy.
|
# message so the frontend can render actionable copy.
|
||||||
body = _assert_envelope(client.get("/http-503"), 503)
|
body = _assert_envelope(client.get("/http-503"), 503)
|
||||||
assert (
|
assert body["error"]["message"] == "Page purchases are temporarily unavailable."
|
||||||
body["error"]["message"] == "Page purchases are temporarily unavailable."
|
|
||||||
)
|
|
||||||
assert body["error"]["message"] != GENERIC_5XX_MESSAGE
|
assert body["error"]["message"] != GENERIC_5XX_MESSAGE
|
||||||
|
|
||||||
def test_502_preserves_detail(self, client):
|
def test_502_preserves_detail(self, client):
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,7 @@ function toClientHeaders(headers: Headers) {
|
||||||
return nextHeaders;
|
return nextHeaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function proxy(
|
async function proxy(request: NextRequest, context: { params: Promise<{ path?: string[] }> }) {
|
||||||
request: NextRequest,
|
|
||||||
context: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const params = await context.params;
|
const params = await context.params;
|
||||||
const path = params.path?.join("/") || "";
|
const path = params.path?.join("/") || "";
|
||||||
const upstreamUrl = new URL(`${getBackendBaseUrl()}/api/v1/${path}`);
|
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,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { Check, Copy, Info } from "lucide-react";
|
import { Check, Copy, Info } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useRef, useState } from "react";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { useApiKey } from "@/hooks/use-api-key";
|
import { useApiKey } from "@/hooks/use-api-key";
|
||||||
|
|
|
||||||
|
|
@ -200,8 +200,8 @@ export function DesktopContent() {
|
||||||
Launch on Startup
|
Launch on Startup
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs md:text-sm">
|
<CardDescription className="text-xs md:text-sm">
|
||||||
Automatically start SurfSense when you sign in to your computer so global
|
Automatically start SurfSense when you sign in to your computer so global shortcuts and
|
||||||
shortcuts and folder sync are always available.
|
folder sync are always available.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-3 md:px-6 pb-3 md:pb-6 space-y-3">
|
<CardContent className="px-3 md:px-6 pb-3 md:pb-6 space-y-3">
|
||||||
|
|
@ -232,8 +232,7 @@ export function DesktopContent() {
|
||||||
Start minimized to tray
|
Start minimized to tray
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Skip the main window on boot — SurfSense lives in the system tray until you need
|
Skip the main window on boot — SurfSense lives in the system tray until you need it.
|
||||||
it.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
|
||||||
|
|
@ -126,9 +126,7 @@ export function PurchaseHistoryContent() {
|
||||||
return [
|
return [
|
||||||
...pagePurchases.map(normalizePagePurchase),
|
...pagePurchases.map(normalizePagePurchase),
|
||||||
...tokenPurchases.map(normalizeTokenPurchase),
|
...tokenPurchases.map(normalizeTokenPurchase),
|
||||||
].sort(
|
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||||
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
|
||||||
);
|
|
||||||
}, [pagesQuery.data, tokensQuery.data]);
|
}, [pagesQuery.data, tokensQuery.data]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,16 @@ import { Check, Copy, Info } from "lucide-react";
|
||||||
import { type FC, useCallback, useRef, useState } from "react";
|
import { type FC, useCallback, useRef, useState } from "react";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||||
import { useApiKey } from "@/hooks/use-api-key";
|
import { useApiKey } from "@/hooks/use-api-key";
|
||||||
import { copyToClipboard as copyToClipboardUtil } from "@/lib/utils";
|
import { copyToClipboard as copyToClipboardUtil } from "@/lib/utils";
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorBenefits } from "../connector-benefits";
|
import { getConnectorBenefits } from "../connector-benefits";
|
||||||
import type { ConnectFormProps } from "../index";
|
import type { ConnectFormProps } from "../index";
|
||||||
|
|
||||||
const PLUGIN_RELEASES_URL =
|
const PLUGIN_RELEASES_URL =
|
||||||
"https://github.com/MODSetter/SurfSense/releases?q=obsidian&expanded=true";
|
"https://github.com/MODSetter/SurfSense/releases?q=obsidian&expanded=true";
|
||||||
|
|
||||||
const BACKEND_URL =
|
const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? "https://surfsense.com";
|
||||||
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? "https://surfsense.com";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obsidian connect form for the plugin-only architecture.
|
* Obsidian connect form for the plugin-only architecture.
|
||||||
|
|
@ -32,9 +31,7 @@ const BACKEND_URL =
|
||||||
export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
|
export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
|
||||||
const { apiKey, isLoading, copied, copyToClipboard } = useApiKey();
|
const { apiKey, isLoading, copied, copyToClipboard } = useApiKey();
|
||||||
const [copiedUrl, setCopiedUrl] = useState(false);
|
const [copiedUrl, setCopiedUrl] = useState(false);
|
||||||
const urlCopyTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
const urlCopyTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const copyServerUrl = useCallback(async () => {
|
const copyServerUrl = useCallback(async () => {
|
||||||
const ok = await copyToClipboardUtil(BACKEND_URL);
|
const ok = await copyToClipboardUtil(BACKEND_URL);
|
||||||
|
|
@ -59,9 +56,8 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
|
||||||
<Info className="size-4 shrink-0 text-purple-500" />
|
<Info className="size-4 shrink-0 text-purple-500" />
|
||||||
<AlertTitle className="text-xs sm:text-sm">Plugin-based sync</AlertTitle>
|
<AlertTitle className="text-xs sm:text-sm">Plugin-based sync</AlertTitle>
|
||||||
<AlertDescription className="text-[10px] sm:text-xs">
|
<AlertDescription className="text-[10px] sm:text-xs">
|
||||||
SurfSense now syncs Obsidian via an official plugin that runs inside
|
SurfSense now syncs Obsidian via an official plugin that runs inside Obsidian itself.
|
||||||
Obsidian itself. Works on desktop and mobile, in cloud and self-hosted
|
Works on desktop and mobile, in cloud and self-hosted deployments.
|
||||||
deployments.
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
|
@ -76,10 +72,9 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
|
||||||
<h3 className="text-sm font-medium sm:text-base">Install the plugin</h3>
|
<h3 className="text-sm font-medium sm:text-base">Install the plugin</h3>
|
||||||
</header>
|
</header>
|
||||||
<p className="mb-3 text-[11px] text-muted-foreground sm:text-xs">
|
<p className="mb-3 text-[11px] text-muted-foreground sm:text-xs">
|
||||||
Grab the latest SurfSense plugin release. Once it's in the community
|
Grab the latest SurfSense plugin release. Once it's in the community store, you'll
|
||||||
store, you'll also be able to install it from{" "}
|
also be able to install it from{" "}
|
||||||
<span className="font-medium">Settings → Community plugins</span>{" "}
|
<span className="font-medium">Settings → Community plugins</span> inside Obsidian.
|
||||||
inside Obsidian.
|
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href={PLUGIN_RELEASES_URL}
|
href={PLUGIN_RELEASES_URL}
|
||||||
|
|
@ -87,7 +82,12 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex"
|
className="inline-flex"
|
||||||
>
|
>
|
||||||
<Button type="button" variant="secondary" size="sm" className="gap-2 text-xs sm:text-sm">
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 text-xs sm:text-sm"
|
||||||
|
>
|
||||||
Open plugin releases
|
Open plugin releases
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -104,9 +104,9 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
|
||||||
<h3 className="text-sm font-medium sm:text-base">Copy your API key</h3>
|
<h3 className="text-sm font-medium sm:text-base">Copy your API key</h3>
|
||||||
</header>
|
</header>
|
||||||
<p className="mb-3 text-[11px] text-muted-foreground sm:text-xs">
|
<p className="mb-3 text-[11px] text-muted-foreground sm:text-xs">
|
||||||
Paste this into the plugin's <span className="font-medium">API token</span>{" "}
|
Paste this into the plugin's <span className="font-medium">API token</span> setting.
|
||||||
setting. The token expires after 24 hours. Long-lived personal access
|
The token expires after 24 hours. Long-lived personal access tokens are coming in a
|
||||||
tokens are coming in a future release.
|
future release.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|
@ -151,9 +151,9 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
|
||||||
<h3 className="text-sm font-medium sm:text-base">Point the plugin at this server</h3>
|
<h3 className="text-sm font-medium sm:text-base">Point the plugin at this server</h3>
|
||||||
</header>
|
</header>
|
||||||
<p className="text-[11px] text-muted-foreground sm:text-xs">
|
<p className="text-[11px] text-muted-foreground sm:text-xs">
|
||||||
For SurfSense Cloud, use the default <span className="font-medium">surfsense.com</span>.
|
For SurfSense Cloud, use the default{" "}
|
||||||
If you are self-hosting, set the plugin's{" "}
|
<span className="font-medium">surfsense.com</span>. If you are self-hosting, set the
|
||||||
<span className="font-medium">Server URL</span> to your frontend domain.
|
plugin's <span className="font-medium">Server URL</span> to your frontend domain.
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
|
@ -168,10 +168,9 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
|
||||||
<h3 className="text-sm font-medium sm:text-base">Pick this search space</h3>
|
<h3 className="text-sm font-medium sm:text-base">Pick this search space</h3>
|
||||||
</header>
|
</header>
|
||||||
<p className="text-[11px] text-muted-foreground sm:text-xs">
|
<p className="text-[11px] text-muted-foreground sm:text-xs">
|
||||||
In the plugin's <span className="font-medium">Search space</span>{" "}
|
In the plugin's <span className="font-medium">Search space</span> setting, choose the
|
||||||
setting, choose the search space you want this vault to sync into.
|
search space you want this vault to sync into. The connector will appear here
|
||||||
The connector will appear here automatically once the plugin makes
|
automatically once the plugin makes its first sync.
|
||||||
its first sync.
|
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -183,11 +182,9 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
|
||||||
What you get with Obsidian integration:
|
What you get with Obsidian integration:
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="list-disc space-y-1 pl-5 text-[10px] text-muted-foreground sm:text-xs">
|
<ul className="list-disc space-y-1 pl-5 text-[10px] text-muted-foreground sm:text-xs">
|
||||||
{getConnectorBenefits(EnumConnectorName.OBSIDIAN_CONNECTOR)?.map(
|
{getConnectorBenefits(EnumConnectorName.OBSIDIAN_CONNECTOR)?.map((benefit) => (
|
||||||
(benefit) => (
|
<li key={benefit}>{benefit}</li>
|
||||||
<li key={benefit}>{benefit}</li>
|
))}
|
||||||
)
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -117,9 +117,7 @@ const PluginStats: FC<{ config: Record<string, unknown> }> = ({ config }) => {
|
||||||
label: "Files synced",
|
label: "Files synced",
|
||||||
value:
|
value:
|
||||||
placeholder ??
|
placeholder ??
|
||||||
(typeof stats?.files_synced === "number"
|
(typeof stats?.files_synced === "number" ? stats.files_synced.toLocaleString() : "—"),
|
||||||
? stats.files_synced.toLocaleString()
|
|
||||||
: "—"),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [config.vault_name, stats, statsError]);
|
}, [config.vault_name, stats, statsError]);
|
||||||
|
|
@ -139,10 +137,7 @@ const PluginStats: FC<{ config: Record<string, unknown> }> = ({ config }) => {
|
||||||
<h3 className="mb-3 text-sm font-medium sm:text-base">Vault status</h3>
|
<h3 className="mb-3 text-sm font-medium sm:text-base">Vault status</h3>
|
||||||
<dl className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
<dl className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
{tileRows.map((stat) => (
|
{tileRows.map((stat) => (
|
||||||
<div
|
<div key={stat.label} className="rounded-lg bg-background/50 p-3">
|
||||||
key={stat.label}
|
|
||||||
className="rounded-lg bg-background/50 p-3"
|
|
||||||
>
|
|
||||||
<dt className="text-xs tracking-wide text-muted-foreground sm:text-sm">
|
<dt className="text-xs tracking-wide text-muted-foreground sm:text-sm">
|
||||||
{stat.label}
|
{stat.label}
|
||||||
</dt>
|
</dt>
|
||||||
|
|
@ -160,8 +155,8 @@ const UnknownConnectorState: FC = () => (
|
||||||
<Info className="size-4 shrink-0" />
|
<Info className="size-4 shrink-0" />
|
||||||
<AlertTitle className="text-xs sm:text-sm">Unrecognized config</AlertTitle>
|
<AlertTitle className="text-xs sm:text-sm">Unrecognized config</AlertTitle>
|
||||||
<AlertDescription className="text-[11px] sm:text-xs">
|
<AlertDescription className="text-[11px] sm:text-xs">
|
||||||
This connector has neither plugin metadata nor a legacy marker. It may predate migration —
|
This connector has neither plugin metadata nor a legacy marker. It may predate migration — you
|
||||||
you can safely delete it and re-install the SurfSense Obsidian plugin to resume syncing.
|
can safely delete it and re-install the SurfSense Obsidian plugin to resume syncing.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -349,12 +349,7 @@ export const AUTO_INDEX_CONNECTOR_TYPES = new Set<string>(Object.keys(AUTO_INDEX
|
||||||
// `lib/posthog/events.ts` or per-connector tracking code.
|
// `lib/posthog/events.ts` or per-connector tracking code.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export type ConnectorTelemetryGroup =
|
export type ConnectorTelemetryGroup = "oauth" | "composio" | "crawler" | "other" | "unknown";
|
||||||
| "oauth"
|
|
||||||
| "composio"
|
|
||||||
| "crawler"
|
|
||||||
| "other"
|
|
||||||
| "unknown";
|
|
||||||
|
|
||||||
export interface ConnectorTelemetryMeta {
|
export interface ConnectorTelemetryMeta {
|
||||||
connector_type: string;
|
connector_type: string;
|
||||||
|
|
@ -363,45 +358,44 @@ export interface ConnectorTelemetryMeta {
|
||||||
is_oauth: boolean;
|
is_oauth: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONNECTOR_TELEMETRY_REGISTRY: ReadonlyMap<string, ConnectorTelemetryMeta> =
|
const CONNECTOR_TELEMETRY_REGISTRY: ReadonlyMap<string, ConnectorTelemetryMeta> = (() => {
|
||||||
(() => {
|
const map = new Map<string, ConnectorTelemetryMeta>();
|
||||||
const map = new Map<string, ConnectorTelemetryMeta>();
|
|
||||||
|
|
||||||
for (const c of OAUTH_CONNECTORS) {
|
for (const c of OAUTH_CONNECTORS) {
|
||||||
map.set(c.connectorType, {
|
map.set(c.connectorType, {
|
||||||
connector_type: c.connectorType,
|
connector_type: c.connectorType,
|
||||||
connector_title: c.title,
|
connector_title: c.title,
|
||||||
connector_group: "oauth",
|
connector_group: "oauth",
|
||||||
is_oauth: true,
|
is_oauth: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const c of COMPOSIO_CONNECTORS) {
|
for (const c of COMPOSIO_CONNECTORS) {
|
||||||
map.set(c.connectorType, {
|
map.set(c.connectorType, {
|
||||||
connector_type: c.connectorType,
|
connector_type: c.connectorType,
|
||||||
connector_title: c.title,
|
connector_title: c.title,
|
||||||
connector_group: "composio",
|
connector_group: "composio",
|
||||||
is_oauth: true,
|
is_oauth: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const c of CRAWLERS) {
|
for (const c of CRAWLERS) {
|
||||||
map.set(c.connectorType, {
|
map.set(c.connectorType, {
|
||||||
connector_type: c.connectorType,
|
connector_type: c.connectorType,
|
||||||
connector_title: c.title,
|
connector_title: c.title,
|
||||||
connector_group: "crawler",
|
connector_group: "crawler",
|
||||||
is_oauth: false,
|
is_oauth: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const c of OTHER_CONNECTORS) {
|
for (const c of OTHER_CONNECTORS) {
|
||||||
map.set(c.connectorType, {
|
map.set(c.connectorType, {
|
||||||
connector_type: c.connectorType,
|
connector_type: c.connectorType,
|
||||||
connector_title: c.title,
|
connector_title: c.title,
|
||||||
connector_group: "other",
|
connector_group: "other",
|
||||||
is_oauth: false,
|
is_oauth: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns telemetry metadata for a connector_type, or a minimal "unknown"
|
* Returns telemetry metadata for a connector_type, or a minimal "unknown"
|
||||||
|
|
|
||||||
|
|
@ -350,11 +350,7 @@ export const useConnectorDialog = () => {
|
||||||
// Set connecting state immediately to disable button and show spinner
|
// Set connecting state immediately to disable button and show spinner
|
||||||
setConnectingId(connector.id);
|
setConnectingId(connector.id);
|
||||||
|
|
||||||
trackConnectorSetupStarted(
|
trackConnectorSetupStarted(Number(searchSpaceId), connector.connectorType, "oauth_click");
|
||||||
Number(searchSpaceId),
|
|
||||||
connector.connectorType,
|
|
||||||
"oauth_click"
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if authEndpoint already has query parameters
|
// Check if authEndpoint already has query parameters
|
||||||
|
|
@ -478,11 +474,7 @@ export const useConnectorDialog = () => {
|
||||||
(connectorType: string) => {
|
(connectorType: string) => {
|
||||||
if (!searchSpaceId) return;
|
if (!searchSpaceId) return;
|
||||||
|
|
||||||
trackConnectorSetupStarted(
|
trackConnectorSetupStarted(Number(searchSpaceId), connectorType, "non_oauth_click");
|
||||||
Number(searchSpaceId),
|
|
||||||
connectorType,
|
|
||||||
"non_oauth_click"
|
|
||||||
);
|
|
||||||
|
|
||||||
setConnectingConnectorType(connectorType);
|
setConnectingConnectorType(connectorType);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -210,8 +210,7 @@ export function FreeChatPage() {
|
||||||
trackAnonymousChatMessageSent({
|
trackAnonymousChatMessageSent({
|
||||||
modelSlug,
|
modelSlug,
|
||||||
messageLength: userQuery.trim().length,
|
messageLength: userQuery.trim().length,
|
||||||
hasUploadedDoc:
|
hasUploadedDoc: anonMode.isAnonymous && anonMode.uploadedDoc !== null ? true : false,
|
||||||
anonMode.isAnonymous && anonMode.uploadedDoc !== null ? true : false,
|
|
||||||
surface: "free_chat_page",
|
surface: "free_chat_page",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -426,15 +426,50 @@ const AiSortIllustration = () => (
|
||||||
<title>AI File Sorting illustration showing automatic folder organization</title>
|
<title>AI File Sorting illustration showing automatic folder organization</title>
|
||||||
{/* Scattered documents on the left */}
|
{/* Scattered documents on the left */}
|
||||||
<g opacity="0.5">
|
<g opacity="0.5">
|
||||||
<rect x="20" y="40" width="35" height="45" rx="4" className="fill-neutral-200 dark:fill-neutral-700" transform="rotate(-8 37 62)" />
|
<rect
|
||||||
<rect x="50" y="80" width="35" height="45" rx="4" className="fill-neutral-200 dark:fill-neutral-700" transform="rotate(5 67 102)" />
|
x="20"
|
||||||
<rect x="15" y="110" width="35" height="45" rx="4" className="fill-neutral-200 dark:fill-neutral-700" transform="rotate(-3 32 132)" />
|
y="40"
|
||||||
|
width="35"
|
||||||
|
height="45"
|
||||||
|
rx="4"
|
||||||
|
className="fill-neutral-200 dark:fill-neutral-700"
|
||||||
|
transform="rotate(-8 37 62)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="50"
|
||||||
|
y="80"
|
||||||
|
width="35"
|
||||||
|
height="45"
|
||||||
|
rx="4"
|
||||||
|
className="fill-neutral-200 dark:fill-neutral-700"
|
||||||
|
transform="rotate(5 67 102)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="15"
|
||||||
|
y="110"
|
||||||
|
width="35"
|
||||||
|
height="45"
|
||||||
|
rx="4"
|
||||||
|
className="fill-neutral-200 dark:fill-neutral-700"
|
||||||
|
transform="rotate(-3 32 132)"
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{/* AI sparkle / magic in the center */}
|
{/* AI sparkle / magic in the center */}
|
||||||
<g transform="translate(140, 90)">
|
<g transform="translate(140, 90)">
|
||||||
<path d="M 0,-18 L 4,-6 L 16,-4 L 6,4 L 8,16 L 0,10 L -8,16 L -6,4 L -16,-4 L -4,-6 Z" className="fill-emerald-500 dark:fill-emerald-400" opacity="0.85">
|
<path
|
||||||
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="10s" repeatCount="indefinite" />
|
d="M 0,-18 L 4,-6 L 16,-4 L 6,4 L 8,16 L 0,10 L -8,16 L -6,4 L -16,-4 L -4,-6 Z"
|
||||||
|
className="fill-emerald-500 dark:fill-emerald-400"
|
||||||
|
opacity="0.85"
|
||||||
|
>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="rotate"
|
||||||
|
from="0"
|
||||||
|
to="360"
|
||||||
|
dur="10s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</path>
|
</path>
|
||||||
<circle cx="0" cy="0" r="3" className="fill-white dark:fill-emerald-200">
|
<circle cx="0" cy="0" r="3" className="fill-white dark:fill-emerald-200">
|
||||||
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite" />
|
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite" />
|
||||||
|
|
@ -442,51 +477,208 @@ const AiSortIllustration = () => (
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{/* Animated sorting arrows */}
|
{/* Animated sorting arrows */}
|
||||||
<g className="stroke-emerald-500 dark:stroke-emerald-400" strokeWidth="2" fill="none" opacity="0.6">
|
<g
|
||||||
|
className="stroke-emerald-500 dark:stroke-emerald-400"
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="none"
|
||||||
|
opacity="0.6"
|
||||||
|
>
|
||||||
<path d="M 100 70 Q 140 60, 180 50" strokeDasharray="4,4">
|
<path d="M 100 70 Q 140 60, 180 50" strokeDasharray="4,4">
|
||||||
<animate attributeName="stroke-dashoffset" from="8" to="0" dur="1s" repeatCount="indefinite" />
|
<animate
|
||||||
|
attributeName="stroke-dashoffset"
|
||||||
|
from="8"
|
||||||
|
to="0"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</path>
|
</path>
|
||||||
<path d="M 100 100 Q 140 100, 180 100" strokeDasharray="4,4">
|
<path d="M 100 100 Q 140 100, 180 100" strokeDasharray="4,4">
|
||||||
<animate attributeName="stroke-dashoffset" from="8" to="0" dur="1s" repeatCount="indefinite" />
|
<animate
|
||||||
|
attributeName="stroke-dashoffset"
|
||||||
|
from="8"
|
||||||
|
to="0"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</path>
|
</path>
|
||||||
<path d="M 100 130 Q 140 140, 180 150" strokeDasharray="4,4">
|
<path d="M 100 130 Q 140 140, 180 150" strokeDasharray="4,4">
|
||||||
<animate attributeName="stroke-dashoffset" from="8" to="0" dur="1s" repeatCount="indefinite" />
|
<animate
|
||||||
|
attributeName="stroke-dashoffset"
|
||||||
|
from="8"
|
||||||
|
to="0"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</path>
|
</path>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{/* Organized folder tree on the right */}
|
{/* Organized folder tree on the right */}
|
||||||
{/* Root folder */}
|
{/* Root folder */}
|
||||||
<g>
|
<g>
|
||||||
<rect x="220" y="30" width="160" height="28" rx="6" className="fill-white dark:fill-neutral-800" opacity="0.9" />
|
<rect
|
||||||
<rect x="228" y="36" width="16" height="14" rx="3" className="fill-emerald-500 dark:fill-emerald-400" />
|
x="220"
|
||||||
<line x1="252" y1="43" x2="330" y2="43" className="stroke-neutral-400 dark:stroke-neutral-500" strokeWidth="2.5" strokeLinecap="round" />
|
y="30"
|
||||||
|
width="160"
|
||||||
|
height="28"
|
||||||
|
rx="6"
|
||||||
|
className="fill-white dark:fill-neutral-800"
|
||||||
|
opacity="0.9"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="228"
|
||||||
|
y="36"
|
||||||
|
width="16"
|
||||||
|
height="14"
|
||||||
|
rx="3"
|
||||||
|
className="fill-emerald-500 dark:fill-emerald-400"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="252"
|
||||||
|
y1="43"
|
||||||
|
x2="330"
|
||||||
|
y2="43"
|
||||||
|
className="stroke-neutral-400 dark:stroke-neutral-500"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{/* Subfolder 1 */}
|
{/* Subfolder 1 */}
|
||||||
<g>
|
<g>
|
||||||
<line x1="240" y1="58" x2="240" y2="76" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
|
<line
|
||||||
<line x1="240" y1="76" x2="250" y2="76" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
|
x1="240"
|
||||||
<rect x="250" y="64" width="130" height="24" rx="5" className="fill-white dark:fill-neutral-800" opacity="0.85" />
|
y1="58"
|
||||||
<rect x="257" y="70" width="12" height="11" rx="2" className="fill-teal-400 dark:fill-teal-500" />
|
x2="240"
|
||||||
<line x1="276" y1="76" x2="340" y2="76" className="stroke-neutral-400 dark:stroke-neutral-500" strokeWidth="2" strokeLinecap="round" />
|
y2="76"
|
||||||
|
className="stroke-neutral-300 dark:stroke-neutral-600"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="240"
|
||||||
|
y1="76"
|
||||||
|
x2="250"
|
||||||
|
y2="76"
|
||||||
|
className="stroke-neutral-300 dark:stroke-neutral-600"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="250"
|
||||||
|
y="64"
|
||||||
|
width="130"
|
||||||
|
height="24"
|
||||||
|
rx="5"
|
||||||
|
className="fill-white dark:fill-neutral-800"
|
||||||
|
opacity="0.85"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="257"
|
||||||
|
y="70"
|
||||||
|
width="12"
|
||||||
|
height="11"
|
||||||
|
rx="2"
|
||||||
|
className="fill-teal-400 dark:fill-teal-500"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="276"
|
||||||
|
y1="76"
|
||||||
|
x2="340"
|
||||||
|
y2="76"
|
||||||
|
className="stroke-neutral-400 dark:stroke-neutral-500"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{/* Subfolder 2 */}
|
{/* Subfolder 2 */}
|
||||||
<g>
|
<g>
|
||||||
<line x1="240" y1="76" x2="240" y2="108" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
|
<line
|
||||||
<line x1="240" y1="108" x2="250" y2="108" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
|
x1="240"
|
||||||
<rect x="250" y="96" width="130" height="24" rx="5" className="fill-white dark:fill-neutral-800" opacity="0.85" />
|
y1="76"
|
||||||
<rect x="257" y="102" width="12" height="11" rx="2" className="fill-cyan-400 dark:fill-cyan-500" />
|
x2="240"
|
||||||
<line x1="276" y1="108" x2="350" y2="108" className="stroke-neutral-400 dark:stroke-neutral-500" strokeWidth="2" strokeLinecap="round" />
|
y2="108"
|
||||||
|
className="stroke-neutral-300 dark:stroke-neutral-600"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="240"
|
||||||
|
y1="108"
|
||||||
|
x2="250"
|
||||||
|
y2="108"
|
||||||
|
className="stroke-neutral-300 dark:stroke-neutral-600"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="250"
|
||||||
|
y="96"
|
||||||
|
width="130"
|
||||||
|
height="24"
|
||||||
|
rx="5"
|
||||||
|
className="fill-white dark:fill-neutral-800"
|
||||||
|
opacity="0.85"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="257"
|
||||||
|
y="102"
|
||||||
|
width="12"
|
||||||
|
height="11"
|
||||||
|
rx="2"
|
||||||
|
className="fill-cyan-400 dark:fill-cyan-500"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="276"
|
||||||
|
y1="108"
|
||||||
|
x2="350"
|
||||||
|
y2="108"
|
||||||
|
className="stroke-neutral-400 dark:stroke-neutral-500"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{/* Subfolder 3 */}
|
{/* Subfolder 3 */}
|
||||||
<g>
|
<g>
|
||||||
<line x1="240" y1="108" x2="240" y2="140" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
|
<line
|
||||||
<line x1="240" y1="140" x2="250" y2="140" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
|
x1="240"
|
||||||
<rect x="250" y="128" width="130" height="24" rx="5" className="fill-white dark:fill-neutral-800" opacity="0.85" />
|
y1="108"
|
||||||
<rect x="257" y="134" width="12" height="11" rx="2" className="fill-emerald-400 dark:fill-emerald-500" />
|
x2="240"
|
||||||
<line x1="276" y1="140" x2="325" y2="140" className="stroke-neutral-400 dark:stroke-neutral-500" strokeWidth="2" strokeLinecap="round" />
|
y2="140"
|
||||||
|
className="stroke-neutral-300 dark:stroke-neutral-600"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="240"
|
||||||
|
y1="140"
|
||||||
|
x2="250"
|
||||||
|
y2="140"
|
||||||
|
className="stroke-neutral-300 dark:stroke-neutral-600"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="250"
|
||||||
|
y="128"
|
||||||
|
width="130"
|
||||||
|
height="24"
|
||||||
|
rx="5"
|
||||||
|
className="fill-white dark:fill-neutral-800"
|
||||||
|
opacity="0.85"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="257"
|
||||||
|
y="134"
|
||||||
|
width="12"
|
||||||
|
height="11"
|
||||||
|
rx="2"
|
||||||
|
className="fill-emerald-400 dark:fill-emerald-500"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="276"
|
||||||
|
y1="140"
|
||||||
|
x2="325"
|
||||||
|
y2="140"
|
||||||
|
className="stroke-neutral-400 dark:stroke-neutral-500"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{/* Sparkle accents */}
|
{/* Sparkle accents */}
|
||||||
|
|
@ -495,10 +687,22 @@ const AiSortIllustration = () => (
|
||||||
<animate attributeName="opacity" values="0;1;0" dur="2s" repeatCount="indefinite" />
|
<animate attributeName="opacity" values="0;1;0" dur="2s" repeatCount="indefinite" />
|
||||||
</circle>
|
</circle>
|
||||||
<circle cx="190" cy="155" r="1.5" className="fill-teal-400">
|
<circle cx="190" cy="155" r="1.5" className="fill-teal-400">
|
||||||
<animate attributeName="opacity" values="0;1;0" dur="2.5s" begin="0.8s" repeatCount="indefinite" />
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0;1;0"
|
||||||
|
dur="2.5s"
|
||||||
|
begin="0.8s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</circle>
|
</circle>
|
||||||
<circle cx="155" cy="120" r="1.5" className="fill-cyan-400">
|
<circle cx="155" cy="120" r="1.5" className="fill-cyan-400">
|
||||||
<animate attributeName="opacity" values="0;1;0" dur="3s" begin="0.4s" repeatCount="indefinite" />
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0;1;0"
|
||||||
|
dur="3s"
|
||||||
|
begin="0.4s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</circle>
|
</circle>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
|
@ -546,35 +546,35 @@ export function DocumentUploadTab({
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="flex flex-col items-center gap-4 py-12 px-4 cursor-pointer w-full bg-transparent outline-none select-none"
|
className="flex flex-col items-center gap-4 py-12 px-4 cursor-pointer w-full bg-transparent outline-none select-none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isElectron) fileInputRef.current?.click();
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!isElectron) fileInputRef.current?.click();
|
if (!isElectron) fileInputRef.current?.click();
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
<Upload className="h-10 w-10 text-muted-foreground" />
|
e.preventDefault();
|
||||||
<div className="text-center space-y-1.5">
|
if (!isElectron) fileInputRef.current?.click();
|
||||||
<p className="text-base font-medium">
|
}
|
||||||
{isElectron ? t("select_files_or_folder") : t("tap_select_files_or_folder")}
|
}}
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">{t("file_size_limit")}</p>
|
|
||||||
</div>
|
|
||||||
<fieldset
|
|
||||||
className="w-full mt-1 border-none p-0 m-0"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onKeyDown={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
{renderBrowseButton({ fullWidth: true })}
|
<Upload className="h-10 w-10 text-muted-foreground" />
|
||||||
</fieldset>
|
<div className="text-center space-y-1.5">
|
||||||
</div>
|
<p className="text-base font-medium">
|
||||||
|
{isElectron ? t("select_files_or_folder") : t("tap_select_files_or_folder")}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">{t("file_size_limit")}</p>
|
||||||
|
</div>
|
||||||
|
<fieldset
|
||||||
|
className="w-full mt-1 border-none p-0 m-0"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{renderBrowseButton({ fullWidth: true })}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue