chore: linting

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-04-07 03:10:06 -07:00
parent 82b5c7f19e
commit 91ea293fa2
14 changed files with 285 additions and 264 deletions

View file

@ -16,7 +16,8 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
import uuid import uuid
from typing import Any, AsyncGenerator from collections.abc import AsyncGenerator
from typing import Any
from deepagents.graph import BASE_AGENT_PROMPT from deepagents.graph import BASE_AGENT_PROMPT
from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
@ -122,6 +123,7 @@ def _build_autocomplete_system_prompt(app_name: str, window_title: str) -> str:
class _KBResult: class _KBResult:
"""Container for pre-computed KB filesystem results.""" """Container for pre-computed KB filesystem results."""
__slots__ = ("files", "ls_ai_msg", "ls_tool_msg") __slots__ = ("files", "ls_ai_msg", "ls_tool_msg")
def __init__( def __init__(
@ -171,13 +173,16 @@ async def precompute_kb_filesystem(
return _KBResult() return _KBResult()
doc_paths = [ doc_paths = [
p for p, v in new_files.items() p
for p, v in new_files.items()
if p.startswith("/documents/") and v is not None if p.startswith("/documents/") and v is not None
] ]
tool_call_id = f"auto_ls_{uuid.uuid4().hex[:12]}" tool_call_id = f"auto_ls_{uuid.uuid4().hex[:12]}"
ai_msg = AIMessage( ai_msg = AIMessage(
content="", content="",
tool_calls=[{"name": "ls", "args": {"path": "/documents"}, "id": tool_call_id}], tool_calls=[
{"name": "ls", "args": {"path": "/documents"}, "id": tool_call_id}
],
) )
tool_msg = ToolMessage( tool_msg = ToolMessage(
content=str(doc_paths) if doc_paths else "No documents found.", content=str(doc_paths) if doc_paths else "No documents found.",
@ -186,7 +191,9 @@ async def precompute_kb_filesystem(
return _KBResult(files=new_files, ls_ai_msg=ai_msg, ls_tool_msg=tool_msg) return _KBResult(files=new_files, ls_ai_msg=ai_msg, ls_tool_msg=tool_msg)
except Exception: except Exception:
logger.warning("KB pre-computation failed, proceeding without KB", exc_info=True) logger.warning(
"KB pre-computation failed, proceeding without KB", exc_info=True
)
return _KBResult() return _KBResult()
@ -320,7 +327,9 @@ async def stream_autocomplete_agent(
) )
try: try:
async for event in agent.astream_events(input_data, config=config, version="v2"): async for event in agent.astream_events(
input_data, config=config, version="v2"
):
event_type = event.get("event", "") event_type = event.get("event", "")
if event_type == "on_chat_model_stream": if event_type == "on_chat_model_stream":
@ -338,7 +347,9 @@ async def stream_autocomplete_agent(
yield step_event yield step_event
current_text_id = streaming_service.generate_text_id() current_text_id = streaming_service.generate_text_id()
yield streaming_service.format_text_start(current_text_id) yield streaming_service.format_text_start(current_text_id)
yield streaming_service.format_text_delta(current_text_id, content) yield streaming_service.format_text_delta(
current_text_id, content
)
elif event_type == "on_tool_start": elif event_type == "on_tool_start":
active_tool_depth += 1 active_tool_depth += 1
@ -425,5 +436,7 @@ def _describe_tool_call(tool_name: str, tool_input: Any) -> tuple[str, list[str]
pat = inp.get("pattern", "") pat = inp.get("pattern", "")
path = inp.get("path", "") path = inp.get("path", "")
display_pat = pat[:60] + ("" if len(pat) > 60 else "") display_pat = pat[:60] + ("" if len(pat) > 60 else "")
return "Searching content", [f'"{display_pat}"' + (f" in {path}" if path else "")] return "Searching content", [
f'"{display_pat}"' + (f" in {path}" if path else "")
]
return f"Using {tool_name}", [] return f"Using {tool_name}", []

View file

@ -98,7 +98,9 @@ async def stream_vision_autocomplete(
step_id=PREP_STEP_ID, step_id=PREP_STEP_ID,
title="Searching knowledge base", title="Searching knowledge base",
status="complete", status="complete",
items=[f"Found {doc_count} document{'s' if doc_count != 1 else ''}"] if kb_query else ["Skipped"], items=[f"Found {doc_count} document{'s' if doc_count != 1 else ''}"]
if kb_query
else ["Skipped"],
) )
# Build agent input with pre-computed KB as initial state # Build agent input with pre-computed KB as initial state
@ -116,24 +118,33 @@ async def stream_vision_autocomplete(
"for the active text area based on what you see." "for the active text area based on what you see."
) )
user_message = HumanMessage(content=[ user_message = HumanMessage(
{"type": "text", "text": instruction}, content=[
{"type": "image_url", "image_url": {"url": screenshot_data_url}}, {"type": "text", "text": instruction},
]) {"type": "image_url", "image_url": {"url": screenshot_data_url}},
]
)
input_data: dict = {"messages": [user_message]} input_data: dict = {"messages": [user_message]}
if has_kb: if has_kb:
input_data["files"] = kb.files input_data["files"] = kb.files
input_data["messages"] = [kb.ls_ai_msg, kb.ls_tool_msg, user_message] input_data["messages"] = [kb.ls_ai_msg, kb.ls_tool_msg, user_message]
logger.info("Autocomplete: injected %d KB files into agent initial state", doc_count) logger.info(
"Autocomplete: injected %d KB files into agent initial state", doc_count
)
else: else:
logger.info("Autocomplete: no KB documents found, proceeding with screenshot only") logger.info(
"Autocomplete: no KB documents found, proceeding with screenshot only"
)
# Stream the agent (message_start already sent above) # Stream the agent (message_start already sent above)
try: try:
async for sse in stream_autocomplete_agent( async for sse in stream_autocomplete_agent(
agent, input_data, streaming, emit_message_start=False, agent,
input_data,
streaming,
emit_message_start=False,
): ):
yield sse yield sse
except Exception as e: except Exception as e:

View file

@ -3,10 +3,7 @@
import { Clipboard, Sparkles } from "lucide-react"; import { Clipboard, Sparkles } from "lucide-react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import { DEFAULT_SHORTCUTS, ShortcutRecorder } from "@/components/desktop/shortcut-recorder";
DEFAULT_SHORTCUTS,
ShortcutRecorder,
} from "@/components/desktop/shortcut-recorder";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
@ -29,22 +26,23 @@ export function DesktopContent() {
let mounted = true; let mounted = true;
Promise.all([ Promise.all([api.getAutocompleteEnabled(), api.getShortcuts?.() ?? Promise.resolve(null)])
api.getAutocompleteEnabled(), .then(([autoEnabled, config]) => {
api.getShortcuts?.() ?? Promise.resolve(null), if (!mounted) return;
]).then(([autoEnabled, config]) => { setEnabled(autoEnabled);
if (!mounted) return; if (config) setShortcuts(config);
setEnabled(autoEnabled); setLoading(false);
if (config) setShortcuts(config); setShortcutsLoaded(true);
setLoading(false); })
setShortcutsLoaded(true); .catch(() => {
}).catch(() => { if (!mounted) return;
if (!mounted) return; setLoading(false);
setLoading(false); setShortcutsLoaded(true);
setShortcutsLoaded(true); });
});
return () => { mounted = false; }; return () => {
mounted = false;
};
}, [api]); }, [api]);
if (!api) { if (!api) {

View file

@ -3,7 +3,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { USER_QUERY_KEY } from "@/atoms/user/user-query.atoms"; import { USER_QUERY_KEY } from "@/atoms/user/user-query.atoms";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
import { getBearerToken, ensureTokensFromElectron, redirectToLogin } from "@/lib/auth-utils"; import { ensureTokensFromElectron, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
import { queryClient } from "@/lib/query-client/client"; import { queryClient } from "@/lib/query-client/client";
interface DashboardLayoutProps { interface DashboardLayoutProps {

View file

@ -2,30 +2,15 @@
import { IconBrandGoogleFilled } from "@tabler/icons-react"; import { IconBrandGoogleFilled } from "@tabler/icons-react";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { import { Clipboard, Eye, EyeOff, Keyboard, Sparkles } from "lucide-react";
Eye,
EyeOff,
Keyboard,
Clipboard,
Sparkles,
} from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms"; import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms";
import { import { DEFAULT_SHORTCUTS, ShortcutRecorder } from "@/components/desktop/shortcut-recorder";
DEFAULT_SHORTCUTS,
ShortcutRecorder,
} from "@/components/desktop/shortcut-recorder";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
@ -38,8 +23,7 @@ const isGoogleAuth = AUTH_TYPE === "GOOGLE";
export default function DesktopLoginPage() { export default function DesktopLoginPage() {
const router = useRouter(); const router = useRouter();
const api = useElectronAPI(); const api = useElectronAPI();
const [{ mutateAsync: login, isPending: isLoggingIn }] = const [{ mutateAsync: login, isPending: isLoggingIn }] = useAtom(loginMutationAtom);
useAtom(loginMutationAtom);
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@ -54,10 +38,13 @@ export default function DesktopLoginPage() {
setShortcutsLoaded(true); setShortcutsLoaded(true);
return; return;
} }
api.getShortcuts().then((config) => { api
if (config) setShortcuts(config); .getShortcuts()
setShortcutsLoaded(true); .then((config) => {
}).catch(() => setShortcutsLoaded(true)); if (config) setShortcuts(config);
setShortcutsLoaded(true);
})
.catch(() => setShortcutsLoaded(true));
}, [api]); }, [api]);
const updateShortcut = useCallback( const updateShortcut = useCallback(
@ -118,8 +105,7 @@ export default function DesktopLoginPage() {
<div <div
className="absolute -top-1/2 left-1/2 size-[800px] -translate-x-1/2 rounded-full opacity-[0.03]" className="absolute -top-1/2 left-1/2 size-[800px] -translate-x-1/2 rounded-full opacity-[0.03]"
style={{ style={{
background: background: "radial-gradient(circle, hsl(var(--primary)) 0%, transparent 70%)",
"radial-gradient(circle, hsl(var(--primary)) 0%, transparent 70%)",
}} }}
/> />
</div> </div>
@ -135,9 +121,7 @@ export default function DesktopLoginPage() {
priority priority
/> />
<CardTitle className="text-xl">Welcome to SurfSense Desktop App</CardTitle> <CardTitle className="text-xl">Welcome to SurfSense Desktop App</CardTitle>
<CardDescription> <CardDescription>Configure your shortcuts, then sign in to get started.</CardDescription>
Configure your shortcuts, then sign in to get started.
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-6"> <CardContent className="flex flex-col gap-6">
@ -181,11 +165,7 @@ export default function DesktopLoginPage() {
{/* ---- Auth Section (second) ---- */} {/* ---- Auth Section (second) ---- */}
{isGoogleAuth ? ( {isGoogleAuth ? (
<Button <Button variant="outline" className="w-full gap-2 py-5" onClick={handleGoogleLogin}>
variant="outline"
className="w-full gap-2 py-5"
onClick={handleGoogleLogin}
>
<IconBrandGoogleFilled className="size-5" /> <IconBrandGoogleFilled className="size-5" />
Continue with Google Continue with Google
</Button> </Button>
@ -230,11 +210,7 @@ export default function DesktopLoginPage() {
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground" className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground"
tabIndex={-1} tabIndex={-1}
> >
{showPassword ? ( {showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
<EyeOff className="size-4" />
) : (
<Eye className="size-4" />
)}
</button> </button>
</div> </div>
</div> </div>

View file

@ -80,7 +80,9 @@ export default function DesktopPermissionsPage() {
poll(); poll();
interval = setInterval(poll, 2000); interval = setInterval(poll, 2000);
return () => { if (interval) clearInterval(interval); }; return () => {
if (interval) clearInterval(interval);
};
}, [api]); }, [api]);
if (!api) { if (!api) {
@ -204,6 +206,7 @@ export default function DesktopPermissionsPage() {
Grant permissions to continue Grant permissions to continue
</Button> </Button>
<button <button
type="button"
onClick={handleSkip} onClick={handleSkip}
className="block mx-auto text-xs text-muted-foreground hover:text-foreground transition-colors" className="block mx-auto text-xs text-muted-foreground hover:text-foreground transition-colors"
> >

View file

@ -2,7 +2,7 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useElectronAPI } from "@/hooks/use-platform"; import { useElectronAPI } from "@/hooks/use-platform";
import { getBearerToken, ensureTokensFromElectron } from "@/lib/auth-utils"; import { ensureTokensFromElectron, getBearerToken } from "@/lib/auth-utils";
type SSEEvent = type SSEEvent =
| { type: "text-delta"; id: string; delta: string } | { type: "text-delta"; id: string; delta: string }
@ -48,9 +48,20 @@ const AUTO_DISMISS_MS = 3000;
function StepIcon({ status }: { status: string }) { function StepIcon({ status }: { status: string }) {
if (status === "complete") { if (status === "complete") {
return ( return (
<svg className="step-icon step-icon-done" viewBox="0 0 16 16" fill="none"> <svg
className="step-icon step-icon-done"
viewBox="0 0 16 16"
fill="none"
aria-label="Step complete"
>
<circle cx="8" cy="8" r="7" stroke="#4ade80" strokeWidth="1.5" /> <circle cx="8" cy="8" r="7" stroke="#4ade80" strokeWidth="1.5" />
<path d="M5 8.5l2 2 4-4.5" stroke="#4ade80" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> <path
d="M5 8.5l2 2 4-4.5"
stroke="#4ade80"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
); );
} }

View file

@ -19,59 +19,59 @@ body:has(.suggestion-body) {
} }
.suggestion-tooltip { .suggestion-tooltip {
box-sizing: border-box; box-sizing: border-box;
background: #1e1e1e; background: #1e1e1e;
border: 1px solid #3c3c3c; border: 1px solid #3c3c3c;
border-radius: 8px; border-radius: 8px;
padding: 8px 12px; padding: 8px 12px;
margin: 4px; margin: 4px;
max-width: 400px; max-width: 400px;
/* MAX_HEIGHT in suggestion-window.ts is 400px. Subtract 8px for margin /* MAX_HEIGHT in suggestion-window.ts is 400px. Subtract 8px for margin
(4px * 2) so the tooltip + margin fits within the Electron window. (4px * 2) so the tooltip + margin fits within the Electron window.
box-sizing: border-box ensures padding + border are included. */ box-sizing: border-box ensures padding + border are included. */
max-height: 392px; max-height: 392px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.suggestion-text { .suggestion-text {
color: #d4d4d4; color: #d4d4d4;
font-size: 13px; font-size: 13px;
line-height: 1.45; line-height: 1.45;
margin: 0 0 6px 0; margin: 0 0 6px 0;
word-wrap: break-word; word-wrap: break-word;
white-space: pre-wrap; white-space: pre-wrap;
overflow-y: auto; overflow-y: auto;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
} }
.suggestion-text::-webkit-scrollbar { .suggestion-text::-webkit-scrollbar {
width: 5px; width: 5px;
} }
.suggestion-text::-webkit-scrollbar-track { .suggestion-text::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
.suggestion-text::-webkit-scrollbar-thumb { .suggestion-text::-webkit-scrollbar-thumb {
background: #555; background: #555;
border-radius: 3px; border-radius: 3px;
} }
.suggestion-text::-webkit-scrollbar-thumb:hover { .suggestion-text::-webkit-scrollbar-thumb:hover {
background: #777; background: #777;
} }
.suggestion-actions { .suggestion-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 4px; gap: 4px;
border-top: 1px solid #2a2a2a; border-top: 1px solid #2a2a2a;
padding-top: 6px; padding-top: 6px;
flex-shrink: 0; flex-shrink: 0;
} }
.suggestion-btn { .suggestion-btn {
@ -120,74 +120,74 @@ body:has(.suggestion-body) {
/* --- Agent activity indicator --- */ /* --- Agent activity indicator --- */
.agent-activity { .agent-activity {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
overflow-y: auto; overflow-y: auto;
max-height: 340px; max-height: 340px;
} }
.activity-initial { .activity-initial {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 2px 0; padding: 2px 0;
} }
.activity-label { .activity-label {
color: #a1a1aa; color: #a1a1aa;
font-size: 12px; font-size: 12px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.activity-steps { .activity-steps {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 3px; gap: 3px;
} }
.activity-step { .activity-step {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
min-height: 18px; min-height: 18px;
} }
.step-label { .step-label {
color: #d4d4d4; color: #d4d4d4;
font-size: 12px; font-size: 12px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.step-detail { .step-detail {
color: #71717a; color: #71717a;
font-size: 11px; font-size: 11px;
} }
/* Spinner (in_progress) */ /* Spinner (in_progress) */
.step-spinner { .step-spinner {
width: 14px; width: 14px;
height: 14px; height: 14px;
flex-shrink: 0; flex-shrink: 0;
border: 1.5px solid #3f3f46; border: 1.5px solid #3f3f46;
border-top-color: #a78bfa; border-top-color: #a78bfa;
border-radius: 50%; border-radius: 50%;
animation: step-spin 0.7s linear infinite; animation: step-spin 0.7s linear infinite;
} }
/* Checkmark icon (complete) */ /* Checkmark icon (complete) */
.step-icon { .step-icon {
width: 14px; width: 14px;
height: 14px; height: 14px;
flex-shrink: 0; flex-shrink: 0;
} }
@keyframes step-spin { @keyframes step-spin {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }

View file

@ -44,13 +44,7 @@ export const DEFAULT_SHORTCUTS = {
// Kbd pill component // Kbd pill component
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function Kbd({ export function Kbd({ keys, className }: { keys: string[]; className?: string }) {
keys,
className,
}: {
keys: string[];
className?: string;
}) {
return ( return (
<span className={cn("inline-flex items-center gap-1", className)}> <span className={cn("inline-flex items-center gap-1", className)}>
{keys.map((key) => ( {keys.map((key) => (
@ -123,9 +117,7 @@ export function ShortcutRecorder({
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-medium leading-none">{label}</p> <p className="text-sm font-medium leading-none">{label}</p>
<p className="mt-1 text-xs text-muted-foreground truncate"> <p className="mt-1 text-xs text-muted-foreground truncate">{description}</p>
{description}
</p>
</div> </div>
</div> </div>
@ -155,9 +147,7 @@ export function ShortcutRecorder({
)} )}
> >
{recording ? ( {recording ? (
<span className="text-xs text-primary animate-pulse"> <span className="text-xs text-primary animate-pulse">Press keys...</span>
Press keys...
</span>
) : ( ) : (
<Kbd keys={displayKeys} /> <Kbd keys={displayKeys} />
)} )}

View file

@ -1,21 +1,14 @@
"use client"; "use client";
import { AnimatePresence, motion } from "motion/react";
import { Monitor } from "lucide-react"; import { Monitor } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import Link from "next/link"; import Link from "next/link";
import React, { useCallback, useEffect, useRef, useState, memo } from "react"; import React, { memo, useCallback, useEffect, useRef, useState } from "react";
import Balancer from "react-wrap-balancer"; import Balancer from "react-wrap-balancer";
import { ExpandedMediaOverlay, useExpandedMedia } from "@/components/ui/expanded-gif-overlay";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { AUTH_TYPE, BACKEND_URL } from "@/lib/env-config"; import { AUTH_TYPE, BACKEND_URL } from "@/lib/env-config";
import { trackLoginAttempt } from "@/lib/posthog/events"; import { trackLoginAttempt } from "@/lib/posthog/events";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import {
ExpandedMediaOverlay,
useExpandedMedia,
} from "@/components/ui/expanded-gif-overlay";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
const GoogleLogo = ({ className }: { className?: string }) => ( const GoogleLogo = ({ className }: { className?: string }) => (
<svg <svg
@ -61,8 +54,7 @@ const TAB_ITEMS = [
}, },
{ {
title: "Search & Citation", title: "Search & Citation",
description: description: "Ask questions and get cited responses from your knowledge base.",
"Ask questions and get cited responses from your knowledge base.",
src: "/homepage/hero_tutorial/BSNCGif.mp4", src: "/homepage/hero_tutorial/BSNCGif.mp4",
featured: false, featured: false,
}, },
@ -86,15 +78,13 @@ const TAB_ITEMS = [
}, },
{ {
title: "Image Generation", title: "Image Generation",
description: description: "Generate high-quality images easily from your conversations.",
"Generate high-quality images easily from your conversations.",
src: "/homepage/hero_tutorial/ImageGenGif.mp4", src: "/homepage/hero_tutorial/ImageGenGif.mp4",
featured: false, featured: false,
}, },
{ {
title: "Collaborative Chat", title: "Collaborative Chat",
description: description: "Collaborate on AI-powered conversations in realtime with your team.",
"Collaborate on AI-powered conversations in realtime with your team.",
src: "/homepage/hero_realtime/RealTimeChatGif.mp4", src: "/homepage/hero_realtime/RealTimeChatGif.mp4",
featured: false, featured: false,
}, },
@ -106,8 +96,7 @@ const TAB_ITEMS = [
}, },
{ {
title: "Video Generation", title: "Video Generation",
description: description: "Create short videos with AI-generated visuals and narration from your sources.",
"Create short videos with AI-generated visuals and narration from your sources.",
src: "/homepage/hero_tutorial/video_gen_surf.mp4", src: "/homepage/hero_tutorial/video_gen_surf.mp4",
featured: false, featured: false,
}, },
@ -119,7 +108,7 @@ export function HeroSection() {
<div className="mt-4 flex w-full min-w-0 flex-col items-start px-2 md:px-8 xl:px-0"> <div className="mt-4 flex w-full min-w-0 flex-col items-start px-2 md:px-8 xl:px-0">
<h1 <h1
className={cn( className={cn(
"relative mt-4 max-w-7xl text-left text-4xl font-bold tracking-tight text-balance text-neutral-900 sm:text-5xl md:text-6xl xl:text-8xl dark:text-neutral-50", "relative mt-4 max-w-7xl text-left text-4xl font-bold tracking-tight text-balance text-neutral-900 sm:text-5xl md:text-6xl xl:text-8xl dark:text-neutral-50"
)} )}
> >
<Balancer>NotebookLM for Teams</Balancer> <Balancer>NotebookLM for Teams</Balancer>
@ -128,10 +117,11 @@ export function HeroSection() {
<div> <div>
<h2 <h2
className={cn( className={cn(
"relative mb-8 max-w-2xl text-left text-sm tracking-wide text-neutral-600 antialiased sm:text-base md:text-xl dark:text-neutral-400", "relative mb-8 max-w-2xl text-left text-sm tracking-wide text-neutral-600 antialiased sm:text-base md:text-xl dark:text-neutral-400"
)} )}
> >
An open source, privacy focused alternative to NotebookLM for teams with no data limits. An open source, privacy focused alternative to NotebookLM for teams with no data
limits.
</h2> </h2>
<div className="relative mb-4 flex w-full flex-col justify-center gap-y-2 sm:flex-row sm:justify-start sm:space-y-0 sm:space-x-4"> <div className="relative mb-4 flex w-full flex-col justify-center gap-y-2 sm:flex-row sm:justify-start sm:space-y-0 sm:space-x-4">
@ -194,33 +184,34 @@ const BrowserWindow = () => {
<div className="no-visible-scrollbar flex min-w-0 shrink flex-row items-center justify-start gap-2 overflow-x-auto mask-l-from-98% py-0.5 pr-2 pl-2 md:pl-4"> <div className="no-visible-scrollbar flex min-w-0 shrink flex-row items-center justify-start gap-2 overflow-x-auto mask-l-from-98% py-0.5 pr-2 pl-2 md:pl-4">
{TAB_ITEMS.map((item, index) => ( {TAB_ITEMS.map((item, index) => (
<React.Fragment key={item.title}> <React.Fragment key={item.title}>
<button <button
type="button" type="button"
onClick={() => setSelectedIndex(index)} onClick={() => setSelectedIndex(index)}
className={cn( className={cn(
"flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs transition duration-150 hover:bg-white sm:text-sm dark:hover:bg-neutral-950", "flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs transition duration-150 hover:bg-white sm:text-sm dark:hover:bg-neutral-950",
selectedIndex === index && !item.featured && selectedIndex === index &&
"bg-white shadow ring-1 shadow-black/10 ring-black/10 dark:bg-neutral-900", !item.featured &&
selectedIndex === index && item.featured && "bg-white shadow ring-1 shadow-black/10 ring-black/10 dark:bg-neutral-900",
"bg-amber-50 shadow ring-1 shadow-amber-200/50 ring-amber-400/60 dark:bg-amber-950/40 dark:shadow-amber-900/30 dark:ring-amber-500/50", selectedIndex === index &&
item.featured && selectedIndex !== index && item.featured &&
"hover:bg-amber-50 dark:hover:bg-amber-950/30", "bg-amber-50 shadow ring-1 shadow-amber-200/50 ring-amber-400/60 dark:bg-amber-950/40 dark:shadow-amber-900/30 dark:ring-amber-500/50",
)} item.featured &&
> selectedIndex !== index &&
"hover:bg-amber-50 dark:hover:bg-amber-950/30"
)}
>
{item.title} {item.title}
{item.featured && ( {item.featured && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span className="inline-flex shrink-0 items-center justify-center rounded border border-amber-300 bg-amber-100 p-0.5 text-amber-700 dark:border-amber-700 dark:bg-amber-900/50 dark:text-amber-400"> <span className="inline-flex shrink-0 items-center justify-center rounded border border-amber-300 bg-amber-100 p-0.5 text-amber-700 dark:border-amber-700 dark:bg-amber-900/50 dark:text-amber-400">
<Monitor className="size-3" /> <Monitor className="size-3" />
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom"> <TooltipContent side="bottom">Desktop app only</TooltipContent>
Desktop app only </Tooltip>
</TooltipContent> )}
</Tooltip> </button>
)}
</button>
{index !== TAB_ITEMS.length - 1 && ( {index !== TAB_ITEMS.length - 1 && (
<div className="h-4 w-px shrink-0 rounded-full bg-neutral-300 dark:bg-neutral-700" /> <div className="h-4 w-px shrink-0 rounded-full bg-neutral-300 dark:bg-neutral-700" />
)} )}
@ -263,13 +254,13 @@ const BrowserWindow = () => {
</p> </p>
</div> </div>
</div> </div>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: wrapper for video expand */} <button
<div type="button"
className="cursor-pointer bg-neutral-50 p-2 sm:p-3 dark:bg-neutral-950" className="cursor-pointer bg-neutral-50 p-2 sm:p-3 dark:bg-neutral-950 w-full"
onClick={open} onClick={open}
> >
<TabVideo src={selectedItem.src} /> <TabVideo src={selectedItem.src} />
</div> </button>
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
</div> </div>
@ -277,11 +268,7 @@ const BrowserWindow = () => {
<AnimatePresence> <AnimatePresence>
{expanded && ( {expanded && (
<ExpandedMediaOverlay <ExpandedMediaOverlay src={selectedItem.src} alt={selectedItem.title} onClose={close} />
src={selectedItem.src}
alt={selectedItem.title}
onClose={close}
/>
)} )}
</AnimatePresence> </AnimatePresence>
</> </>
@ -297,7 +284,7 @@ const TabVideo = memo(function TabVideo({ src }: { src: string }) {
const video = videoRef.current; const video = videoRef.current;
if (!video) return; if (!video) return;
video.currentTime = 0; video.currentTime = 0;
video.play().catch(() => { }); video.play().catch(() => {});
}, [src]); }, [src]);
const handleCanPlay = useCallback(() => { const handleCanPlay = useCallback(() => {
@ -324,8 +311,7 @@ const TabVideo = memo(function TabVideo({ src }: { src: string }) {
); );
}); });
const GITHUB_RELEASES_URL = const GITHUB_RELEASES_URL = "https://github.com/MODSetter/SurfSense/releases/latest";
"https://github.com/MODSetter/SurfSense/releases/latest";
const DownloadApp = memo(function DownloadApp() { const DownloadApp = memo(function DownloadApp() {
return ( return (
@ -340,7 +326,16 @@ const DownloadApp = memo(function DownloadApp() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-2 rounded-lg border border-neutral-200 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 shadow-sm transition hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800" className="flex items-center gap-2 rounded-lg border border-neutral-200 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 shadow-sm transition hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
> >
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg
className="size-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Download for macOS"
>
<path d="M12 17V3" /> <path d="M12 17V3" />
<path d="m6 11 6 6 6-6" /> <path d="m6 11 6 6 6-6" />
<path d="M19 21H5" /> <path d="M19 21H5" />
@ -353,7 +348,16 @@ const DownloadApp = memo(function DownloadApp() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-2 rounded-lg border border-neutral-200 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 shadow-sm transition hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800" className="flex items-center gap-2 rounded-lg border border-neutral-200 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 shadow-sm transition hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
> >
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg
className="size-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Download for Windows"
>
<path d="M12 17V3" /> <path d="M12 17V3" />
<path d="m6 11 6 6 6-6" /> <path d="m6 11 6 6 6-6" />
<path d="M19 21H5" /> <path d="M19 21H5" />
@ -366,7 +370,16 @@ const DownloadApp = memo(function DownloadApp() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-2 rounded-lg border border-neutral-200 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 shadow-sm transition hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800" className="flex items-center gap-2 rounded-lg border border-neutral-200 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 shadow-sm transition hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
> >
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg
className="size-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Download for Linux"
>
<path d="M12 17V3" /> <path d="M12 17V3" />
<path d="m6 11 6 6 6-6" /> <path d="m6 11 6 6 6-6" />
<path d="M19 21H5" /> <path d="M19 21H5" />

View file

@ -302,24 +302,27 @@ export function DocumentsSidebar({
[searchSpaceId, electronAPI] [searchSpaceId, electronAPI]
); );
const handleStopWatching = useCallback(async (folder: FolderDisplay) => { const handleStopWatching = useCallback(
if (!electronAPI) return; async (folder: FolderDisplay) => {
if (!electronAPI) return;
const watchedFolders = await electronAPI.getWatchedFolders(); const watchedFolders = await electronAPI.getWatchedFolders();
const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id); const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id);
if (!matched) { if (!matched) {
toast.error("This folder is not being watched"); toast.error("This folder is not being watched");
return; return;
} }
await electronAPI.removeWatchedFolder(matched.path); await electronAPI.removeWatchedFolder(matched.path);
try { try {
await foldersApiService.stopWatching(folder.id); await foldersApiService.stopWatching(folder.id);
} catch (err) { } catch (err) {
console.error("[DocumentsSidebar] Failed to clear watched metadata:", err); console.error("[DocumentsSidebar] Failed to clear watched metadata:", err);
} }
toast.success(`Stopped watching: ${matched.name}`); toast.success(`Stopped watching: ${matched.name}`);
}, [electronAPI]); },
[electronAPI]
);
const handleRenameFolder = useCallback(async (folder: FolderDisplay, newName: string) => { const handleRenameFolder = useCallback(async (folder: FolderDisplay, newName: string) => {
try { try {
@ -330,22 +333,25 @@ export function DocumentsSidebar({
} }
}, []); }, []);
const handleDeleteFolder = useCallback(async (folder: FolderDisplay) => { const handleDeleteFolder = useCallback(
if (!confirm(`Delete folder "${folder.name}" and all its contents?`)) return; async (folder: FolderDisplay) => {
try { if (!confirm(`Delete folder "${folder.name}" and all its contents?`)) return;
if (electronAPI) { try {
const watchedFolders = await electronAPI.getWatchedFolders(); if (electronAPI) {
const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id); const watchedFolders = await electronAPI.getWatchedFolders();
if (matched) { const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id);
await electronAPI.removeWatchedFolder(matched.path); if (matched) {
await electronAPI.removeWatchedFolder(matched.path);
}
} }
await foldersApiService.deleteFolder(folder.id);
toast.success("Folder deleted");
} catch (e: unknown) {
toast.error((e as Error)?.message || "Failed to delete folder");
} }
await foldersApiService.deleteFolder(folder.id); },
toast.success("Folder deleted"); [electronAPI]
} catch (e: unknown) { );
toast.error((e as Error)?.message || "Failed to delete folder");
}
}, [electronAPI]);
const handleMoveFolder = useCallback( const handleMoveFolder = useCallback(
(folder: FolderDisplay) => { (folder: FolderDisplay) => {

View file

@ -25,8 +25,8 @@ import {
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { documentsApiService } from "@/lib/apis/documents-api.service";
import { useElectronAPI } from "@/hooks/use-platform"; import { useElectronAPI } from "@/hooks/use-platform";
import { documentsApiService } from "@/lib/apis/documents-api.service";
import { import {
trackDocumentUploadFailure, trackDocumentUploadFailure,
trackDocumentUploadStarted, trackDocumentUploadStarted,

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { createContext, useEffect, useState, type ReactNode } from "react"; import { createContext, type ReactNode, useEffect, useState } from "react";
export interface PlatformContextValue { export interface PlatformContextValue {
isDesktop: boolean; isDesktop: boolean;
@ -25,7 +25,5 @@ export function PlatformProvider({ children }: { children: ReactNode }) {
setValue({ isDesktop, isWeb: !isDesktop, electronAPI: api }); setValue({ isDesktop, isWeb: !isDesktop, electronAPI: api });
}, []); }, []);
return ( return <PlatformContext.Provider value={value}>{children}</PlatformContext.Provider>;
<PlatformContext.Provider value={value}>{children}</PlatformContext.Provider>
);
} }

View file

@ -90,7 +90,9 @@ interface ElectronAPI {
setAuthTokens: (bearer: string, refresh: string) => Promise<void>; setAuthTokens: (bearer: string, refresh: string) => Promise<void>;
// Keyboard shortcut configuration // Keyboard shortcut configuration
getShortcuts: () => Promise<{ quickAsk: string; autocomplete: string }>; getShortcuts: () => Promise<{ quickAsk: string; autocomplete: string }>;
setShortcuts: (config: Partial<{ quickAsk: string; autocomplete: string }>) => Promise<{ quickAsk: string; autocomplete: string }>; setShortcuts: (
config: Partial<{ quickAsk: string; autocomplete: string }>
) => Promise<{ quickAsk: string; autocomplete: string }>;
} }
declare global { declare global {