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 08c1dd30c..b4bd76e8f 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 @@ -1,314 +1,212 @@ "use client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Info } from "lucide-react"; -import type { FC } from "react"; -import { useRef, useState } from "react"; -import { useForm } from "react-hook-form"; -import * as z from "zod"; +import { Check, Copy, Download, Info, KeyRound, Settings2 } from "lucide-react"; +import { type FC, useCallback, useRef, useState } from "react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; +import { Button } from "@/components/ui/button"; +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 obsidianConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - vault_path: z.string().min(1, { - message: "Vault path is required.", - }), - vault_name: z.string().min(1, { - message: "Vault name is required.", - }), - exclude_folders: z.string().optional(), - include_attachments: z.boolean(), -}); +const PLUGIN_RELEASES_URL = + "https://github.com/MODSetter/SurfSense/releases?q=obsidian&expanded=true"; -type ObsidianConnectorFormValues = z.infer; +const BACKEND_URL = + process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? "https://api.surfsense.com"; -export const ObsidianConnectForm: FC = ({ onSubmit, isSubmitting }) => { - const isSubmittingRef = useRef(false); - const [periodicEnabled, setPeriodicEnabled] = useState(true); - const [frequencyMinutes, setFrequencyMinutes] = useState("60"); - const form = useForm({ - resolver: zodResolver(obsidianConnectorFormSchema), - defaultValues: { - name: "Obsidian Vault", - vault_path: "", - vault_name: "", - exclude_folders: ".obsidian,.trash", - include_attachments: false, - }, - }); +/** + * Obsidian connect form for the plugin-only architecture. + * + * The legacy `vault_path` form was removed because it only worked on + * self-hosted with a server-side bind mount and broke for everyone else. + * The plugin pushes data over HTTPS so this UI is purely instructional — + * there is no backend create call here. The connector row is created + * server-side the first time the plugin calls `POST /obsidian/connect`. + * + * The footer "Connect" button in `ConnectorConnectView` triggers this + * form's submit; we just close the dialog (`onBack()`) since there's + * nothing to validate or persist from this side. + */ +export const ObsidianConnectForm: FC = ({ onBack }) => { + const { apiKey, isLoading, copied, copyToClipboard } = useApiKey(); + const [copiedUrl, setCopiedUrl] = useState(false); + const urlCopyTimerRef = useRef | undefined>( + undefined + ); - const handleSubmit = async (values: ObsidianConnectorFormValues) => { - // Prevent multiple submissions - if (isSubmittingRef.current || isSubmitting) { - return; - } + const copyServerUrl = useCallback(async () => { + const ok = await copyToClipboardUtil(BACKEND_URL); + if (!ok) return; + setCopiedUrl(true); + if (urlCopyTimerRef.current) clearTimeout(urlCopyTimerRef.current); + urlCopyTimerRef.current = setTimeout(() => setCopiedUrl(false), 2000); + }, []); - isSubmittingRef.current = true; - try { - // Parse exclude_folders into an array - const excludeFolders = values.exclude_folders - ? values.exclude_folders - .split(",") - .map((f) => f.trim()) - .filter(Boolean) - : [".obsidian", ".trash"]; - - await onSubmit({ - name: values.name, - connector_type: EnumConnectorName.OBSIDIAN_CONNECTOR, - config: { - vault_path: values.vault_path, - vault_name: values.vault_name, - exclude_folders: excludeFolders, - include_attachments: values.include_attachments, - }, - is_indexable: true, - is_active: true, - last_indexed_at: null, - periodic_indexing_enabled: periodicEnabled, - indexing_frequency_minutes: periodicEnabled ? Number.parseInt(frequencyMinutes, 10) : null, - next_scheduled_at: null, - periodicEnabled, - frequencyMinutes, - }); - } finally { - isSubmittingRef.current = false; - } + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + onBack(); }; return (
- + {/* Form is intentionally empty so the footer Connect button is a no-op + that just closes the dialog (see component-level docstring). */} +
+ + - Self-Hosted Only + Plugin-based sync - This connector requires direct file system access and only works with self-hosted - SurfSense installations. + SurfSense now syncs Obsidian via an official plugin that runs inside + Obsidian itself. Works on desktop and mobile, in cloud and self-hosted + deployments — no server-side vault mounts required. -
- - - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> + {/* Step 1 — Install plugin */} +
+
+
+ 1 +
+

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. +

+ + + +
- ( - - Vault Path - - - - - The absolute path to your Obsidian vault on the server. This must be accessible - from the SurfSense backend. - - - - )} - /> + {/* Step 2 — Copy API key */} +
+
+
+ 2 +
+

+ 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. +

- ( - - Vault Name - - - - - A display name for your vault. This will be used in search results. - - - - )} - /> - - ( - - Exclude Folders - - - - - Comma-separated list of folder names to exclude from indexing. - - - - )} - /> - - ( - -
- Include Attachments - - Index attachment folders and embedded files (images, PDFs, etc.) - -
- - - -
- )} - /> - - {/* Indexing Configuration */} -
-

