chore: ran linting

This commit is contained in:
Anish Sarkar 2026-03-10 16:17:12 +05:30
parent ad7bbcbc8f
commit 6a88f9e0eb
18 changed files with 478 additions and 472 deletions

View file

@ -9,7 +9,14 @@ import { useEffect } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -108,37 +115,26 @@ export default function MorePagesPage() {
<div <div
className={cn( className={cn(
"flex h-9 w-9 shrink-0 items-center justify-center rounded-full", "flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
task.completed task.completed ? "bg-primary text-primary-foreground" : "bg-muted"
? "bg-primary text-primary-foreground"
: "bg-muted"
)} )}
> >
{task.completed ? ( {task.completed ? <Check className="h-4 w-4" /> : <Star className="h-4 w-4" />}
<Check className="h-4 w-4" />
) : (
<Star className="h-4 w-4" />
)}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p <p
className={cn( className={cn(
"text-sm font-medium", "text-sm font-medium",
task.completed && task.completed && "text-muted-foreground line-through"
"text-muted-foreground line-through"
)} )}
> >
{task.title} {task.title}
</p> </p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">+{task.pages_reward} pages</p>
+{task.pages_reward} pages
</p>
</div> </div>
<Button <Button
variant={task.completed ? "ghost" : "outline"} variant={task.completed ? "ghost" : "outline"}
size="sm" size="sm"
disabled={ disabled={task.completed || completeMutation.isPending}
task.completed || completeMutation.isPending
}
onClick={() => handleTaskClick(task)} onClick={() => handleTaskClick(task)}
asChild={!task.completed} asChild={!task.completed}
> >
@ -181,8 +177,9 @@ export default function MorePagesPage() {
</Badge> </Badge>
</div> </div>
<CardDescription> <CardDescription>
For a limited time, get <span className="font-semibold text-foreground">6,000 additional pages</span> at For a limited time, get{" "}
no cost. Contact us and we&apos;ll upgrade your account instantly. <span className="font-semibold text-foreground">6,000 additional pages</span> at no
cost. Contact us and we&apos;ll upgrade your account instantly.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardFooter className="pt-2"> <CardFooter className="pt-2">
@ -196,9 +193,7 @@ export default function MorePagesPage() {
<DialogContent className="select-none sm:max-w-sm"> <DialogContent className="select-none sm:max-w-sm">
<DialogHeader> <DialogHeader>
<DialogTitle>Get in Touch</DialogTitle> <DialogTitle>Get in Touch</DialogTitle>
<DialogDescription> <DialogDescription>Pick the option that works best for you.</DialogDescription>
Pick the option that works best for you.
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Button asChild> <Button asChild>

View file

@ -1,16 +1,10 @@
import Image from "next/image";
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import Image from "next/image";
export const baseOptions: BaseLayoutProps = { export const baseOptions: BaseLayoutProps = {
nav: { nav: {
title: ( title: (
<> <>
<Image <Image src="/icon-128.svg" alt="SurfSense" width={24} height={24} className="dark:invert" />
src="/icon-128.svg"
alt="SurfSense"
width={24}
height={24}
className="dark:invert"
/>
SurfSense Docs SurfSense Docs
</> </>
), ),

View file

@ -10,10 +10,7 @@ import { useAtomValue } from "jotai";
import { CheckIcon, CopyIcon, DownloadIcon, MessageSquare, RefreshCwIcon } from "lucide-react"; import { CheckIcon, CopyIcon, DownloadIcon, MessageSquare, RefreshCwIcon } from "lucide-react";
import type { FC } from "react"; import type { FC } from "react";
import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { import { commentsEnabledAtom, targetCommentIdAtom } from "@/atoms/chat/current-thread.atom";
commentsEnabledAtom,
targetCommentIdAtom,
} from "@/atoms/chat/current-thread.atom";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { MarkdownText } from "@/components/assistant-ui/markdown-text"; import { MarkdownText } from "@/components/assistant-ui/markdown-text";
import { import {
@ -139,7 +136,8 @@ export const AssistantMessage: FC = () => {
commentPanelRef.current?.contains(target) || commentPanelRef.current?.contains(target) ||
commentTriggerRef.current?.contains(target) || commentTriggerRef.current?.contains(target) ||
target.closest?.("[data-radix-popper-content-wrapper]") target.closest?.("[data-radix-popper-content-wrapper]")
) return; )
return;
setIsInlineOpen(false); setIsInlineOpen(false);
}; };
document.addEventListener("mousedown", handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
@ -178,7 +176,9 @@ export const AssistantMessage: FC = () => {
<button <button
ref={isDesktop ? commentTriggerRef : undefined} ref={isDesktop ? commentTriggerRef : undefined}
type="button" type="button"
onClick={isDesktop ? () => setIsInlineOpen((prev) => !prev) : () => setIsSheetOpen(true)} onClick={
isDesktop ? () => setIsInlineOpen((prev) => !prev) : () => setIsSheetOpen(true)
}
className={cn( className={cn(
"flex items-center gap-1.5 rounded-full px-3 py-1 text-sm transition-colors", "flex items-center gap-1.5 rounded-full px-3 py-1 text-sm transition-colors",
isDesktop && isInlineOpen isDesktop && isInlineOpen
@ -206,11 +206,7 @@ export const AssistantMessage: FC = () => {
ref={commentPanelRef} ref={commentPanelRef}
className="absolute right-0 top-10 z-30 w-full max-w-md animate-in fade-in slide-in-from-top-2 duration-200" className="absolute right-0 top-10 z-30 w-full max-w-md animate-in fade-in slide-in-from-top-2 duration-200"
> >
<CommentPanelContainer <CommentPanelContainer messageId={dbMessageId} isOpen={true} variant="inline" />
messageId={dbMessageId}
isOpen={true}
variant="inline"
/>
</div> </div>
)} )}

View file

@ -6,6 +6,7 @@ import Link from "next/link";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { type FC, forwardRef, useImperativeHandle, useMemo } from "react"; import { type FC, forwardRef, useImperativeHandle, useMemo } from "react";
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
import { import {
globalNewLLMConfigsAtom, globalNewLLMConfigsAtom,
llmPreferencesAtom, llmPreferencesAtom,
@ -19,7 +20,6 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsContent } from "@/components/ui/tabs"; import { Tabs, TabsContent } from "@/components/ui/tabs";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
import { useConnectorsElectric } from "@/hooks/use-connectors-electric"; import { useConnectorsElectric } from "@/hooks/use-connectors-electric";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header"; import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header";
@ -47,400 +47,407 @@ interface ConnectorIndicatorProps {
export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>( export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>(
({ showTrigger = true }, ref) => { ({ showTrigger = true }, ref) => {
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { data: currentUser } = useAtomValue(currentUserAtom); const { data: currentUser } = useAtomValue(currentUserAtom);
const { data: preferences = {}, isFetching: preferencesLoading } = const { data: preferences = {}, isFetching: preferencesLoading } =
useAtomValue(llmPreferencesAtom); useAtomValue(llmPreferencesAtom);
const { data: globalConfigs = [], isFetching: globalConfigsLoading } = const { data: globalConfigs = [], isFetching: globalConfigsLoading } =
useAtomValue(globalNewLLMConfigsAtom); useAtomValue(globalNewLLMConfigsAtom);
// Check if document summary LLM is properly configured // Check if document summary LLM is properly configured
// - If ID is 0 (Auto mode), we need global configs to be available // - If ID is 0 (Auto mode), we need global configs to be available
// - If ID is positive (user config) or negative (specific global config), it's configured // - If ID is positive (user config) or negative (specific global config), it's configured
// - If ID is null/undefined, it's not configured // - If ID is null/undefined, it's not configured
const docSummaryLlmId = preferences.document_summary_llm_id; const docSummaryLlmId = preferences.document_summary_llm_id;
const isAutoMode = docSummaryLlmId === 0; const isAutoMode = docSummaryLlmId === 0;
const hasGlobalConfigs = globalConfigs.length > 0; const hasGlobalConfigs = globalConfigs.length > 0;
const hasDocumentSummaryLLM = const hasDocumentSummaryLLM =
docSummaryLlmId !== null && docSummaryLlmId !== null &&
docSummaryLlmId !== undefined && docSummaryLlmId !== undefined &&
// If it's Auto mode, we need global configs to actually be available // If it's Auto mode, we need global configs to actually be available
(!isAutoMode || hasGlobalConfigs); (!isAutoMode || hasGlobalConfigs);
const llmConfigLoading = preferencesLoading || globalConfigsLoading; const llmConfigLoading = preferencesLoading || globalConfigsLoading;
// Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min) // Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min)
const { data: documentTypeCounts, isFetching: documentTypesLoading } = const { data: documentTypeCounts, isFetching: documentTypesLoading } =
useAtomValue(documentTypeCountsAtom); useAtomValue(documentTypeCountsAtom);
// Read status inbox items from shared atom (populated by LayoutDataProvider) // Read status inbox items from shared atom (populated by LayoutDataProvider)
// instead of creating a duplicate useInbox("status") hook. // instead of creating a duplicate useInbox("status") hook.
const statusInboxItems = useAtomValue(statusInboxItemsAtom); const statusInboxItems = useAtomValue(statusInboxItemsAtom);
const inboxItems = useMemo( const inboxItems = useMemo(
() => statusInboxItems.filter((item) => item.type === "connector_indexing"), () => statusInboxItems.filter((item) => item.type === "connector_indexing"),
[statusInboxItems] [statusInboxItems]
); );
// Check if YouTube view is active // Check if YouTube view is active
const isYouTubeView = searchParams.get("view") === "youtube"; const isYouTubeView = searchParams.get("view") === "youtube";
// Use the custom hook for dialog state management // Use the custom hook for dialog state management
const { const {
isOpen, isOpen,
activeTab, activeTab,
connectingId, connectingId,
isScrolled, isScrolled,
searchQuery, searchQuery,
indexingConfig, indexingConfig,
indexingConnector, indexingConnector,
indexingConnectorConfig, indexingConnectorConfig,
editingConnector, editingConnector,
connectingConnectorType, connectingConnectorType,
isCreatingConnector, isCreatingConnector,
startDate, startDate,
endDate, endDate,
isStartingIndexing, isStartingIndexing,
isSaving, isSaving,
isDisconnecting, isDisconnecting,
periodicEnabled, periodicEnabled,
frequencyMinutes, frequencyMinutes,
enableSummary, enableSummary,
allConnectors, allConnectors,
viewingAccountsType, viewingAccountsType,
viewingMCPList, viewingMCPList,
setSearchQuery, setSearchQuery,
setStartDate, setStartDate,
setEndDate, setEndDate,
setPeriodicEnabled, setPeriodicEnabled,
setFrequencyMinutes, setFrequencyMinutes,
setEnableSummary, setEnableSummary,
handleOpenChange, handleOpenChange,
handleTabChange, handleTabChange,
handleScroll, handleScroll,
handleConnectOAuth, handleConnectOAuth,
handleConnectNonOAuth, handleConnectNonOAuth,
handleCreateWebcrawler, handleCreateWebcrawler,
handleCreateYouTubeCrawler, handleCreateYouTubeCrawler,
handleSubmitConnectForm, handleSubmitConnectForm,
handleStartIndexing, handleStartIndexing,
handleSkipIndexing, handleSkipIndexing,
handleStartEdit, handleStartEdit,
handleSaveConnector, handleSaveConnector,
handleDisconnectConnector, handleDisconnectConnector,
handleBackFromEdit, handleBackFromEdit,
handleBackFromConnect, handleBackFromConnect,
handleBackFromYouTube, handleBackFromYouTube,
handleViewAccountsList, handleViewAccountsList,
handleBackFromAccountsList, handleBackFromAccountsList,
handleBackFromMCPList, handleBackFromMCPList,
handleAddNewMCPFromList, handleAddNewMCPFromList,
handleQuickIndexConnector, handleQuickIndexConnector,
connectorConfig, connectorConfig,
setConnectorConfig, setConnectorConfig,
setIndexingConnectorConfig, setIndexingConnectorConfig,
setConnectorName, setConnectorName,
} = useConnectorDialog(); } = useConnectorDialog();
// Fetch connectors using Electric SQL + PGlite for real-time updates // Fetch connectors using Electric SQL + PGlite for real-time updates
// This provides instant updates when connectors change, without polling // This provides instant updates when connectors change, without polling
const { const {
connectors: connectorsFromElectric = [], connectors: connectorsFromElectric = [],
loading: connectorsLoading, loading: connectorsLoading,
error: connectorsError, error: connectorsError,
refreshConnectors: refreshConnectorsElectric, refreshConnectors: refreshConnectorsElectric,
} = useConnectorsElectric(searchSpaceId); } = useConnectorsElectric(searchSpaceId);
// Fallback to API if Electric is not available or fails // Fallback to API if Electric is not available or fails
// Use Electric data if: 1) we have data, or 2) still loading without error // Use Electric data if: 1) we have data, or 2) still loading without error
// Use API data if: Electric failed (has error) or finished loading with no data // Use API data if: Electric failed (has error) or finished loading with no data
const useElectricData = const useElectricData =
connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError); connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError);
const connectors = useElectricData ? connectorsFromElectric : allConnectors || []; const connectors = useElectricData ? connectorsFromElectric : allConnectors || [];
// Manual refresh function that works with both Electric and API // Manual refresh function that works with both Electric and API
const refreshConnectors = async () => { const refreshConnectors = async () => {
if (useElectricData) { if (useElectricData) {
await refreshConnectorsElectric(); await refreshConnectorsElectric();
} else { } else {
// Fallback: use allConnectors from useConnectorDialog (which uses connectorsAtom) // Fallback: use allConnectors from useConnectorDialog (which uses connectorsAtom)
// The connectorsAtom will handle refetching if needed // The connectorsAtom will handle refetching if needed
} }
}; };
// Track indexing state locally - clears automatically when Electric SQL detects last_indexed_at changed // Track indexing state locally - clears automatically when Electric SQL detects last_indexed_at changed
// Also clears when failed notifications are detected // Also clears when failed notifications are detected
const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors( const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors(
connectors as SearchSourceConnector[], connectors as SearchSourceConnector[],
inboxItems inboxItems
); );
const isLoading = connectorsLoading || documentTypesLoading; const isLoading = connectorsLoading || documentTypesLoading;
// Get document types that have documents in the search space // Get document types that have documents in the search space
const activeDocumentTypes = documentTypeCounts const activeDocumentTypes = documentTypeCounts
? Object.entries(documentTypeCounts).filter(([, count]) => count > 0) ? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
: []; : [];
const hasConnectors = connectors.length > 0; const hasConnectors = connectors.length > 0;
const hasSources = hasConnectors || activeDocumentTypes.length > 0; const hasSources = hasConnectors || activeDocumentTypes.length > 0;
const totalSourceCount = connectors.length + activeDocumentTypes.length; const totalSourceCount = connectors.length + activeDocumentTypes.length;
const activeConnectorsCount = connectors.length; const activeConnectorsCount = connectors.length;
// Check which connectors are already connected // Check which connectors are already connected
// Using Electric SQL + PGlite for real-time connector updates // Using Electric SQL + PGlite for real-time connector updates
const connectedTypes = new Set<string>( const connectedTypes = new Set<string>(
(connectors || []).map((c: SearchSourceConnector) => c.connector_type) (connectors || []).map((c: SearchSourceConnector) => c.connector_type)
); );
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: () => handleOpenChange(true), open: () => handleOpenChange(true),
})); }));
if (!searchSpaceId) return null; if (!searchSpaceId) return null;
return ( return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}> <Dialog open={isOpen} onOpenChange={handleOpenChange}>
{showTrigger && ( {showTrigger && (
<TooltipIconButton <TooltipIconButton
data-joyride="connector-icon" data-joyride="connector-icon"
tooltip={hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"} tooltip={
side="bottom" hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"
className={cn(
"size-[34px] rounded-full p-1 flex items-center justify-center transition-colors relative",
"hover:bg-muted-foreground/15 dark:hover:bg-muted-foreground/30",
"outline-none focus:outline-none focus-visible:outline-none font-semibold text-xs",
"border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none"
)}
aria-label={
hasConnectors ? `View ${activeConnectorsCount} connectors` : "Add your first connector"
}
onClick={() => handleOpenChange(true)}
>
{isLoading ? (
<Spinner size="sm" />
) : (
<>
<Cable className="size-4 stroke-[1.5px]" />
{activeConnectorsCount > 0 && (
<span className="absolute -top-0.5 right-0 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm select-none">
{activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
</span>
)}
</>
)}
</TooltipIconButton>
)}
<DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 dark:ring-0 bg-muted dark:bg-muted text-foreground focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5 select-none">
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
{/* YouTube Crawler View - shown when adding YouTube videos */}
{isYouTubeView && searchSpaceId ? (
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} />
) : viewingMCPList ? (
<ConnectorAccountsListView
connectorType="MCP_CONNECTOR"
connectorTitle="MCP Connectors"
connectors={(allConnectors || []) as SearchSourceConnector[]}
indexingConnectorIds={indexingConnectorIds}
onBack={handleBackFromMCPList}
onManage={handleStartEdit}
onAddAccount={handleAddNewMCPFromList}
addButtonText="Add New MCP Server"
/>
) : viewingAccountsType ? (
<ConnectorAccountsListView
connectorType={viewingAccountsType.connectorType}
connectorTitle={viewingAccountsType.connectorTitle}
connectors={(connectors || []) as SearchSourceConnector[]} // Using Electric SQL + PGlite for real-time connector updates (all connector types)
indexingConnectorIds={indexingConnectorIds}
onBack={handleBackFromAccountsList}
onManage={handleStartEdit}
onAddAccount={() => {
// Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS
const oauthConnector =
OAUTH_CONNECTORS.find(
(c) => c.connectorType === viewingAccountsType.connectorType
) ||
COMPOSIO_CONNECTORS.find(
(c) => c.connectorType === viewingAccountsType.connectorType
);
if (oauthConnector) {
handleConnectOAuth(oauthConnector);
}
}}
isConnecting={connectingId !== null}
/>
) : connectingConnectorType ? (
<ConnectorConnectView
connectorType={connectingConnectorType}
onSubmit={(formData) => handleSubmitConnectForm(formData, startIndexing)}
onBack={handleBackFromConnect}
isSubmitting={isCreatingConnector}
/>
) : editingConnector ? (
<ConnectorEditView
connector={{
...editingConnector,
config: connectorConfig || editingConnector.config,
name: editingConnector.name,
// Sync last_indexed_at with live data from Electric SQL for real-time updates
last_indexed_at:
(connectors as SearchSourceConnector[]).find((c) => c.id === editingConnector.id)
?.last_indexed_at ?? editingConnector.last_indexed_at,
}}
startDate={startDate}
endDate={endDate}
periodicEnabled={periodicEnabled}
frequencyMinutes={frequencyMinutes}
enableSummary={enableSummary}
isSaving={isSaving}
isDisconnecting={isDisconnecting}
isIndexing={indexingConnectorIds.has(editingConnector.id)}
searchSpaceId={searchSpaceId?.toString()}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onPeriodicEnabledChange={setPeriodicEnabled}
onFrequencyChange={setFrequencyMinutes}
onEnableSummaryChange={setEnableSummary}
onSave={() => {
startIndexing(editingConnector.id);
handleSaveConnector(() => refreshConnectors());
}}
onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())}
onBack={handleBackFromEdit}
onQuickIndex={
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR"
? () => {
startIndexing(editingConnector.id);
handleQuickIndexConnector(
editingConnector.id,
editingConnector.connector_type,
stopIndexing,
startDate,
endDate
);
}
: undefined
} }
onConfigChange={setConnectorConfig} side="bottom"
onNameChange={setConnectorName} className={cn(
/> "size-[34px] rounded-full p-1 flex items-center justify-center transition-colors relative",
) : indexingConfig ? ( "hover:bg-muted-foreground/15 dark:hover:bg-muted-foreground/30",
<IndexingConfigurationView "outline-none focus:outline-none focus-visible:outline-none font-semibold text-xs",
config={indexingConfig} "border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none"
connector={ )}
indexingConnector aria-label={
? { hasConnectors
...indexingConnector, ? `View ${activeConnectorsCount} connectors`
config: indexingConnectorConfig || indexingConnector.config, : "Add your first connector"
}
: undefined
} }
startDate={startDate} onClick={() => handleOpenChange(true)}
endDate={endDate}
periodicEnabled={periodicEnabled}
frequencyMinutes={frequencyMinutes}
enableSummary={enableSummary}
isStartingIndexing={isStartingIndexing}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onPeriodicEnabledChange={setPeriodicEnabled}
onFrequencyChange={setFrequencyMinutes}
onEnableSummaryChange={setEnableSummary}
onConfigChange={setIndexingConnectorConfig}
onStartIndexing={() => {
if (indexingConfig.connectorId) {
startIndexing(indexingConfig.connectorId);
}
handleStartIndexing(() => refreshConnectors());
}}
onSkip={handleSkipIndexing}
/>
) : (
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className="flex-1 flex flex-col min-h-0"
> >
{/* Header */} {isLoading ? (
<ConnectorDialogHeader <Spinner size="sm" />
activeTab={activeTab} ) : (
totalSourceCount={activeConnectorsCount} <>
searchQuery={searchQuery} <Cable className="size-4 stroke-[1.5px]" />
onTabChange={handleTabChange} {activeConnectorsCount > 0 && (
onSearchChange={setSearchQuery} <span className="absolute -top-0.5 right-0 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm select-none">
isScrolled={isScrolled} {activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
</span>
)}
</>
)}
</TooltipIconButton>
)}
<DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 dark:ring-0 bg-muted dark:bg-muted text-foreground focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5 select-none">
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
{/* YouTube Crawler View - shown when adding YouTube videos */}
{isYouTubeView && searchSpaceId ? (
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} />
) : viewingMCPList ? (
<ConnectorAccountsListView
connectorType="MCP_CONNECTOR"
connectorTitle="MCP Connectors"
connectors={(allConnectors || []) as SearchSourceConnector[]}
indexingConnectorIds={indexingConnectorIds}
onBack={handleBackFromMCPList}
onManage={handleStartEdit}
onAddAccount={handleAddNewMCPFromList}
addButtonText="Add New MCP Server"
/> />
) : viewingAccountsType ? (
<ConnectorAccountsListView
connectorType={viewingAccountsType.connectorType}
connectorTitle={viewingAccountsType.connectorTitle}
connectors={(connectors || []) as SearchSourceConnector[]} // Using Electric SQL + PGlite for real-time connector updates (all connector types)
indexingConnectorIds={indexingConnectorIds}
onBack={handleBackFromAccountsList}
onManage={handleStartEdit}
onAddAccount={() => {
// Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS
const oauthConnector =
OAUTH_CONNECTORS.find(
(c) => c.connectorType === viewingAccountsType.connectorType
) ||
COMPOSIO_CONNECTORS.find(
(c) => c.connectorType === viewingAccountsType.connectorType
);
if (oauthConnector) {
handleConnectOAuth(oauthConnector);
}
}}
isConnecting={connectingId !== null}
/>
) : connectingConnectorType ? (
<ConnectorConnectView
connectorType={connectingConnectorType}
onSubmit={(formData) => handleSubmitConnectForm(formData, startIndexing)}
onBack={handleBackFromConnect}
isSubmitting={isCreatingConnector}
/>
) : editingConnector ? (
<ConnectorEditView
connector={{
...editingConnector,
config: connectorConfig || editingConnector.config,
name: editingConnector.name,
// Sync last_indexed_at with live data from Electric SQL for real-time updates
last_indexed_at:
(connectors as SearchSourceConnector[]).find((c) => c.id === editingConnector.id)
?.last_indexed_at ?? editingConnector.last_indexed_at,
}}
startDate={startDate}
endDate={endDate}
periodicEnabled={periodicEnabled}
frequencyMinutes={frequencyMinutes}
enableSummary={enableSummary}
isSaving={isSaving}
isDisconnecting={isDisconnecting}
isIndexing={indexingConnectorIds.has(editingConnector.id)}
searchSpaceId={searchSpaceId?.toString()}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onPeriodicEnabledChange={setPeriodicEnabled}
onFrequencyChange={setFrequencyMinutes}
onEnableSummaryChange={setEnableSummary}
onSave={() => {
startIndexing(editingConnector.id);
handleSaveConnector(() => refreshConnectors());
}}
onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())}
onBack={handleBackFromEdit}
onQuickIndex={
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR"
? () => {
startIndexing(editingConnector.id);
handleQuickIndexConnector(
editingConnector.id,
editingConnector.connector_type,
stopIndexing,
startDate,
endDate
);
}
: undefined
}
onConfigChange={setConnectorConfig}
onNameChange={setConnectorName}
/>
) : indexingConfig ? (
<IndexingConfigurationView
config={indexingConfig}
connector={
indexingConnector
? {
...indexingConnector,
config: indexingConnectorConfig || indexingConnector.config,
}
: undefined
}
startDate={startDate}
endDate={endDate}
periodicEnabled={periodicEnabled}
frequencyMinutes={frequencyMinutes}
enableSummary={enableSummary}
isStartingIndexing={isStartingIndexing}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onPeriodicEnabledChange={setPeriodicEnabled}
onFrequencyChange={setFrequencyMinutes}
onEnableSummaryChange={setEnableSummary}
onConfigChange={setIndexingConnectorConfig}
onStartIndexing={() => {
if (indexingConfig.connectorId) {
startIndexing(indexingConfig.connectorId);
}
handleStartIndexing(() => refreshConnectors());
}}
onSkip={handleSkipIndexing}
/>
) : (
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className="flex-1 flex flex-col min-h-0"
>
{/* Header */}
<ConnectorDialogHeader
activeTab={activeTab}
totalSourceCount={activeConnectorsCount}
searchQuery={searchQuery}
onTabChange={handleTabChange}
onSearchChange={setSearchQuery}
isScrolled={isScrolled}
/>
{/* Content */} {/* Content */}
<div className="flex-1 min-h-0 relative overflow-hidden"> <div className="flex-1 min-h-0 relative overflow-hidden">
<div className="h-full overflow-y-auto" onScroll={handleScroll}> <div className="h-full overflow-y-auto" onScroll={handleScroll}>
<div className="px-4 sm:px-12 py-4 sm:py-8 pb-12 sm:pb-16"> <div className="px-4 sm:px-12 py-4 sm:py-8 pb-12 sm:pb-16">
{/* LLM Configuration Warning */} {/* LLM Configuration Warning */}
{!llmConfigLoading && !hasDocumentSummaryLLM && ( {!llmConfigLoading && !hasDocumentSummaryLLM && (
<Alert variant="destructive" className="mb-6"> <Alert variant="destructive" className="mb-6">
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
<AlertTitle>LLM Configuration Required</AlertTitle> <AlertTitle>LLM Configuration Required</AlertTitle>
<AlertDescription className="mt-2"> <AlertDescription className="mt-2">
<p className="mb-3"> <p className="mb-3">
{isAutoMode && !hasGlobalConfigs {isAutoMode && !hasGlobalConfigs
? "Auto mode is selected but no global LLM configurations are available. Please configure a custom LLM in Settings to process and summarize documents from your connected sources." ? "Auto mode is selected but no global LLM configurations are available. Please configure a custom LLM in Settings to process and summarize documents from your connected sources."
: "You need to configure a Document Summary LLM before adding connectors. This LLM is used to process and summarize documents from your connected sources."} : "You need to configure a Document Summary LLM before adding connectors. This LLM is used to process and summarize documents from your connected sources."}
</p> </p>
<Button asChild size="sm" variant="outline"> <Button asChild size="sm" variant="outline">
<Link href={`/dashboard/${searchSpaceId}/settings?tab=models`}> <Link href={`/dashboard/${searchSpaceId}/settings?tab=models`}>
<Settings className="mr-2 h-4 w-4" /> <Settings className="mr-2 h-4 w-4" />
Go to Settings Go to Settings
</Link> </Link>
</Button> </Button>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
<TabsContent value="all" className="m-0"> <TabsContent value="all" className="m-0">
<AllConnectorsTab <AllConnectorsTab
searchQuery={searchQuery}
searchSpaceId={searchSpaceId}
connectedTypes={connectedTypes}
connectingId={connectingId}
allConnectors={connectors}
documentTypeCounts={documentTypeCounts}
indexingConnectorIds={indexingConnectorIds}
onConnectOAuth={hasDocumentSummaryLLM ? handleConnectOAuth : () => {}}
onConnectNonOAuth={hasDocumentSummaryLLM ? handleConnectNonOAuth : () => {}}
onCreateWebcrawler={
hasDocumentSummaryLLM ? handleCreateWebcrawler : () => {}
}
onCreateYouTubeCrawler={
hasDocumentSummaryLLM ? handleCreateYouTubeCrawler : () => {}
}
onManage={handleStartEdit}
onViewAccountsList={handleViewAccountsList}
/>
</TabsContent>
<ActiveConnectorsTab
searchQuery={searchQuery} searchQuery={searchQuery}
searchSpaceId={searchSpaceId} hasSources={hasSources}
connectedTypes={connectedTypes} totalSourceCount={totalSourceCount}
connectingId={connectingId} activeDocumentTypes={activeDocumentTypes}
allConnectors={connectors} connectors={connectors as SearchSourceConnector[]}
documentTypeCounts={documentTypeCounts}
indexingConnectorIds={indexingConnectorIds} indexingConnectorIds={indexingConnectorIds}
onConnectOAuth={hasDocumentSummaryLLM ? handleConnectOAuth : () => {}} onTabChange={handleTabChange}
onConnectNonOAuth={hasDocumentSummaryLLM ? handleConnectNonOAuth : () => {}}
onCreateWebcrawler={hasDocumentSummaryLLM ? handleCreateWebcrawler : () => {}}
onCreateYouTubeCrawler={
hasDocumentSummaryLLM ? handleCreateYouTubeCrawler : () => {}
}
onManage={handleStartEdit} onManage={handleStartEdit}
onViewAccountsList={handleViewAccountsList} onViewAccountsList={handleViewAccountsList}
/> />
</TabsContent> </div>
<ActiveConnectorsTab
searchQuery={searchQuery}
hasSources={hasSources}
totalSourceCount={totalSourceCount}
activeDocumentTypes={activeDocumentTypes}
connectors={connectors as SearchSourceConnector[]}
indexingConnectorIds={indexingConnectorIds}
onTabChange={handleTabChange}
onManage={handleStartEdit}
onViewAccountsList={handleViewAccountsList}
/>
</div> </div>
{/* Bottom fade shadow */}
<div className="absolute bottom-0 left-0 right-0 h-7 bg-linear-to-t from-muted via-muted/80 to-transparent pointer-events-none z-10" />
</div> </div>
{/* Bottom fade shadow */} </Tabs>
<div className="absolute bottom-0 left-0 right-0 h-7 bg-linear-to-t from-muted via-muted/80 to-transparent pointer-events-none z-10" /> )}
</div> </DialogContent>
</Tabs> </Dialog>
)} );
</DialogContent> }
</Dialog> );
);
});
ConnectorIndicator.displayName = "ConnectorIndicator"; ConnectorIndicator.displayName = "ConnectorIndicator";

View file

@ -63,7 +63,8 @@ export const WebcrawlerConfig: FC<ConnectorConfigProps> = ({ connector, onConfig
<div className="flex items-start gap-3 rounded-lg border border-blue-200/50 bg-blue-50/50 dark:border-blue-500/20 dark:bg-blue-950/20 p-3 text-xs sm:text-sm"> <div className="flex items-start gap-3 rounded-lg border border-blue-200/50 bg-blue-50/50 dark:border-blue-500/20 dark:bg-blue-950/20 p-3 text-xs sm:text-sm">
<Info className="size-4 mt-0.5 shrink-0 text-blue-600 dark:text-blue-400" /> <Info className="size-4 mt-0.5 shrink-0 text-blue-600 dark:text-blue-400" />
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Want a quick answer from a webpage without indexing it? Just paste the URL directly into the chat instead. Want a quick answer from a webpage without indexing it? Just paste the URL directly into
the chat instead.
</p> </p>
</div> </div>

View file

@ -280,9 +280,7 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId,
<div className="flex items-start gap-3 rounded-lg border border-blue-200/50 bg-blue-50/50 dark:border-blue-500/20 dark:bg-blue-950/20 p-4 text-sm"> <div className="flex items-start gap-3 rounded-lg border border-blue-200/50 bg-blue-50/50 dark:border-blue-500/20 dark:bg-blue-950/20 p-4 text-sm">
<Info className="size-4 mt-0.5 shrink-0 text-blue-600 dark:text-blue-400" /> <Info className="size-4 mt-0.5 shrink-0 text-blue-600 dark:text-blue-400" />
<p className="text-muted-foreground"> <p className="text-muted-foreground">{t("chat_tip")}</p>
{t("chat_tip")}
</p>
</div> </div>
<div className="bg-muted/50 rounded-lg p-4 text-sm"> <div className="bg-muted/50 rounded-lg p-4 text-sm">

View file

@ -14,7 +14,6 @@ import {
AlertCircle, AlertCircle,
ArrowDownIcon, ArrowDownIcon,
ArrowUpIcon, ArrowUpIcon,
Unplug,
CheckIcon, CheckIcon,
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
@ -24,6 +23,7 @@ import {
RefreshCwIcon, RefreshCwIcon,
SquareIcon, SquareIcon,
SquareLibrary, SquareLibrary,
Unplug,
Upload, Upload,
X, X,
} from "lucide-react"; } from "lucide-react";
@ -47,12 +47,12 @@ import {
} from "@/atoms/new-llm-config/new-llm-config-query.atoms"; } from "@/atoms/new-llm-config/new-llm-config-query.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { AssistantMessage } from "@/components/assistant-ui/assistant-message"; import { AssistantMessage } from "@/components/assistant-ui/assistant-message";
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
import { ChatSessionStatus } from "@/components/assistant-ui/chat-session-status"; import { ChatSessionStatus } from "@/components/assistant-ui/chat-session-status";
import { import {
ConnectorIndicator, ConnectorIndicator,
type ConnectorIndicatorHandle, type ConnectorIndicatorHandle,
} from "@/components/assistant-ui/connector-popup"; } from "@/components/assistant-ui/connector-popup";
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
import { import {
InlineMentionEditor, InlineMentionEditor,
type InlineMentionEditorRef, type InlineMentionEditorRef,
@ -65,6 +65,7 @@ import {
import { ToolFallback } from "@/components/assistant-ui/tool-fallback"; import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { UserMessage } from "@/components/assistant-ui/user-message"; import { UserMessage } from "@/components/assistant-ui/user-message";
import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/components/layout/ui/sidebar/SidebarSlideOutPanel";
import { import {
DocumentMentionPicker, DocumentMentionPicker,
type DocumentMentionPickerRef, type DocumentMentionPickerRef,
@ -83,7 +84,6 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { Document } from "@/contracts/types/document.types"; import type { Document } from "@/contracts/types/document.types";
import { useBatchCommentsPreload } from "@/hooks/use-comments"; import { useBatchCommentsPreload } from "@/hooks/use-comments";
import { useCommentsElectric } from "@/hooks/use-comments-electric"; import { useCommentsElectric } from "@/hooks/use-comments-electric";
import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/components/layout/ui/sidebar/SidebarSlideOutPanel";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
/** Placeholder texts that cycle in new chats when input is empty */ /** Placeholder texts that cycle in new chats when input is empty */
@ -273,16 +273,10 @@ const ConnectToolsBanner: FC = () => {
onClick={() => setConnectorDialogOpen(true)} onClick={() => setConnectorDialogOpen(true)}
> >
<Unplug className="size-4 text-muted-foreground/70 shrink-0" /> <Unplug className="size-4 text-muted-foreground/70 shrink-0" />
<span className="text-[13px] text-muted-foreground/80 flex-1"> <span className="text-[13px] text-muted-foreground/80 flex-1">Connect your tools</span>
Connect your tools
</span>
<AvatarGroup className="shrink-0"> <AvatarGroup className="shrink-0">
{BANNER_CONNECTORS.map(({ type, label }, i) => ( {BANNER_CONNECTORS.map(({ type, label }, i) => (
<Avatar <Avatar key={type} className="size-6" style={{ zIndex: BANNER_CONNECTORS.length - i }}>
key={type}
className="size-6"
style={{ zIndex: BANNER_CONNECTORS.length - i }}
>
<AvatarFallback className="bg-muted text-[10px]"> <AvatarFallback className="bg-muted text-[10px]">
{getConnectorIcon(type, "size-3.5")} {getConnectorIcon(type, "size-3.5")}
</AvatarFallback> </AvatarFallback>
@ -516,9 +510,9 @@ const Composer: FC = () => {
currentUserId={currentUser?.id ?? null} currentUserId={currentUser?.id ?? null}
members={members ?? []} members={members ?? []}
/> />
<div className="aui-composer-attachment-dropzone flex w-full flex-col overflow-hidden rounded-2xl border-input bg-muted pt-2 outline-none transition-shadow"> <div className="aui-composer-attachment-dropzone flex w-full flex-col overflow-hidden rounded-2xl border-input bg-muted pt-2 outline-none transition-shadow">
{/* Inline editor with @mention support */} {/* Inline editor with @mention support */}
<div ref={editorContainerRef} className="aui-composer-input-wrapper px-4 pt-3 pb-6"> <div ref={editorContainerRef} className="aui-composer-input-wrapper px-4 pt-3 pb-6">
<InlineMentionEditor <InlineMentionEditor
ref={editorRef} ref={editorRef}
placeholder={currentPlaceholder} placeholder={currentPlaceholder}
@ -658,9 +652,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
<Unplug className="size-4 shrink-0" /> <Unplug className="size-4 shrink-0" />
{connectorCount > 0 ? "Manage connectors" : "Connect your tools"} {connectorCount > 0 ? "Manage connectors" : "Connect your tools"}
{connectorCount > 0 && ( {connectorCount > 0 && (
<span className="ml-auto text-xs text-muted-foreground"> <span className="ml-auto text-xs text-muted-foreground">{connectorCount}</span>
{connectorCount}
</span>
)} )}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@ -685,7 +677,6 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
)} )}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AssistantIf condition={({ thread }) => !thread.isRunning}> <AssistantIf condition={({ thread }) => !thread.isRunning}>
<ComposerPrimitive.Send asChild disabled={isSendDisabled}> <ComposerPrimitive.Send asChild disabled={isSendDisabled}>
<TooltipIconButton <TooltipIconButton

View file

@ -73,7 +73,9 @@ export function CommentPanel({
isInline && "w-full rounded-xl border bg-card shadow-lg max-h-80", isInline && "w-full rounded-xl border bg-card shadow-lg max-h-80",
!isMobile && !isInline && "w-85 rounded-lg border bg-card" !isMobile && !isInline && "w-85 rounded-lg border bg-card"
)} )}
style={!isMobile && !isInline && effectiveMaxHeight ? { maxHeight: effectiveMaxHeight } : undefined} style={
!isMobile && !isInline && effectiveMaxHeight ? { maxHeight: effectiveMaxHeight } : undefined
}
> >
{hasThreads && ( {hasThreads && (
<div className={cn("min-h-0 flex-1 overflow-y-auto scrollbar-thin", isMobile && "pb-24")}> <div className={cn("min-h-0 flex-1 overflow-y-auto scrollbar-thin", isMobile && "pb-24")}>

View file

@ -176,7 +176,6 @@ function GetStartedButton() {
); );
} }
const BackgroundGrids = () => { const BackgroundGrids = () => {
return ( return (
<div className="pointer-events-none absolute inset-0 z-0 grid h-screen w-full -rotate-45 transform select-none grid-cols-2 gap-10 md:grid-cols-4"> <div className="pointer-events-none absolute inset-0 z-0 grid h-screen w-full -rotate-45 transform select-none grid-cols-2 gap-10 md:grid-cols-4">

View file

@ -696,7 +696,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
markAsRead: statusInbox.markAsRead, markAsRead: statusInbox.markAsRead,
markAllAsRead: statusInbox.markAllAsRead, markAllAsRead: statusInbox.markAllAsRead,
}, },
}} }}
announcementsPanel={{ announcementsPanel={{
open: isAnnouncementsSidebarOpen, open: isAnnouncementsSidebarOpen,
onOpenChange: setIsAnnouncementsSidebarOpen, onOpenChange: setIsAnnouncementsSidebarOpen,

View file

@ -1,12 +1,6 @@
"use client"; "use client";
import { import { ArchiveIcon, MoreHorizontal, PenLine, RotateCcwIcon, Trash2 } from "lucide-react";
ArchiveIcon,
MoreHorizontal,
PenLine,
RotateCcwIcon,
Trash2,
} from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";

View file

@ -14,8 +14,8 @@ import {
import { sidebarSelectedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom"; import { sidebarSelectedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom";
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms"; import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms"; import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { DocumentTypeEnum } from "@/contracts/types/document.types"; import type { DocumentTypeEnum } from "@/contracts/types/document.types";
@ -40,7 +40,12 @@ interface DocumentsSidebarProps {
onDockedChange?: (docked: boolean) => void; onDockedChange?: (docked: boolean) => void;
} }
export function DocumentsSidebar({ open, onOpenChange, isDocked = false, onDockedChange }: DocumentsSidebarProps) { export function DocumentsSidebar({
open,
onOpenChange,
isDocked = false,
onDockedChange,
}: DocumentsSidebarProps) {
const t = useTranslations("documents"); const t = useTranslations("documents");
const tSidebar = useTranslations("sidebar"); const tSidebar = useTranslations("sidebar");
const params = useParams(); const params = useParams();
@ -213,25 +218,20 @@ export function DocumentsSidebar({ open, onOpenChange, isDocked = false, onDocke
</div> </div>
</div> </div>
{/* Connected tools strip */} {/* Connected tools strip */}
<div className="shrink-0 mx-4 mb-3 flex select-none items-center gap-2 rounded-lg border bg-muted/50 px-3 py-2"> <div className="shrink-0 mx-4 mb-3 flex select-none items-center gap-2 rounded-lg border bg-muted/50 px-3 py-2">
<button <button
type="button" type="button"
onClick={() => setConnectorDialogOpen(true)} onClick={() => setConnectorDialogOpen(true)}
className="flex items-center gap-2 min-w-0 flex-1 text-left" className="flex items-center gap-2 min-w-0 flex-1 text-left"
> >
<Unplug className="size-4 shrink-0 text-muted-foreground" /> <Unplug className="size-4 shrink-0 text-muted-foreground" />
<span className="truncate text-xs text-muted-foreground"> <span className="truncate text-xs text-muted-foreground">Connect your tools</span>
Connect your tools
</span>
<AvatarGroup className="ml-auto shrink-0"> <AvatarGroup className="ml-auto shrink-0">
{SHOWCASE_CONNECTORS.map(({ type, label }, i) => ( {SHOWCASE_CONNECTORS.map(({ type, label }, i) => (
<Tooltip key={type}> <Tooltip key={type}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Avatar <Avatar className="size-6" style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}>
className="size-6"
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
>
<AvatarFallback className="bg-muted text-[10px]"> <AvatarFallback className="bg-muted text-[10px]">
{getConnectorIcon(type, "size-3.5")} {getConnectorIcon(type, "size-3.5")}
</AvatarFallback> </AvatarFallback>

View file

@ -352,27 +352,51 @@ function ReportPanelContent({
> >
{!shareToken && ( {!shareToken && (
<> <>
<DropdownMenuLabel className="text-xs text-muted-foreground">Documents</DropdownMenuLabel> <DropdownMenuLabel className="text-xs text-muted-foreground">
<DropdownMenuItem onClick={() => handleExport("pdf")} disabled={exporting !== null}> Documents
</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => handleExport("pdf")}
disabled={exporting !== null}
>
PDF (.pdf) PDF (.pdf)
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport("docx")} disabled={exporting !== null}> <DropdownMenuItem
onClick={() => handleExport("docx")}
disabled={exporting !== null}
>
Word (.docx) Word (.docx)
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport("odt")} disabled={exporting !== null}> <DropdownMenuItem
onClick={() => handleExport("odt")}
disabled={exporting !== null}
>
OpenDocument (.odt) OpenDocument (.odt)
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-muted-foreground">Web &amp; E-Book</DropdownMenuLabel> <DropdownMenuLabel className="text-xs text-muted-foreground">
<DropdownMenuItem onClick={() => handleExport("html")} disabled={exporting !== null}> Web &amp; E-Book
</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => handleExport("html")}
disabled={exporting !== null}
>
HTML (.html) HTML (.html)
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport("epub")} disabled={exporting !== null}> <DropdownMenuItem
onClick={() => handleExport("epub")}
disabled={exporting !== null}
>
EPUB (.epub) EPUB (.epub)
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-muted-foreground">Source &amp; Plain</DropdownMenuLabel> <DropdownMenuLabel className="text-xs text-muted-foreground">
<DropdownMenuItem onClick={() => handleExport("latex")} disabled={exporting !== null}> Source &amp; Plain
</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => handleExport("latex")}
disabled={exporting !== null}
>
LaTeX (.tex) LaTeX (.tex)
</DropdownMenuItem> </DropdownMenuItem>
</> </>
@ -381,7 +405,10 @@ function ReportPanelContent({
Markdown (.md) Markdown (.md)
</DropdownMenuItem> </DropdownMenuItem>
{!shareToken && ( {!shareToken && (
<DropdownMenuItem onClick={() => handleExport("plain")} disabled={exporting !== null}> <DropdownMenuItem
onClick={() => handleExport("plain")}
disabled={exporting !== null}
>
Plain Text (.txt) Plain Text (.txt)
</DropdownMenuItem> </DropdownMenuItem>
)} )}

View file

@ -700,7 +700,12 @@ function PermissionsEditor({
tabIndex={0} tabIndex={0}
className="w-full flex items-center justify-between px-3 py-2.5 cursor-pointer hover:bg-muted/40 transition-colors" className="w-full flex items-center justify-between px-3 py-2.5 cursor-pointer hover:bg-muted/40 transition-colors"
onClick={() => toggleCategoryExpanded(category)} onClick={() => toggleCategoryExpanded(category)}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleCategoryExpanded(category); } }} onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggleCategoryExpanded(category);
}
}}
> >
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<IconComponent className="h-4 w-4 text-muted-foreground shrink-0" /> <IconComponent className="h-4 w-4 text-muted-foreground shrink-0" />
@ -763,7 +768,12 @@ function PermissionsEditor({
isSelected ? "bg-muted/60 hover:bg-muted/80" : "hover:bg-muted/40" isSelected ? "bg-muted/60 hover:bg-muted/80" : "hover:bg-muted/40"
)} )}
onClick={() => onTogglePermission(perm.value)} onClick={() => onTogglePermission(perm.value)}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onTogglePermission(perm.value); } }} onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onTogglePermission(perm.value);
}
}}
> >
<div className="flex-1 min-w-0 text-left"> <div className="flex-1 min-w-0 text-left">
<span className="text-sm font-medium">{actionLabel}</span> <span className="text-sm font-medium">{actionLabel}</span>

View file

@ -39,13 +39,7 @@ function AvatarFallback({
} }
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
return ( return <div data-slot="avatar-group" className={cn("flex -space-x-2", className)} {...props} />;
<div
data-slot="avatar-group"
className={cn("flex -space-x-2", className)}
{...props}
/>
);
} }
function AvatarGroupCount({ className, ...props }: React.ComponentProps<"span">) { function AvatarGroupCount({ className, ...props }: React.ComponentProps<"span">) {

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { AnimatePresence, motion } from "motion/react";
import { ChevronLeft, ChevronRight } from "lucide-react"; import { ChevronLeft, ChevronRight } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay"; import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay";

View file

@ -59,7 +59,7 @@ export function useInbox(
searchSpaceId: number | null, searchSpaceId: number | null,
category: NotificationCategory, category: NotificationCategory,
prefetchedUnread?: { total_unread: number; recent_unread: number } | null, prefetchedUnread?: { total_unread: number; recent_unread: number } | null,
prefetchedUnreadReady = true, prefetchedUnreadReady = true
) { ) {
const electricClient = useElectricClient(); const electricClient = useElectricClient();

View file

@ -156,9 +156,7 @@ class NotificationsApiService {
* Get unread counts for all categories in a single request. * Get unread counts for all categories in a single request.
* Replaces 2 separate getUnreadCount calls (comments + status). * Replaces 2 separate getUnreadCount calls (comments + status).
*/ */
getBatchUnreadCounts = async ( getBatchUnreadCounts = async (searchSpaceId?: number): Promise<GetBatchUnreadCountResponse> => {
searchSpaceId?: number
): Promise<GetBatchUnreadCountResponse> => {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (searchSpaceId !== undefined) { if (searchSpaceId !== undefined) {
params.append("search_space_id", String(searchSpaceId)); params.append("search_space_id", String(searchSpaceId));