diff --git a/.cursor/skills/playwright-testing/browser-apis/iframes.md b/.cursor/skills/playwright-testing/browser-apis/iframes.md index 145e050ff..155cc1c1b 100644 --- a/.cursor/skills/playwright-testing/browser-apis/iframes.md +++ b/.cursor/skills/playwright-testing/browser-apis/iframes.md @@ -372,7 +372,7 @@ test("mock iframe response", async ({ page }) => {

Mocked Widget

- +

Mocked widget content

`, diff --git a/.cursor/skills/playwright-testing/core/locators.md b/.cursor/skills/playwright-testing/core/locators.md index f806635d6..afe3af361 100644 --- a/.cursor/skills/playwright-testing/core/locators.md +++ b/.cursor/skills/playwright-testing/core/locators.md @@ -100,7 +100,7 @@ use: { Usage: ```typescript -// HTML: +// React: page.getByTestId("submit-btn"); ``` diff --git a/.cursor/skills/vercel-react-best-practices/AGENTS.md b/.cursor/skills/vercel-react-best-practices/AGENTS.md index 94c3c8441..2b839ab51 100644 --- a/.cursor/skills/vercel-react-best-practices/AGENTS.md +++ b/.cursor/skills/vercel-react-best-practices/AGENTS.md @@ -549,6 +549,8 @@ Preload heavy bundles before they're needed to reduce perceived latency. **Example: preload on hover/focus** ```tsx +import { Button } from '@/components/ui/button' + function EditorButton({ onClick }: { onClick: () => void }) { const preload = () => { if (typeof window !== 'undefined') { @@ -557,13 +559,13 @@ function EditorButton({ onClick }: { onClick: () => void }) { } return ( - + ) } ``` @@ -1239,11 +1241,12 @@ function StaticContent() { **For mutations:** ```tsx +import { Button } from '@/components/ui/button' import { useSWRMutation } from 'swr/mutation' function UpdateButton() { const { trigger } = useSWRMutation('/api/user', updateUser) - return + return } ``` @@ -1369,6 +1372,8 @@ Don't subscribe to dynamic state (searchParams, localStorage) if you only read i **Incorrect: subscribes to all searchParams changes** ```tsx +import { Button } from '@/components/ui/button' + function ShareButton({ chatId }: { chatId: string }) { const searchParams = useSearchParams() @@ -1377,13 +1382,15 @@ function ShareButton({ chatId }: { chatId: string }) { shareChat(chatId, { ref }) } - return + return } ``` **Correct: reads on demand, no subscription** ```tsx +import { Button } from '@/components/ui/button' + function ShareButton({ chatId }: { chatId: string }) { const handleShare = () => { const params = new URLSearchParams(window.location.search) @@ -1391,7 +1398,7 @@ function ShareButton({ chatId }: { chatId: string }) { shareChat(chatId, { ref }) } - return + return } ``` @@ -1549,6 +1556,8 @@ If a side effect is triggered by a specific user action (submit, click, drag), r **Incorrect: event modeled as state + effect** ```tsx +import { Button } from '@/components/ui/button' + function Form() { const [submitted, setSubmitted] = useState(false) const theme = useContext(ThemeContext) @@ -1560,13 +1569,15 @@ function Form() { } }, [submitted, theme]) - return + return } ``` **Correct: do it in the handler** ```tsx +import { Button } from '@/components/ui/button' + function Form() { const theme = useContext(ThemeContext) @@ -1575,7 +1586,7 @@ function Form() { showToast('Registered', theme) } - return + return } ``` diff --git a/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md b/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md index 700050406..0662ef81b 100644 --- a/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md +++ b/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md @@ -12,6 +12,8 @@ Preload heavy bundles before they're needed to reduce perceived latency. **Example (preload on hover/focus):** ```tsx +import { Button } from "@/components/ui/button" + function EditorButton({ onClick }: { onClick: () => void }) { const preload = () => { if (typeof window !== 'undefined') { @@ -20,13 +22,13 @@ function EditorButton({ onClick }: { onClick: () => void }) { } return ( - + ) } ``` diff --git a/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md b/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md index 2a430f27f..22d419bca 100644 --- a/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md +++ b/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md @@ -45,11 +45,12 @@ function StaticContent() { **For mutations:** ```tsx +import { Button } from '@/components/ui/button' import { useSWRMutation } from 'swr/mutation' function UpdateButton() { const { trigger } = useSWRMutation('/api/user', updateUser) - return + return } ``` diff --git a/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md b/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md index e867c95f0..94410bc5b 100644 --- a/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +++ b/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md @@ -12,6 +12,8 @@ Don't subscribe to dynamic state (searchParams, localStorage) if you only read i **Incorrect (subscribes to all searchParams changes):** ```tsx +import { Button } from '@/components/ui/button' + function ShareButton({ chatId }: { chatId: string }) { const searchParams = useSearchParams() @@ -20,13 +22,15 @@ function ShareButton({ chatId }: { chatId: string }) { shareChat(chatId, { ref }) } - return + return } ``` **Correct (reads on demand, no subscription):** ```tsx +import { Button } from '@/components/ui/button' + function ShareButton({ chatId }: { chatId: string }) { const handleShare = () => { const params = new URLSearchParams(window.location.search) @@ -34,6 +38,6 @@ function ShareButton({ chatId }: { chatId: string }) { shareChat(chatId, { ref }) } - return + return } ``` diff --git a/.cursor/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md b/.cursor/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md index dd58a1af0..299815d69 100644 --- a/.cursor/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +++ b/.cursor/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md @@ -12,6 +12,8 @@ If a side effect is triggered by a specific user action (submit, click, drag), r **Incorrect (event modeled as state + effect):** ```tsx +import { Button } from '@/components/ui/button' + function Form() { const [submitted, setSubmitted] = useState(false) const theme = useContext(ThemeContext) @@ -23,13 +25,15 @@ function Form() { } }, [submitted, theme]) - return + return } ``` **Correct (do it in the handler):** ```tsx +import { Button } from '@/components/ui/button' + function Form() { const theme = useContext(ThemeContext) @@ -38,7 +42,7 @@ function Form() { showToast('Registered', theme) } - return + return } ``` diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/tools/search_surfsense_docs.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/tools/search_surfsense_docs.py index 0d702be4c..ccc5c49e2 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/tools/search_surfsense_docs.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/tools/search_surfsense_docs.py @@ -9,6 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.db import SurfsenseDocsChunk, SurfsenseDocsDocument from app.utils.document_converters import embed_text +from app.utils.surfsense_docs import surfsense_docs_public_url def format_surfsense_docs_results(results: list[tuple]) -> str: @@ -19,13 +20,14 @@ def format_surfsense_docs_results(results: list[tuple]) -> str: # Group chunks by document grouped: dict[int, dict] = {} for chunk, doc in results: + public_url = surfsense_docs_public_url(doc.source) if doc.id not in grouped: grouped[doc.id] = { "document_id": f"doc-{doc.id}", "document_type": "SURFSENSE_DOCS", "title": doc.title, - "url": doc.source, - "metadata": {"source": doc.source}, + "url": public_url, + "metadata": {"source": doc.source, "public_url": public_url}, "chunks": [], } grouped[doc.id]["chunks"].append( diff --git a/surfsense_backend/app/agents/new_chat/tools/search_surfsense_docs.py b/surfsense_backend/app/agents/new_chat/tools/search_surfsense_docs.py index 2965f2f02..d8a0efac7 100644 --- a/surfsense_backend/app/agents/new_chat/tools/search_surfsense_docs.py +++ b/surfsense_backend/app/agents/new_chat/tools/search_surfsense_docs.py @@ -17,6 +17,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.db import SurfsenseDocsChunk, SurfsenseDocsDocument, async_session_maker from app.utils.document_converters import embed_text +from app.utils.surfsense_docs import surfsense_docs_public_url def format_surfsense_docs_results(results: list[tuple]) -> str: @@ -40,13 +41,14 @@ def format_surfsense_docs_results(results: list[tuple]) -> str: # Group chunks by document grouped: dict[int, dict] = {} for chunk, doc in results: + public_url = surfsense_docs_public_url(doc.source) if doc.id not in grouped: grouped[doc.id] = { "document_id": f"doc-{doc.id}", "document_type": "SURFSENSE_DOCS", "title": doc.title, - "url": doc.source, - "metadata": {"source": doc.source}, + "url": public_url, + "metadata": {"source": doc.source, "public_url": public_url}, "chunks": [], } grouped[doc.id]["chunks"].append( diff --git a/surfsense_backend/app/routes/surfsense_docs_routes.py b/surfsense_backend/app/routes/surfsense_docs_routes.py index e1713e8a3..0d5428dec 100644 --- a/surfsense_backend/app/routes/surfsense_docs_routes.py +++ b/surfsense_backend/app/routes/surfsense_docs_routes.py @@ -24,6 +24,7 @@ from app.schemas.surfsense_docs import ( SurfsenseDocsDocumentWithChunksRead, ) from app.users import current_active_user +from app.utils.surfsense_docs import surfsense_docs_public_url router = APIRouter() @@ -76,6 +77,7 @@ async def get_surfsense_doc_by_chunk_id( id=document.id, title=document.title, source=document.source, + public_url=surfsense_docs_public_url(document.source), content=document.content, chunks=[ SurfsenseDocsChunkRead(id=c.id, content=c.content) @@ -146,6 +148,7 @@ async def list_surfsense_docs( id=doc.id, title=doc.title, source=doc.source, + public_url=surfsense_docs_public_url(doc.source), content=doc.content, created_at=doc.created_at, updated_at=doc.updated_at, diff --git a/surfsense_backend/app/schemas/surfsense_docs.py b/surfsense_backend/app/schemas/surfsense_docs.py index ce32c0ef8..3adf25032 100644 --- a/surfsense_backend/app/schemas/surfsense_docs.py +++ b/surfsense_backend/app/schemas/surfsense_docs.py @@ -22,6 +22,7 @@ class SurfsenseDocsDocumentRead(BaseModel): id: int title: str source: str + public_url: str content: str created_at: datetime | None = None updated_at: datetime | None = None @@ -35,6 +36,7 @@ class SurfsenseDocsDocumentWithChunksRead(BaseModel): id: int title: str source: str + public_url: str content: str chunks: list[SurfsenseDocsChunkRead] diff --git a/surfsense_backend/app/tasks/chat/stream_new_chat.py b/surfsense_backend/app/tasks/chat/stream_new_chat.py index 3d639affb..9a69b6164 100644 --- a/surfsense_backend/app/tasks/chat/stream_new_chat.py +++ b/surfsense_backend/app/tasks/chat/stream_new_chat.py @@ -81,6 +81,7 @@ from app.tasks.chat.streaming.helpers.interrupt_inspector import ( ) from app.utils.content_utils import bootstrap_history_from_db from app.utils.perf import get_perf_logger, log_system_snapshot, trim_native_heap +from app.utils.surfsense_docs import surfsense_docs_public_url from app.utils.user_message_multimodal import build_human_message_content _background_tasks: set[asyncio.Task] = set() @@ -216,14 +217,17 @@ def format_mentioned_surfsense_docs_as_context( ) for doc in documents: - metadata_json = json.dumps({"source": doc.source}, ensure_ascii=False) + public_url = surfsense_docs_public_url(doc.source) + metadata_json = json.dumps( + {"source": doc.source, "public_url": public_url}, ensure_ascii=False + ) context_parts.append("") context_parts.append("") context_parts.append(f" doc-{doc.id}") context_parts.append(" SURFSENSE_DOCS") context_parts.append(f" <![CDATA[{doc.title}]]>") - context_parts.append(f" ") + context_parts.append(f" ") context_parts.append( f" " ) diff --git a/surfsense_backend/app/utils/surfsense_docs.py b/surfsense_backend/app/utils/surfsense_docs.py new file mode 100644 index 000000000..9a6ab11a9 --- /dev/null +++ b/surfsense_backend/app/utils/surfsense_docs.py @@ -0,0 +1,13 @@ +"""Utilities for SurfSense's built-in documentation index.""" + +from pathlib import PurePosixPath + +DOCS_PUBLIC_ROOT = PurePosixPath("/docs") + + +def surfsense_docs_public_url(source: str) -> str: + """Return the public docs route for an indexed documentation source path.""" + docs_path = PurePosixPath(source).with_suffix("") + if docs_path.name == "index": + docs_path = docs_path.parent + return (DOCS_PUBLIC_ROOT / docs_path).as_posix() diff --git a/surfsense_desktop/assets/icon-128.png b/surfsense_desktop/assets/icon-128.png new file mode 100644 index 000000000..8be6ee21a Binary files /dev/null and b/surfsense_desktop/assets/icon-128.png differ diff --git a/surfsense_desktop/assets/iconTemplate.png b/surfsense_desktop/assets/iconTemplate.png new file mode 100644 index 000000000..13d1c9840 Binary files /dev/null and b/surfsense_desktop/assets/iconTemplate.png differ diff --git a/surfsense_desktop/assets/iconTemplate@2x.png b/surfsense_desktop/assets/iconTemplate@2x.png new file mode 100644 index 000000000..70710f739 Binary files /dev/null and b/surfsense_desktop/assets/iconTemplate@2x.png differ diff --git a/surfsense_desktop/src/modules/tray.ts b/surfsense_desktop/src/modules/tray.ts index 5fb1acbdf..f0221fe53 100644 --- a/surfsense_desktop/src/modules/tray.ts +++ b/surfsense_desktop/src/modules/tray.ts @@ -11,11 +11,20 @@ let registeredGeneralAssist: string | null = null; let registeredScreenshotAssist: string | null = null; function getTrayIcon(): NativeImage { - const iconName = process.platform === 'win32' ? 'icon.ico' : 'icon.png'; + const iconName = + process.platform === 'darwin' + ? 'iconTemplate.png' + : process.platform === 'win32' + ? 'icon.ico' + : 'icon.png'; const iconPath = app.isPackaged ? path.join(process.resourcesPath, 'assets', iconName) : path.join(__dirname, '..', 'assets', iconName); const img = nativeImage.createFromPath(iconPath); + if (process.platform === 'darwin') { + img.setTemplateImage(true); + return img; + } return img.resize({ width: 16, height: 16 }); } diff --git a/surfsense_desktop/src/modules/window.ts b/surfsense_desktop/src/modules/window.ts index 8b7c02133..5317005d5 100644 --- a/surfsense_desktop/src/modules/window.ts +++ b/surfsense_desktop/src/modules/window.ts @@ -7,6 +7,7 @@ import { setActiveSearchSpaceId } from './active-search-space'; const isDev = !app.isPackaged; const HOSTED_FRONTEND_URL = process.env.HOSTED_FRONTEND_URL as string; +const isMac = process.platform === 'darwin'; let mainWindow: BrowserWindow | null = null; let isQuitting = false; @@ -35,7 +36,12 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow { webviewTag: false, }, show: false, - titleBarStyle: 'hiddenInset', + ...(isMac + ? { + titleBarStyle: 'hidden' as const, + trafficLightPosition: { x: 12, y: 10 }, + } + : {}), }); mainWindow.once('ready-to-show', () => { diff --git a/surfsense_web/app/(home)/announcements/layout.tsx b/surfsense_web/app/(home)/announcements/layout.tsx index e5102b85e..157666ba4 100644 --- a/surfsense_web/app/(home)/announcements/layout.tsx +++ b/surfsense_web/app/(home)/announcements/layout.tsx @@ -2,20 +2,20 @@ import type { Metadata } from "next"; import type { ReactNode } from "react"; export const metadata: Metadata = { - title: "Announcements | SurfSense", + title: "What's New | SurfSense", description: "Latest product updates, feature releases, and news from SurfSense.", alternates: { canonical: "https://www.surfsense.com/announcements", }, openGraph: { - title: "Announcements | SurfSense", + title: "What's New | SurfSense", description: "Latest product updates, feature releases, and news from SurfSense.", url: "https://www.surfsense.com/announcements", type: "website", }, twitter: { card: "summary_large_image", - title: "Announcements | SurfSense", + title: "What's New | SurfSense", description: "Latest product updates, feature releases, and news from SurfSense.", }, }; diff --git a/surfsense_web/app/(home)/announcements/page.tsx b/surfsense_web/app/(home)/announcements/page.tsx index 966c09f77..f287e43d1 100644 --- a/surfsense_web/app/(home)/announcements/page.tsx +++ b/surfsense_web/app/(home)/announcements/page.tsx @@ -24,7 +24,7 @@ export default function AnnouncementsPage() {

- Announcements + What's New

diff --git a/surfsense_web/app/(home)/login/GoogleLoginButton.tsx b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx index e22fc2798..581bfe17f 100644 --- a/surfsense_web/app/(home)/login/GoogleLoginButton.tsx +++ b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx @@ -1,15 +1,47 @@ "use client"; -import { IconBrandGoogleFilled } from "@tabler/icons-react"; -import { motion } from "motion/react"; import { useTranslations } from "next-intl"; +import { useState } from "react"; import { Logo } from "@/components/Logo"; +import { Button } from "@/components/ui/button"; import { trackLoginAttempt } from "@/lib/posthog/events"; import { AmbientBackground } from "./AmbientBackground"; +function GoogleGLogo({ className }: { className?: string }) { + return ( + + ); +} + export function GoogleLoginButton() { const t = useTranslations("auth"); + const [isRedirecting, setIsRedirecting] = useState(false); const handleGoogleLogin = () => { + if (isRedirecting) return; + setIsRedirecting(true); + // Track Google login attempt trackLoginAttempt("google"); @@ -73,21 +105,15 @@ export function GoogleLoginButton() { */} - -
-
-
-
-
-
- + {t("continue_with_google")} -
+ ); diff --git a/surfsense_web/app/(home)/login/LocalLoginForm.tsx b/surfsense_web/app/(home)/login/LocalLoginForm.tsx index a0326e39b..9692d35e1 100644 --- a/surfsense_web/app/(home)/login/LocalLoginForm.tsx +++ b/surfsense_web/app/(home)/login/LocalLoginForm.tsx @@ -7,6 +7,7 @@ import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useState } from "react"; import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms"; +import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import { getAuthErrorDetails, isNetworkError } from "@/lib/auth-errors"; import { AUTH_TYPE } from "@/lib/env-config"; @@ -120,11 +121,13 @@ export function LocalLoginForm() {

{error.title}

{error.message}

- + )} @@ -191,21 +194,23 @@ export function LocalLoginForm() { }`} disabled={isLoggingIn} /> - + - + {authType === "LOCAL" && ( diff --git a/surfsense_web/app/(home)/login/page.tsx b/surfsense_web/app/(home)/login/page.tsx index c336e757c..42a9182e9 100644 --- a/surfsense_web/app/(home)/login/page.tsx +++ b/surfsense_web/app/(home)/login/page.tsx @@ -6,6 +6,7 @@ import { useTranslations } from "next-intl"; import { Suspense, useEffect, useState } from "react"; import { toast } from "sonner"; import { Logo } from "@/components/Logo"; +import { Button } from "@/components/ui/button"; import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors"; import { setRedirectPath } from "@/lib/auth-utils"; @@ -154,10 +155,12 @@ function LoginContent() {

{urlError.title}

{urlError.message}

- + )} diff --git a/surfsense_web/app/(home)/register/page.tsx b/surfsense_web/app/(home)/register/page.tsx index 00f142567..1fd1a4ecb 100644 --- a/surfsense_web/app/(home)/register/page.tsx +++ b/surfsense_web/app/(home)/register/page.tsx @@ -9,6 +9,7 @@ import { useEffect, useState } from "react"; import { type ExternalToast, toast } from "sonner"; import { registerMutationAtom } from "@/atoms/auth/auth-mutation.atoms"; import { Logo } from "@/components/Logo"; +import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors"; import { getBearerToken } from "@/lib/auth-utils"; @@ -199,11 +200,13 @@ export default function RegisterPage() {

{error.title}

{error.message}

- + )} @@ -295,18 +298,18 @@ export default function RegisterPage() { /> - +
diff --git a/surfsense_web/app/dashboard/[search_space_id]/buy-more/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/buy-more/page.tsx index 0c5662712..74bcaff2e 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/buy-more/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/buy-more/page.tsx @@ -4,7 +4,7 @@ import { motion } from "motion/react"; import { useState } from "react"; import { BuyPagesContent } from "@/components/settings/buy-pages-content"; import { BuyTokensContent } from "@/components/settings/buy-tokens-content"; -import { cn } from "@/lib/utils"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; const TABS = [ { id: "pages", label: "Pages" }, @@ -17,33 +17,38 @@ export default function BuyMorePage() { const [activeTab, setActiveTab] = useState("pages"); return ( -
- + { + setActiveTab(value as TabId); + }} + className="relative min-h-[37rem] w-full" > -
+ {TABS.map((tab) => ( - + ))} -
+ - {activeTab === "pages" ? : } -
-
+ + + + + + + + ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index d95aab6e8..759539ce3 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -144,6 +144,19 @@ export function DashboardClientLayout({ const electronAPI = useElectronAPI(); + useEffect(() => { + const htmlBackground = document.documentElement.style.backgroundColor; + const bodyBackground = document.body.style.backgroundColor; + + document.documentElement.style.backgroundColor = "var(--panel)"; + document.body.style.backgroundColor = "var(--panel)"; + + return () => { + document.documentElement.style.backgroundColor = htmlBackground; + document.body.style.backgroundColor = bodyBackground; + }; + }, []); + useEffect(() => { if (!electronAPI?.onChatScreenCapture) return; return electronAPI.onChatScreenCapture((dataUrl: string) => { @@ -163,12 +176,13 @@ export function DashboardClientLayout({ setActiveSearchSpaceIdState(activeSeacrhSpaceId); // Sync to Electron store if stored value is null (first navigation) - if (electronAPI?.setActiveSearchSpace) { + if (electronAPI?.getActiveSearchSpace && electronAPI.setActiveSearchSpace) { + const setActiveSearchSpace = electronAPI.setActiveSearchSpace; electronAPI - .getActiveSearchSpace?.() - .then((stored) => { + .getActiveSearchSpace() + .then((stored: string | null) => { if (!stored) { - electronAPI.setActiveSearchSpace!(activeSeacrhSpaceId); + setActiveSearchSpace(activeSeacrhSpaceId); } }) .catch(() => {}); diff --git a/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx index c5be2b590..dea1e16c2 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx @@ -17,7 +17,6 @@ import { } from "@tanstack/react-table"; import { useAtomValue } from "jotai"; import { - Activity, AlertCircle, AlertTriangle, Bug, @@ -38,6 +37,7 @@ import { RefreshCw, Terminal, Trash, + Workflow, X, Zap, } from "lucide-react"; @@ -133,7 +133,6 @@ const logStatusConfig = { function MessageDetails({ message, taskName, - metadata, createdAt, children, }: { @@ -623,7 +622,7 @@ function LogsSummaryDashboard({ {t("total_logs")} - +
{summary.total_logs}
@@ -739,7 +738,7 @@ function LogsFilters({
{Boolean(filterInput) && ( + ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/loading.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/loading.tsx index 6eb9223ca..108671662 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/loading.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/loading.tsx @@ -2,42 +2,59 @@ import { Skeleton } from "@/components/ui/skeleton"; export default function Loading() { return ( -
-
- {/* User message */} -
- +
+
+
+
+ {/* User message */} +
+ +
+ + {/* Assistant message */} +
+ + + +
+ + {/* User message */} +
+ +
+ + {/* Assistant message */} +
+ + + +
+ + {/* User message */} +
+ +
- {/* Assistant message */} -
- - - -
- - {/* User message */} -
- -
- - {/* Assistant message */} -
- - - -
- - {/* User message */} -
- -
-
- - {/* Input bar */} -
-
- + {/* Input bar */} +
+
+ +
diff --git a/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx index 4dba3bbb6..3e3f41deb 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx @@ -151,7 +151,7 @@ export default function OnboardPage() { } return ( -
+
{/* Header */}
@@ -165,7 +165,7 @@ export default function OnboardPage() {
{/* Form card */} -
+
}) { + const { search_space_id } = await params; + return ; +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/image-models/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/image-models/page.tsx new file mode 100644 index 000000000..b300f8078 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/image-models/page.tsx @@ -0,0 +1,6 @@ +import { ImageModelManager } from "@/components/settings/image-model-manager"; + +export default async function Page({ params }: { params: Promise<{ search_space_id: string }> }) { + const { search_space_id } = await params; + return ; +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/layout-shell.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/layout-shell.tsx new file mode 100644 index 000000000..96d77d131 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/layout-shell.tsx @@ -0,0 +1,179 @@ +"use client"; + +import { + BookText, + Bot, + Brain, + CircleUser, + Earth, + ImageIcon, + ListChecks, + ScanEye, + UserKey, +} from "lucide-react"; +import Link from "next/link"; +import { useSelectedLayoutSegment } from "next/navigation"; +import { useTranslations } from "next-intl"; +import type React from "react"; +import { useCallback, useMemo, useState } from "react"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; + +export type SearchSpaceSettingsTab = + | "general" + | "roles" + | "models" + | "image-models" + | "vision-models" + | "team-roles" + | "prompts" + | "team-memory" + | "public-links"; + +const DEFAULT_TAB: SearchSpaceSettingsTab = "general"; + +interface SearchSpaceSettingsLayoutShellProps { + searchSpaceId: string; + children: React.ReactNode; +} + +export function SearchSpaceSettingsLayoutShell({ + searchSpaceId, + children, +}: SearchSpaceSettingsLayoutShellProps) { + const t = useTranslations("searchSpaceSettings"); + const segment = useSelectedLayoutSegment(); + const [tabScrollPos, setTabScrollPos] = useState<"start" | "middle" | "end">("start"); + + const handleTabScroll = useCallback((e: React.UIEvent) => { + const el = e.currentTarget; + const atStart = el.scrollLeft <= 2; + const atEnd = el.scrollWidth - el.scrollLeft - el.clientWidth <= 2; + setTabScrollPos(atStart ? "start" : atEnd ? "end" : "middle"); + }, []); + + const navItems = useMemo( + () => [ + { + value: "general" as const, + label: t("nav_general"), + icon: , + }, + { + value: "roles" as const, + label: t("nav_role_assignments"), + icon: , + }, + { + value: "models" as const, + label: t("nav_agent_models"), + icon: , + }, + { + value: "image-models" as const, + label: t("nav_image_models"), + icon: , + }, + { + value: "vision-models" as const, + label: t("nav_vision_models"), + icon: , + }, + { + value: "team-roles" as const, + label: t("nav_team_roles"), + icon: , + }, + { + value: "prompts" as const, + label: t("nav_system_instructions"), + icon: , + }, + { + value: "team-memory" as const, + label: "Team Memory", + icon: , + }, + { + value: "public-links" as const, + label: t("nav_public_links"), + icon: , + }, + ], + [t] + ); + + const activeTab: SearchSpaceSettingsTab = + segment && navItems.some((item) => item.value === segment) + ? (segment as SearchSpaceSettingsTab) + : DEFAULT_TAB; + const selectedLabel = navItems.find((item) => item.value === activeTab)?.label ?? t("title"); + + const hrefFor = (tab: SearchSpaceSettingsTab) => + `/dashboard/${searchSpaceId}/search-space-settings/${tab}`; + + return ( +
+
+

{t("title")}

+ +
+
+ {navItems.map((item) => ( + + {item.icon} + {item.label} + + ))} +
+
+
+ +
+
+

{selectedLabel}

+ +
+
{children}
+
+
+ ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/layout.tsx new file mode 100644 index 000000000..330158da7 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/layout.tsx @@ -0,0 +1,19 @@ +import type React from "react"; +import { use } from "react"; +import { SearchSpaceSettingsLayoutShell } from "./layout-shell"; + +export default function SearchSpaceSettingsLayout({ + params, + children, +}: { + params: Promise<{ search_space_id: string }>; + children: React.ReactNode; +}) { + const { search_space_id } = use(params); + + return ( + + {children} + + ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/models/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/models/page.tsx new file mode 100644 index 000000000..d68194782 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/models/page.tsx @@ -0,0 +1,6 @@ +import { AgentModelManager } from "@/components/settings/agent-model-manager"; + +export default async function Page({ params }: { params: Promise<{ search_space_id: string }> }) { + const { search_space_id } = await params; + return ; +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/page.tsx new file mode 100644 index 000000000..27c59328b --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/page.tsx @@ -0,0 +1,10 @@ +import { redirect } from "next/navigation"; + +export default async function SearchSpaceSettingsPage({ + params, +}: { + params: Promise<{ search_space_id: string }>; +}) { + const { search_space_id } = await params; + redirect(`/dashboard/${search_space_id}/search-space-settings/general`); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/prompts/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/prompts/page.tsx new file mode 100644 index 000000000..cc837299d --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/prompts/page.tsx @@ -0,0 +1,6 @@ +import { PromptConfigManager } from "@/components/settings/prompt-config-manager"; + +export default async function Page({ params }: { params: Promise<{ search_space_id: string }> }) { + const { search_space_id } = await params; + return ; +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/public-links/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/public-links/page.tsx new file mode 100644 index 000000000..2cddfe3e0 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/public-links/page.tsx @@ -0,0 +1,6 @@ +import { PublicChatSnapshotsManager } from "@/components/public-chat-snapshots/public-chat-snapshots-manager"; + +export default async function Page({ params }: { params: Promise<{ search_space_id: string }> }) { + const { search_space_id } = await params; + return ; +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/roles/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/roles/page.tsx new file mode 100644 index 000000000..5bad50cd3 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/roles/page.tsx @@ -0,0 +1,6 @@ +import { LLMRoleManager } from "@/components/settings/llm-role-manager"; + +export default async function Page({ params }: { params: Promise<{ search_space_id: string }> }) { + const { search_space_id } = await params; + return ; +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/team-memory/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/team-memory/page.tsx new file mode 100644 index 000000000..0652b012e --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/team-memory/page.tsx @@ -0,0 +1,6 @@ +import { TeamMemoryManager } from "@/components/settings/team-memory-manager"; + +export default async function Page({ params }: { params: Promise<{ search_space_id: string }> }) { + const { search_space_id } = await params; + return ; +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/team-roles/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/team-roles/page.tsx new file mode 100644 index 000000000..a343eaacb --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/team-roles/page.tsx @@ -0,0 +1,6 @@ +import { RolesManager } from "@/components/settings/roles-manager"; + +export default async function Page({ params }: { params: Promise<{ search_space_id: string }> }) { + const { search_space_id } = await params; + return ; +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/vision-models/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/vision-models/page.tsx new file mode 100644 index 000000000..06aea003a --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/vision-models/page.tsx @@ -0,0 +1,6 @@ +import { VisionModelManager } from "@/components/settings/vision-model-manager"; + +export default async function Page({ params }: { params: Promise<{ search_space_id: string }> }) { + const { search_space_id } = await params; + return ; +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx new file mode 100644 index 000000000..c75eaf4e4 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -0,0 +1,15 @@ +import { TeamContent } from "./team-content"; + +export default async function TeamPage({ + params, +}: { + params: Promise<{ search_space_id: string }>; +}) { + const { search_space_id } = await params; + + return ( +
+ +
+ ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx index d9ca9efb3..f003dde1b 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx @@ -1,7 +1,7 @@ "use client"; import { useQuery } from "@tanstack/react-query"; -import { useAtomValue, useSetAtom } from "jotai"; +import { useAtomValue } from "jotai"; import { Calendar, Check, @@ -20,6 +20,7 @@ import { UserPlus, Users, } from "lucide-react"; +import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { @@ -31,7 +32,6 @@ import { updateMemberMutationAtom, } from "@/atoms/members/members-mutation.atoms"; import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; -import { searchSpaceSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms"; import { AlertDialog, AlertDialogAction, @@ -240,46 +240,77 @@ export function TeamContent({ searchSpaceId }: TeamContentProps) { if (accessLoading || membersLoading) { return ( -
-
- - +
+
+ + +
+ + members +
-
+
- - - + + + + + Name + - - + + + + Last logged in + -
- -
+ + + Role +
- {SKELETON_KEYS.map((id) => ( - - + {SKELETON_KEYS.slice(0, 2).map((id) => ( + +
-
- - -
+
- +
- +
@@ -294,41 +325,63 @@ export function TeamContent({ searchSpaceId }: TeamContentProps) { return (
- {rolesLoading ? ( - - ) : ( - canInvite && ( + {canInvite && + (rolesLoading ? ( + + ) : ( - ) - )} - {invitesLoading ? ( - - ) : ( - canInvite && - activeInvites.length > 0 && ( - - ) - )} + ))} + {canInvite && + (invitesLoading ? ( + + ) : ( + activeInvites.length > 0 && ( + + ) + ))}

{members.length} {members.length === 1 ? "member" : "members"}

-
+
- - + + Name - + Last logged in @@ -346,6 +399,7 @@ export function TeamContent({ searchSpaceId }: TeamContentProps) { {owners.map((member) => ( ( Promise; onRemoveMember: (membershipId: number) => Promise; }) { - const setSearchSpaceSettingsDialog = useSetAtom(searchSpaceSettingsDialogAtom); + const router = useRouter(); const initials = getAvatarInitials(member); const displayName = member.user_display_name || member.user_email || "Unknown"; const roleName = member.is_owner ? "Owner" : member.role?.name || "No role"; const showActions = !member.is_owner && (canManageRoles || canRemove); return ( - - + +
{member.user_avatar_url && ( )} - {initials} + + {initials} +

{displayName}

@@ -474,7 +533,7 @@ function MemberRow({
- + {member.user_last_login ? formatRelativeDate(member.user_last_login) : "Never"} @@ -482,18 +541,20 @@ function MemberRow({ {showActions ? ( - + e.preventDefault()} - className="min-w-[120px] dark:bg-neutral-900 dark:border dark:border-white/5" + className="min-w-[120px]" > {canManageRoles && roles @@ -536,13 +597,10 @@ function MemberRow({ )} - + - setSearchSpaceSettingsDialog({ - open: true, - initialTab: "team-roles", - }) + router.push(`/dashboard/${searchSpaceId}/search-space-settings/team-roles`) } > Manage Roles @@ -707,7 +765,7 @@ function CreateInviteDialog({
- Allow — run without asking - Ask — pause for approval - Deny — block silently + Allow (run without asking) + Ask (pause for approval) + Deny (block silently)

{ACTION_DESCRIPTIONS[formData.action]}

- -
- - -
+ + + + + + + + + {isLoading && ( +
+ {["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => ( + + + + + + + + ))}
)} - {sortedRules.length === 0 && !showForm && ( + {isError && ( + + + Failed to load rules + + {error instanceof Error ? error.message : "Unknown error."} + + + )} + + {!isLoading && !isError && sortedRules.length === 0 && !showForm && (

No rules yet

@@ -343,8 +381,8 @@ export function AgentPermissionsContent() {
)} - {sortedRules.length > 0 && ( -
+ {!isLoading && !isError && sortedRules.length > 0 && ( +
{sortedRules.map((rule) => { const badge = ACTION_BADGE[rule.action]; const isUpdating = @@ -352,14 +390,14 @@ export function AgentPermissionsContent() { const isDeleting = deleteMutation.isPending && deleteMutation.variables === rule.id; return ( -
-
+
- + {rule.permission} {rule.pattern !== "*" && ( @@ -374,7 +412,7 @@ export function AgentPermissionsContent() {

-
+
@@ -152,21 +171,23 @@ export function DesktopContent() { No search spaces found. Create one first.

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

Launch on Startup - - +

+

Automatically start SurfSense when you sign in to your computer so global shortcuts and folder sync are always available. - - - -

+

+
+
+
-
+
- - +
+
); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/HotkeysContent.tsx similarity index 80% rename from surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent.tsx rename to surfsense_web/app/dashboard/[search_space_id]/user-settings/components/HotkeysContent.tsx index f1679cb15..9916f6cda 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/HotkeysContent.tsx @@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { DEFAULT_SHORTCUTS, keyEventToAccelerator } from "@/components/desktop/shortcut-recorder"; import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; import { ShortcutKbd } from "@/components/ui/shortcut-kbd"; import { Spinner } from "@/components/ui/spinner"; import { useElectronAPI } from "@/hooks/use-platform"; @@ -78,7 +79,7 @@ function HotkeyRow({ ); return ( -
+
@@ -90,38 +91,39 @@ function HotkeyRow({ )} - +
); } -export function DesktopShortcutsContent() { +export function HotkeysContent() { const api = useElectronAPI(); const [shortcuts, setShortcuts] = useState(DEFAULT_SHORTCUTS); const [shortcutsLoaded, setShortcutsLoaded] = useState(false); @@ -178,17 +180,19 @@ export function DesktopShortcutsContent() { return shortcutsLoaded ? (
- {HOTKEY_ROWS.map((row) => ( - updateShortcut(row.key, accel)} - onReset={() => resetShortcut(row.key)} - /> + {HOTKEY_ROWS.map((row, index) => ( +
+ updateShortcut(row.key, accel)} + onReset={() => resetShortcut(row.key)} + /> + {index < HOTKEY_ROWS.length - 1 ? : null} +
))}
diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx index 3d0550b6c..3542f0925 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx @@ -177,9 +177,9 @@ export function MemoryContent() { return (
- - - + + +

SurfSense uses this personal memory to personalize your responses across all conversations. @@ -222,7 +222,9 @@ export function MemoryContent() { onClick={handleEdit} disabled={editing || !editQuery.trim()} className={`h-11 w-11 shrink-0 rounded-full ${ - editing ? "" : "bg-muted-foreground/15 hover:bg-muted-foreground/20" + editing + ? "" + : "bg-muted-foreground/15 hover:bg-accent hover:text-accent-foreground" }`} > {editing ? ( diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ProfileContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ProfileContent.tsx index b7a594f01..89bc362eb 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ProfileContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ProfileContent.tsx @@ -11,8 +11,17 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Spinner } from "@/components/ui/spinner"; +import { getUserAvatarColor, getUserInitials } from "@/lib/user-avatar"; -function AvatarDisplay({ url, fallback }: { url?: string; fallback: string }) { +function AvatarDisplay({ + url, + fallback, + bgColor, +}: { + url?: string; + fallback: string; + bgColor: string; +}) { const [errorUrl, setErrorUrl] = useState(); const hasError = errorUrl === url; @@ -23,15 +32,19 @@ function AvatarDisplay({ url, fallback }: { url?: string; fallback: string }) { alt="Avatar" width={64} height={64} - className="h-16 w-16 rounded-xl object-cover" + className="h-16 w-16 rounded-full object-cover select-none" onError={() => setErrorUrl(url)} + referrerPolicy="no-referrer" unoptimized /> ); } return ( -

+
{fallback}
); @@ -50,11 +63,6 @@ export function ProfileContent() { } }, [user]); - const getInitials = (email: string) => { - const name = email.split("@")[0]; - return name.slice(0, 2).toUpperCase(); - }; - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -69,6 +77,7 @@ export function ProfileContent() { }; const hasChanges = displayName !== (user?.display_name || ""); + const avatarBgColor = getUserAvatarColor(user?.email || ""); return (
@@ -78,13 +87,13 @@ export function ProfileContent() {
) : (
-
+
-
@@ -114,7 +123,7 @@ export function ProfileContent() { type="submit" variant="outline" disabled={isPending || !hasChanges} - className="relative gap-2 bg-white text-black hover:bg-neutral-100 dark:bg-white dark:text-black dark:hover:bg-neutral-200" + className="relative gap-2 bg-white text-black hover:bg-accent hover:text-accent-foreground dark:bg-white dark:text-black" > {t("profile_save")} {isPending && } diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PromptsContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PromptsContent.tsx index c78d4f9f0..e9415a1f2 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PromptsContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PromptsContent.tsx @@ -1,7 +1,7 @@ "use client"; import { useAtomValue } from "jotai"; -import { AlertTriangle, Globe, Lock, Pencil, Sparkles, Trash2 } from "lucide-react"; +import { AlertTriangle, Globe, Lock, MoreHorizontal, Pencil, Sparkles, Trash2 } from "lucide-react"; import { useCallback, useState } from "react"; import { toast } from "sonner"; import { @@ -10,6 +10,7 @@ import { updatePromptMutationAtom, } from "@/atoms/prompts/prompts-mutation.atoms"; import { promptsAtom } from "@/atoms/prompts/prompts-query.atoms"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { AlertDialog, AlertDialogAction, @@ -21,9 +22,32 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { ShortcutKbd } from "@/components/ui/shortcut-kbd"; +import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; import type { PromptRead } from "@/contracts/types/prompts.types"; @@ -123,24 +147,6 @@ export function PromptsContent() { const list = prompts ?? []; - if (isLoading) { - return ( -
- -
- ); - } - - if (isError) { - return ( -
- -

Failed to load prompts

-

Please try refreshing the page.

-
- ); - } - return (
@@ -148,97 +154,150 @@ export function PromptsContent() { Create prompt templates triggered with in the chat composer.

- {!showForm && ( - - )} +
- {showForm && ( -
-

- {editingId !== null ? "Edit prompt" : "New prompt"} -

+ { + setShowForm(open); + if (!open) { + setFormData(EMPTY_FORM); + setEditingId(null); + } + }} + > + + + {editingId !== null ? "Edit prompt" : "New prompt"} + + Create prompt templates triggered with / in the chat composer. + + -
- - setFormData((p) => ({ ...p, name: e.target.value }))} - placeholder="e.g. Fix grammar" - /> +
+
+ + setFormData((p) => ({ ...p, name: e.target.value }))} + placeholder="e.g. Fix grammar" + /> +
+ +
+ +