Indexing Configuration

- - {/* Periodic Sync Config */} -
-
-
-

Enable Periodic Sync

-

- Automatically re-index at regular intervals -

-
- -
- - {periodicEnabled && ( -
-
- - -
-
- )} -
+ {isLoading ? ( +
+ ) : apiKey ? ( +
+
+

+ {apiKey} +

- - -
+ +
+ ) : ( +

+ No API key available — try refreshing the page. +

+ )} +
+ + {/* Step 3 — Server URL */} +
+
+
+ 3 +
+

+ Point the plugin at this server +

+
+

+ Paste this URL into the plugin's Server URL{" "} + setting. We auto-detect it from your current dashboard origin. +

+
+
+

+ {BACKEND_URL} +

+
+ +
+
+ + {/* Step 4 — Pick search space */} +
+
+
+ 4 +
+

+ 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. +

+
- {/* What you get section */} {getConnectorBenefits(EnumConnectorName.OBSIDIAN_CONNECTOR) && ( -
-

+
+

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/connect-forms/connector-benefits.ts b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/connector-benefits.ts index 0dc093100..f4883fa36 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/connector-benefits.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/connector-benefits.ts @@ -104,11 +104,11 @@ export function getConnectorBenefits(connectorType: string): string[] | null { "No manual indexing required - meetings are added automatically", ], OBSIDIAN_CONNECTOR: [ - "Search through all your Obsidian notes and knowledge base", - "Access note content with YAML frontmatter metadata preserved", - "Wiki-style links ([[note]]) and #tags are indexed", - "Connect your personal knowledge base directly to your search space", - "Incremental sync - only changed files are re-indexed", + "Search through all of your Obsidian notes", + "Realtime sync as you create, edit, rename, or delete notes", + "YAML frontmatter, [[wiki links]], and #tags are preserved and indexed", + "Open any chat citation straight back in Obsidian via deep links", + "Each device is identifiable, so you can revoke a vault from one machine", "Full support for your vault's folder structure", ], }; 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 3da1d6e7e..acea1c51b 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 @@ -1,94 +1,58 @@ "use client"; -import type { FC } from "react"; -import { useState } from "react"; +import { AlertTriangle, Check, Copy, Download, Info } from "lucide-react"; +import { type FC, useCallback, useMemo, useRef, useState } from "react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; +import { useApiKey } from "@/hooks/use-api-key"; +import { copyToClipboard as copyToClipboardUtil } from "@/lib/utils"; import type { ConnectorConfigProps } from "../index"; export interface ObsidianConfigProps extends ConnectorConfigProps { onNameChange?: (name: string) => void; } +const PLUGIN_RELEASES_URL = + "https://github.com/MODSetter/SurfSense/releases?q=obsidian&expanded=true"; + +function formatTimestamp(value: unknown): string { + if (typeof value !== "string" || !value) return "—"; + const d = new Date(value); + if (Number.isNaN(d.getTime())) return value; + return d.toLocaleString(); +} + +/** + * Obsidian connector config view. + * + * Renders one of two modes depending on the connector's `config`: + * + * 1. **Plugin connector** (`config.source === "plugin"`) — read-only stats + * panel showing what the plugin most recently reported. + * 2. **Legacy server-path connector** (`config.legacy === true`, set by the + * Phase 3 alembic) — migration banner plus an "Install Plugin" CTA. + * The user's existing notes stay searchable; only background sync stops. + */ export const ObsidianConfig: FC = ({ connector, - onConfigChange, onNameChange, }) => { - const [vaultPath, setVaultPath] = useState( - (connector.config?.vault_path as string) || "" - ); - const [vaultName, setVaultName] = useState( - (connector.config?.vault_name as string) || "" - ); - const [excludeFolders, setExcludeFolders] = useState(() => { - const folders = connector.config?.exclude_folders; - if (Array.isArray(folders)) { - return folders.join(", "); - } - return (folders as string) || ".obsidian, .trash"; - }); - const [includeAttachments, setIncludeAttachments] = useState( - (connector.config?.include_attachments as boolean) || false - ); const [name, setName] = useState(connector.name || ""); - - const handleVaultPathChange = (value: string) => { - setVaultPath(value); - if (onConfigChange) { - onConfigChange({ - ...connector.config, - vault_path: value, - }); - } - }; - - const handleVaultNameChange = (value: string) => { - setVaultName(value); - if (onConfigChange) { - onConfigChange({ - ...connector.config, - vault_name: value, - }); - } - }; - - const handleExcludeFoldersChange = (value: string) => { - setExcludeFolders(value); - const foldersArray = value - .split(",") - .map((f) => f.trim()) - .filter(Boolean); - if (onConfigChange) { - onConfigChange({ - ...connector.config, - exclude_folders: foldersArray, - }); - } - }; - - const handleIncludeAttachmentsChange = (value: boolean) => { - setIncludeAttachments(value); - if (onConfigChange) { - onConfigChange({ - ...connector.config, - include_attachments: value, - }); - } - }; + const config = (connector.config ?? {}) as Record; + const isLegacy = config.legacy === true; + const isPlugin = config.source === "plugin"; const handleNameChange = (value: string) => { setName(value); - if (onNameChange) { - onNameChange(value); - } + onNameChange?.(value); }; return (
- {/* Connector Name */} -
+ {/* Connector name (always editable) */} +
= ({
- {/* Configuration */} -
-
-

- Vault Configuration -

-
+ {isLegacy ? ( + + ) : isPlugin ? ( + + ) : ( + + )} +
+ ); +}; -
-
- - handleVaultPathChange(e.target.value)} - placeholder="/path/to/your/obsidian/vault" - className="border-slate-400/20 focus-visible:border-slate-400/40 font-mono" - /> -

- The absolute path to your Obsidian vault on the server. -

-
+const LegacyBanner: FC = () => { + return ( +
+ + + + This connector has been migrated + + + This Obsidian connector used the legacy server-path method, which has + been removed. To resume syncing, install the SurfSense Obsidian + plugin and connect with this account. Your existing notes remain + searchable. After the plugin re-indexes your vault, you can delete + this connector to remove older copies. + + -
- - handleVaultNameChange(e.target.value)} - placeholder="My Knowledge Base" - className="border-slate-400/20 focus-visible:border-slate-400/40" - /> -

- A display name for your vault in search results. -

-
+ + + -
- - handleExcludeFoldersChange(e.target.value)} - placeholder=".obsidian, .trash, templates" - className="border-slate-400/20 focus-visible:border-slate-400/40 font-mono" - /> -

- Comma-separated list of folder names to exclude from indexing. -

-
+ +
+ ); +}; -
-
- -

- Index attachment folders and embedded files +const PluginStats: FC<{ config: Record }> = ({ config }) => { + const stats: { label: string; value: string }[] = useMemo(() => { + const filesSynced = config.files_synced; + return [ + { label: "Vault", value: (config.vault_name as string) || "—" }, + { + label: "Plugin version", + value: (config.plugin_version as string) || "—", + }, + { + label: "Device", + value: + (config.device_label as string) || + (config.device_id as string) || + "—", + }, + { + label: "Last sync", + value: formatTimestamp(config.last_sync_at), + }, + { + label: "Files synced", + value: + typeof filesSynced === "number" ? filesSynced.toLocaleString() : "—", + }, + ]; + }, [config]); + + return ( +

+ + + Plugin connected + + Edits in Obsidian sync over HTTPS. To stop syncing, disable or + uninstall the plugin in Obsidian, or delete this connector. + + + +
+

Vault status

+
+ {stats.map((stat) => ( +
+
+ {stat.label} +
+
+ {stat.value} +
+
+ ))} +
+
+
+ ); +}; + +const UnknownConnectorState: FC = () => ( + + + Unrecognized config + + This connector has neither plugin metadata nor a legacy marker. It may + predate the migration — you can safely delete it and re-install the + SurfSense Obsidian plugin to resume syncing. + + +); + +const ApiKeyReminder: FC = () => { + const { apiKey, isLoading, copied, copyToClipboard } = useApiKey(); + const [copiedUrl, setCopiedUrl] = useState(false); + const urlCopyTimerRef = useRef | undefined>( + undefined + ); + + const backendUrl = + process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? "https://api.surfsense.com"; + + const copyServerUrl = useCallback(async () => { + const ok = await copyToClipboardUtil(backendUrl); + if (!ok) return; + setCopiedUrl(true); + if (urlCopyTimerRef.current) clearTimeout(urlCopyTimerRef.current); + urlCopyTimerRef.current = setTimeout(() => setCopiedUrl(false), 2000); + }, [backendUrl]); + + return ( +
+

+ Plugin connection details +

+

+ Paste these into the plugin's settings inside Obsidian. +

+ +
+ + {isLoading ? ( +
+ ) : ( +
+
+

+ {apiKey || "No API key available"}

- +
+ )} +

+ Token expires after 24 hours; long-lived tokens are coming in a + future release. +

+
+ +
+ +
+
+

+ {backendUrl} +

+
+
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 da6885ffe..86d214134 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 @@ -180,7 +180,7 @@ export const OTHER_CONNECTORS = [ { id: "obsidian-connector", title: "Obsidian", - description: "Index your Obsidian vault (Local folder scan on Desktop)", + description: "Sync your Obsidian vault on desktop or mobile via the SurfSense plugin", connectorType: EnumConnectorName.OBSIDIAN_CONNECTOR, }, ] as const;