mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
chore: ran linting
This commit is contained in:
parent
ad7bbcbc8f
commit
6a88f9e0eb
18 changed files with 478 additions and 472 deletions
|
|
@ -9,7 +9,14 @@ import { useEffect } from "react";
|
|||
import { toast } from "sonner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
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 {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -108,37 +115,26 @@ export default function MorePagesPage() {
|
|||
<div
|
||||
className={cn(
|
||||
"flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
|
||||
task.completed
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted"
|
||||
task.completed ? "bg-primary text-primary-foreground" : "bg-muted"
|
||||
)}
|
||||
>
|
||||
{task.completed ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Star className="h-4 w-4" />
|
||||
)}
|
||||
{task.completed ? <Check className="h-4 w-4" /> : <Star className="h-4 w-4" />}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm font-medium",
|
||||
task.completed &&
|
||||
"text-muted-foreground line-through"
|
||||
task.completed && "text-muted-foreground line-through"
|
||||
)}
|
||||
>
|
||||
{task.title}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
+{task.pages_reward} pages
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">+{task.pages_reward} pages</p>
|
||||
</div>
|
||||
<Button
|
||||
variant={task.completed ? "ghost" : "outline"}
|
||||
size="sm"
|
||||
disabled={
|
||||
task.completed || completeMutation.isPending
|
||||
}
|
||||
disabled={task.completed || completeMutation.isPending}
|
||||
onClick={() => handleTaskClick(task)}
|
||||
asChild={!task.completed}
|
||||
>
|
||||
|
|
@ -181,8 +177,9 @@ export default function MorePagesPage() {
|
|||
</Badge>
|
||||
</div>
|
||||
<CardDescription>
|
||||
For a limited time, get <span className="font-semibold text-foreground">6,000 additional pages</span> at
|
||||
no cost. Contact us and we'll upgrade your account instantly.
|
||||
For a limited time, get{" "}
|
||||
<span className="font-semibold text-foreground">6,000 additional pages</span> at no
|
||||
cost. Contact us and we'll upgrade your account instantly.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className="pt-2">
|
||||
|
|
@ -196,9 +193,7 @@ export default function MorePagesPage() {
|
|||
<DialogContent className="select-none sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Get in Touch</DialogTitle>
|
||||
<DialogDescription>
|
||||
Pick the option that works best for you.
|
||||
</DialogDescription>
|
||||
<DialogDescription>Pick the option that works best for you.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button asChild>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
import Image from "next/image";
|
||||
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
|
||||
import Image from "next/image";
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
nav: {
|
||||
title: (
|
||||
<>
|
||||
<Image
|
||||
src="/icon-128.svg"
|
||||
alt="SurfSense"
|
||||
width={24}
|
||||
height={24}
|
||||
className="dark:invert"
|
||||
/>
|
||||
<Image src="/icon-128.svg" alt="SurfSense" width={24} height={24} className="dark:invert" />
|
||||
SurfSense Docs
|
||||
</>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ import { useAtomValue } from "jotai";
|
|||
import { CheckIcon, CopyIcon, DownloadIcon, MessageSquare, RefreshCwIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
commentsEnabledAtom,
|
||||
targetCommentIdAtom,
|
||||
} from "@/atoms/chat/current-thread.atom";
|
||||
import { commentsEnabledAtom, targetCommentIdAtom } from "@/atoms/chat/current-thread.atom";
|
||||
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
||||
import {
|
||||
|
|
@ -139,7 +136,8 @@ export const AssistantMessage: FC = () => {
|
|||
commentPanelRef.current?.contains(target) ||
|
||||
commentTriggerRef.current?.contains(target) ||
|
||||
target.closest?.("[data-radix-popper-content-wrapper]")
|
||||
) return;
|
||||
)
|
||||
return;
|
||||
setIsInlineOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
|
@ -178,7 +176,9 @@ export const AssistantMessage: FC = () => {
|
|||
<button
|
||||
ref={isDesktop ? commentTriggerRef : undefined}
|
||||
type="button"
|
||||
onClick={isDesktop ? () => setIsInlineOpen((prev) => !prev) : () => setIsSheetOpen(true)}
|
||||
onClick={
|
||||
isDesktop ? () => setIsInlineOpen((prev) => !prev) : () => setIsSheetOpen(true)
|
||||
}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-full px-3 py-1 text-sm transition-colors",
|
||||
isDesktop && isInlineOpen
|
||||
|
|
@ -206,11 +206,7 @@ export const AssistantMessage: FC = () => {
|
|||
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"
|
||||
>
|
||||
<CommentPanelContainer
|
||||
messageId={dbMessageId}
|
||||
isOpen={true}
|
||||
variant="inline"
|
||||
/>
|
||||
<CommentPanelContainer messageId={dbMessageId} isOpen={true} variant="inline" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import Link from "next/link";
|
|||
import { useSearchParams } from "next/navigation";
|
||||
import { type FC, forwardRef, useImperativeHandle, useMemo } from "react";
|
||||
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
|
||||
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
|
||||
import {
|
||||
globalNewLLMConfigsAtom,
|
||||
llmPreferencesAtom,
|
||||
|
|
@ -19,7 +20,6 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
|||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
|
||||
import { useConnectorsElectric } from "@/hooks/use-connectors-electric";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header";
|
||||
|
|
@ -47,400 +47,407 @@ interface ConnectorIndicatorProps {
|
|||
|
||||
export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>(
|
||||
({ showTrigger = true }, ref) => {
|
||||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||
const searchParams = useSearchParams();
|
||||
const { data: currentUser } = useAtomValue(currentUserAtom);
|
||||
const { data: preferences = {}, isFetching: preferencesLoading } =
|
||||
useAtomValue(llmPreferencesAtom);
|
||||
const { data: globalConfigs = [], isFetching: globalConfigsLoading } =
|
||||
useAtomValue(globalNewLLMConfigsAtom);
|
||||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||
const searchParams = useSearchParams();
|
||||
const { data: currentUser } = useAtomValue(currentUserAtom);
|
||||
const { data: preferences = {}, isFetching: preferencesLoading } =
|
||||
useAtomValue(llmPreferencesAtom);
|
||||
const { data: globalConfigs = [], isFetching: globalConfigsLoading } =
|
||||
useAtomValue(globalNewLLMConfigsAtom);
|
||||
|
||||
// Check if document summary LLM is properly configured
|
||||
// - 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 null/undefined, it's not configured
|
||||
const docSummaryLlmId = preferences.document_summary_llm_id;
|
||||
const isAutoMode = docSummaryLlmId === 0;
|
||||
const hasGlobalConfigs = globalConfigs.length > 0;
|
||||
// Check if document summary LLM is properly configured
|
||||
// - 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 null/undefined, it's not configured
|
||||
const docSummaryLlmId = preferences.document_summary_llm_id;
|
||||
const isAutoMode = docSummaryLlmId === 0;
|
||||
const hasGlobalConfigs = globalConfigs.length > 0;
|
||||
|
||||
const hasDocumentSummaryLLM =
|
||||
docSummaryLlmId !== null &&
|
||||
docSummaryLlmId !== undefined &&
|
||||
// If it's Auto mode, we need global configs to actually be available
|
||||
(!isAutoMode || hasGlobalConfigs);
|
||||
const hasDocumentSummaryLLM =
|
||||
docSummaryLlmId !== null &&
|
||||
docSummaryLlmId !== undefined &&
|
||||
// If it's Auto mode, we need global configs to actually be available
|
||||
(!isAutoMode || hasGlobalConfigs);
|
||||
|
||||
const llmConfigLoading = preferencesLoading || globalConfigsLoading;
|
||||
const llmConfigLoading = preferencesLoading || globalConfigsLoading;
|
||||
|
||||
// Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min)
|
||||
const { data: documentTypeCounts, isFetching: documentTypesLoading } =
|
||||
useAtomValue(documentTypeCountsAtom);
|
||||
// Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min)
|
||||
const { data: documentTypeCounts, isFetching: documentTypesLoading } =
|
||||
useAtomValue(documentTypeCountsAtom);
|
||||
|
||||
// Read status inbox items from shared atom (populated by LayoutDataProvider)
|
||||
// instead of creating a duplicate useInbox("status") hook.
|
||||
const statusInboxItems = useAtomValue(statusInboxItemsAtom);
|
||||
const inboxItems = useMemo(
|
||||
() => statusInboxItems.filter((item) => item.type === "connector_indexing"),
|
||||
[statusInboxItems]
|
||||
);
|
||||
// Read status inbox items from shared atom (populated by LayoutDataProvider)
|
||||
// instead of creating a duplicate useInbox("status") hook.
|
||||
const statusInboxItems = useAtomValue(statusInboxItemsAtom);
|
||||
const inboxItems = useMemo(
|
||||
() => statusInboxItems.filter((item) => item.type === "connector_indexing"),
|
||||
[statusInboxItems]
|
||||
);
|
||||
|
||||
// Check if YouTube view is active
|
||||
const isYouTubeView = searchParams.get("view") === "youtube";
|
||||
// Check if YouTube view is active
|
||||
const isYouTubeView = searchParams.get("view") === "youtube";
|
||||
|
||||
// Use the custom hook for dialog state management
|
||||
const {
|
||||
isOpen,
|
||||
activeTab,
|
||||
connectingId,
|
||||
isScrolled,
|
||||
searchQuery,
|
||||
indexingConfig,
|
||||
indexingConnector,
|
||||
indexingConnectorConfig,
|
||||
editingConnector,
|
||||
connectingConnectorType,
|
||||
isCreatingConnector,
|
||||
startDate,
|
||||
endDate,
|
||||
isStartingIndexing,
|
||||
isSaving,
|
||||
isDisconnecting,
|
||||
periodicEnabled,
|
||||
frequencyMinutes,
|
||||
enableSummary,
|
||||
allConnectors,
|
||||
viewingAccountsType,
|
||||
viewingMCPList,
|
||||
setSearchQuery,
|
||||
setStartDate,
|
||||
setEndDate,
|
||||
setPeriodicEnabled,
|
||||
setFrequencyMinutes,
|
||||
setEnableSummary,
|
||||
handleOpenChange,
|
||||
handleTabChange,
|
||||
handleScroll,
|
||||
handleConnectOAuth,
|
||||
handleConnectNonOAuth,
|
||||
handleCreateWebcrawler,
|
||||
handleCreateYouTubeCrawler,
|
||||
handleSubmitConnectForm,
|
||||
handleStartIndexing,
|
||||
handleSkipIndexing,
|
||||
handleStartEdit,
|
||||
handleSaveConnector,
|
||||
handleDisconnectConnector,
|
||||
handleBackFromEdit,
|
||||
handleBackFromConnect,
|
||||
handleBackFromYouTube,
|
||||
handleViewAccountsList,
|
||||
handleBackFromAccountsList,
|
||||
handleBackFromMCPList,
|
||||
handleAddNewMCPFromList,
|
||||
handleQuickIndexConnector,
|
||||
connectorConfig,
|
||||
setConnectorConfig,
|
||||
setIndexingConnectorConfig,
|
||||
setConnectorName,
|
||||
} = useConnectorDialog();
|
||||
// Use the custom hook for dialog state management
|
||||
const {
|
||||
isOpen,
|
||||
activeTab,
|
||||
connectingId,
|
||||
isScrolled,
|
||||
searchQuery,
|
||||
indexingConfig,
|
||||
indexingConnector,
|
||||
indexingConnectorConfig,
|
||||
editingConnector,
|
||||
connectingConnectorType,
|
||||
isCreatingConnector,
|
||||
startDate,
|
||||
endDate,
|
||||
isStartingIndexing,
|
||||
isSaving,
|
||||
isDisconnecting,
|
||||
periodicEnabled,
|
||||
frequencyMinutes,
|
||||
enableSummary,
|
||||
allConnectors,
|
||||
viewingAccountsType,
|
||||
viewingMCPList,
|
||||
setSearchQuery,
|
||||
setStartDate,
|
||||
setEndDate,
|
||||
setPeriodicEnabled,
|
||||
setFrequencyMinutes,
|
||||
setEnableSummary,
|
||||
handleOpenChange,
|
||||
handleTabChange,
|
||||
handleScroll,
|
||||
handleConnectOAuth,
|
||||
handleConnectNonOAuth,
|
||||
handleCreateWebcrawler,
|
||||
handleCreateYouTubeCrawler,
|
||||
handleSubmitConnectForm,
|
||||
handleStartIndexing,
|
||||
handleSkipIndexing,
|
||||
handleStartEdit,
|
||||
handleSaveConnector,
|
||||
handleDisconnectConnector,
|
||||
handleBackFromEdit,
|
||||
handleBackFromConnect,
|
||||
handleBackFromYouTube,
|
||||
handleViewAccountsList,
|
||||
handleBackFromAccountsList,
|
||||
handleBackFromMCPList,
|
||||
handleAddNewMCPFromList,
|
||||
handleQuickIndexConnector,
|
||||
connectorConfig,
|
||||
setConnectorConfig,
|
||||
setIndexingConnectorConfig,
|
||||
setConnectorName,
|
||||
} = useConnectorDialog();
|
||||
|
||||
// Fetch connectors using Electric SQL + PGlite for real-time updates
|
||||
// This provides instant updates when connectors change, without polling
|
||||
const {
|
||||
connectors: connectorsFromElectric = [],
|
||||
loading: connectorsLoading,
|
||||
error: connectorsError,
|
||||
refreshConnectors: refreshConnectorsElectric,
|
||||
} = useConnectorsElectric(searchSpaceId);
|
||||
// Fetch connectors using Electric SQL + PGlite for real-time updates
|
||||
// This provides instant updates when connectors change, without polling
|
||||
const {
|
||||
connectors: connectorsFromElectric = [],
|
||||
loading: connectorsLoading,
|
||||
error: connectorsError,
|
||||
refreshConnectors: refreshConnectorsElectric,
|
||||
} = useConnectorsElectric(searchSpaceId);
|
||||
|
||||
// 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 API data if: Electric failed (has error) or finished loading with no data
|
||||
const useElectricData =
|
||||
connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError);
|
||||
const connectors = useElectricData ? connectorsFromElectric : allConnectors || [];
|
||||
// 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 API data if: Electric failed (has error) or finished loading with no data
|
||||
const useElectricData =
|
||||
connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError);
|
||||
const connectors = useElectricData ? connectorsFromElectric : allConnectors || [];
|
||||
|
||||
// Manual refresh function that works with both Electric and API
|
||||
const refreshConnectors = async () => {
|
||||
if (useElectricData) {
|
||||
await refreshConnectorsElectric();
|
||||
} else {
|
||||
// Fallback: use allConnectors from useConnectorDialog (which uses connectorsAtom)
|
||||
// The connectorsAtom will handle refetching if needed
|
||||
}
|
||||
};
|
||||
// Manual refresh function that works with both Electric and API
|
||||
const refreshConnectors = async () => {
|
||||
if (useElectricData) {
|
||||
await refreshConnectorsElectric();
|
||||
} else {
|
||||
// Fallback: use allConnectors from useConnectorDialog (which uses connectorsAtom)
|
||||
// The connectorsAtom will handle refetching if needed
|
||||
}
|
||||
};
|
||||
|
||||
// Track indexing state locally - clears automatically when Electric SQL detects last_indexed_at changed
|
||||
// Also clears when failed notifications are detected
|
||||
const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors(
|
||||
connectors as SearchSourceConnector[],
|
||||
inboxItems
|
||||
);
|
||||
// Track indexing state locally - clears automatically when Electric SQL detects last_indexed_at changed
|
||||
// Also clears when failed notifications are detected
|
||||
const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors(
|
||||
connectors as SearchSourceConnector[],
|
||||
inboxItems
|
||||
);
|
||||
|
||||
const isLoading = connectorsLoading || documentTypesLoading;
|
||||
const isLoading = connectorsLoading || documentTypesLoading;
|
||||
|
||||
// Get document types that have documents in the search space
|
||||
const activeDocumentTypes = documentTypeCounts
|
||||
? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
|
||||
: [];
|
||||
// Get document types that have documents in the search space
|
||||
const activeDocumentTypes = documentTypeCounts
|
||||
? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
|
||||
: [];
|
||||
|
||||
const hasConnectors = connectors.length > 0;
|
||||
const hasSources = hasConnectors || activeDocumentTypes.length > 0;
|
||||
const totalSourceCount = connectors.length + activeDocumentTypes.length;
|
||||
const hasConnectors = connectors.length > 0;
|
||||
const hasSources = hasConnectors || activeDocumentTypes.length > 0;
|
||||
const totalSourceCount = connectors.length + activeDocumentTypes.length;
|
||||
|
||||
const activeConnectorsCount = connectors.length;
|
||||
const activeConnectorsCount = connectors.length;
|
||||
|
||||
// Check which connectors are already connected
|
||||
// Using Electric SQL + PGlite for real-time connector updates
|
||||
const connectedTypes = new Set<string>(
|
||||
(connectors || []).map((c: SearchSourceConnector) => c.connector_type)
|
||||
);
|
||||
// Check which connectors are already connected
|
||||
// Using Electric SQL + PGlite for real-time connector updates
|
||||
const connectedTypes = new Set<string>(
|
||||
(connectors || []).map((c: SearchSourceConnector) => c.connector_type)
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => handleOpenChange(true),
|
||||
}));
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => handleOpenChange(true),
|
||||
}));
|
||||
|
||||
if (!searchSpaceId) return null;
|
||||
if (!searchSpaceId) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
{showTrigger && (
|
||||
<TooltipIconButton
|
||||
data-joyride="connector-icon"
|
||||
tooltip={hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"}
|
||||
side="bottom"
|
||||
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
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
{showTrigger && (
|
||||
<TooltipIconButton
|
||||
data-joyride="connector-icon"
|
||||
tooltip={
|
||||
hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"
|
||||
}
|
||||
onConfigChange={setConnectorConfig}
|
||||
onNameChange={setConnectorName}
|
||||
/>
|
||||
) : indexingConfig ? (
|
||||
<IndexingConfigurationView
|
||||
config={indexingConfig}
|
||||
connector={
|
||||
indexingConnector
|
||||
? {
|
||||
...indexingConnector,
|
||||
config: indexingConnectorConfig || indexingConnector.config,
|
||||
}
|
||||
: undefined
|
||||
side="bottom"
|
||||
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"
|
||||
}
|
||||
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"
|
||||
onClick={() => handleOpenChange(true)}
|
||||
>
|
||||
{/* Header */}
|
||||
<ConnectorDialogHeader
|
||||
activeTab={activeTab}
|
||||
totalSourceCount={activeConnectorsCount}
|
||||
searchQuery={searchQuery}
|
||||
onTabChange={handleTabChange}
|
||||
onSearchChange={setSearchQuery}
|
||||
isScrolled={isScrolled}
|
||||
{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}
|
||||
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 */}
|
||||
<div className="flex-1 min-h-0 relative overflow-hidden">
|
||||
<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">
|
||||
{/* LLM Configuration Warning */}
|
||||
{!llmConfigLoading && !hasDocumentSummaryLLM && (
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>LLM Configuration Required</AlertTitle>
|
||||
<AlertDescription className="mt-2">
|
||||
<p className="mb-3">
|
||||
{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."
|
||||
: "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>
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<Link href={`/dashboard/${searchSpaceId}/settings?tab=models`}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Go to Settings
|
||||
</Link>
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-h-0 relative overflow-hidden">
|
||||
<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">
|
||||
{/* LLM Configuration Warning */}
|
||||
{!llmConfigLoading && !hasDocumentSummaryLLM && (
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>LLM Configuration Required</AlertTitle>
|
||||
<AlertDescription className="mt-2">
|
||||
<p className="mb-3">
|
||||
{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."
|
||||
: "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>
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<Link href={`/dashboard/${searchSpaceId}/settings?tab=models`}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Go to Settings
|
||||
</Link>
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TabsContent value="all" className="m-0">
|
||||
<AllConnectorsTab
|
||||
<TabsContent value="all" className="m-0">
|
||||
<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}
|
||||
searchSpaceId={searchSpaceId}
|
||||
connectedTypes={connectedTypes}
|
||||
connectingId={connectingId}
|
||||
allConnectors={connectors}
|
||||
documentTypeCounts={documentTypeCounts}
|
||||
hasSources={hasSources}
|
||||
totalSourceCount={totalSourceCount}
|
||||
activeDocumentTypes={activeDocumentTypes}
|
||||
connectors={connectors as SearchSourceConnector[]}
|
||||
indexingConnectorIds={indexingConnectorIds}
|
||||
onConnectOAuth={hasDocumentSummaryLLM ? handleConnectOAuth : () => {}}
|
||||
onConnectNonOAuth={hasDocumentSummaryLLM ? handleConnectNonOAuth : () => {}}
|
||||
onCreateWebcrawler={hasDocumentSummaryLLM ? handleCreateWebcrawler : () => {}}
|
||||
onCreateYouTubeCrawler={
|
||||
hasDocumentSummaryLLM ? handleCreateYouTubeCrawler : () => {}
|
||||
}
|
||||
onTabChange={handleTabChange}
|
||||
onManage={handleStartEdit}
|
||||
onViewAccountsList={handleViewAccountsList}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<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>
|
||||
{/* 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>
|
||||
</Tabs>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
</Tabs>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ConnectorIndicator.displayName = "ConnectorIndicator";
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<Info className="size-4 mt-0.5 shrink-0 text-blue-600 dark:text-blue-400" />
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<Info className="size-4 mt-0.5 shrink-0 text-blue-600 dark:text-blue-400" />
|
||||
<p className="text-muted-foreground">
|
||||
{t("chat_tip")}
|
||||
</p>
|
||||
<p className="text-muted-foreground">{t("chat_tip")}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted/50 rounded-lg p-4 text-sm">
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
AlertCircle,
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
Unplug,
|
||||
CheckIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
|
|
@ -24,6 +23,7 @@ import {
|
|||
RefreshCwIcon,
|
||||
SquareIcon,
|
||||
SquareLibrary,
|
||||
Unplug,
|
||||
Upload,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
|
@ -47,12 +47,12 @@ import {
|
|||
} from "@/atoms/new-llm-config/new-llm-config-query.atoms";
|
||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||
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 {
|
||||
ConnectorIndicator,
|
||||
type ConnectorIndicatorHandle,
|
||||
} from "@/components/assistant-ui/connector-popup";
|
||||
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
|
||||
import {
|
||||
InlineMentionEditor,
|
||||
type InlineMentionEditorRef,
|
||||
|
|
@ -65,6 +65,7 @@ import {
|
|||
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { UserMessage } from "@/components/assistant-ui/user-message";
|
||||
import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/components/layout/ui/sidebar/SidebarSlideOutPanel";
|
||||
import {
|
||||
DocumentMentionPicker,
|
||||
type DocumentMentionPickerRef,
|
||||
|
|
@ -83,7 +84,6 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|||
import type { Document } from "@/contracts/types/document.types";
|
||||
import { useBatchCommentsPreload } from "@/hooks/use-comments";
|
||||
import { useCommentsElectric } from "@/hooks/use-comments-electric";
|
||||
import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/components/layout/ui/sidebar/SidebarSlideOutPanel";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** Placeholder texts that cycle in new chats when input is empty */
|
||||
|
|
@ -273,16 +273,10 @@ const ConnectToolsBanner: FC = () => {
|
|||
onClick={() => setConnectorDialogOpen(true)}
|
||||
>
|
||||
<Unplug className="size-4 text-muted-foreground/70 shrink-0" />
|
||||
<span className="text-[13px] text-muted-foreground/80 flex-1">
|
||||
Connect your tools
|
||||
</span>
|
||||
<span className="text-[13px] text-muted-foreground/80 flex-1">Connect your tools</span>
|
||||
<AvatarGroup className="shrink-0">
|
||||
{BANNER_CONNECTORS.map(({ type, label }, i) => (
|
||||
<Avatar
|
||||
key={type}
|
||||
className="size-6"
|
||||
style={{ zIndex: BANNER_CONNECTORS.length - i }}
|
||||
>
|
||||
<Avatar key={type} className="size-6" style={{ zIndex: BANNER_CONNECTORS.length - i }}>
|
||||
<AvatarFallback className="bg-muted text-[10px]">
|
||||
{getConnectorIcon(type, "size-3.5")}
|
||||
</AvatarFallback>
|
||||
|
|
@ -516,9 +510,9 @@ const Composer: FC = () => {
|
|||
currentUserId={currentUser?.id ?? null}
|
||||
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">
|
||||
{/* Inline editor with @mention support */}
|
||||
<div ref={editorContainerRef} className="aui-composer-input-wrapper px-4 pt-3 pb-6">
|
||||
<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 */}
|
||||
<div ref={editorContainerRef} className="aui-composer-input-wrapper px-4 pt-3 pb-6">
|
||||
<InlineMentionEditor
|
||||
ref={editorRef}
|
||||
placeholder={currentPlaceholder}
|
||||
|
|
@ -658,9 +652,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
<Unplug className="size-4 shrink-0" />
|
||||
{connectorCount > 0 ? "Manage connectors" : "Connect your tools"}
|
||||
{connectorCount > 0 && (
|
||||
<span className="ml-auto text-xs text-muted-foreground">
|
||||
{connectorCount}
|
||||
</span>
|
||||
<span className="ml-auto text-xs text-muted-foreground">{connectorCount}</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
|
@ -685,7 +677,6 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<AssistantIf condition={({ thread }) => !thread.isRunning}>
|
||||
<ComposerPrimitive.Send asChild disabled={isSendDisabled}>
|
||||
<TooltipIconButton
|
||||
|
|
|
|||
|
|
@ -73,7 +73,9 @@ export function CommentPanel({
|
|||
isInline && "w-full rounded-xl border bg-card shadow-lg max-h-80",
|
||||
!isMobile && !isInline && "w-85 rounded-lg border bg-card"
|
||||
)}
|
||||
style={!isMobile && !isInline && effectiveMaxHeight ? { maxHeight: effectiveMaxHeight } : undefined}
|
||||
style={
|
||||
!isMobile && !isInline && effectiveMaxHeight ? { maxHeight: effectiveMaxHeight } : undefined
|
||||
}
|
||||
>
|
||||
{hasThreads && (
|
||||
<div className={cn("min-h-0 flex-1 overflow-y-auto scrollbar-thin", isMobile && "pb-24")}>
|
||||
|
|
|
|||
|
|
@ -176,7 +176,6 @@ function GetStartedButton() {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
const BackgroundGrids = () => {
|
||||
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">
|
||||
|
|
|
|||
|
|
@ -696,7 +696,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
markAsRead: statusInbox.markAsRead,
|
||||
markAllAsRead: statusInbox.markAllAsRead,
|
||||
},
|
||||
}}
|
||||
}}
|
||||
announcementsPanel={{
|
||||
open: isAnnouncementsSidebarOpen,
|
||||
onOpenChange: setIsAnnouncementsSidebarOpen,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
ArchiveIcon,
|
||||
MoreHorizontal,
|
||||
PenLine,
|
||||
RotateCcwIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { ArchiveIcon, MoreHorizontal, PenLine, RotateCcwIcon, Trash2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import {
|
|||
import { sidebarSelectedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom";
|
||||
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
|
||||
import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
||||
|
|
@ -40,7 +40,12 @@ interface DocumentsSidebarProps {
|
|||
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 tSidebar = useTranslations("sidebar");
|
||||
const params = useParams();
|
||||
|
|
@ -213,25 +218,20 @@ export function DocumentsSidebar({ open, onOpenChange, isDocked = false, onDocke
|
|||
</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">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConnectorDialogOpen(true)}
|
||||
className="flex items-center gap-2 min-w-0 flex-1 text-left"
|
||||
>
|
||||
<Unplug className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
Connect your tools
|
||||
</span>
|
||||
<Unplug className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate text-xs text-muted-foreground">Connect your tools</span>
|
||||
<AvatarGroup className="ml-auto shrink-0">
|
||||
{SHOWCASE_CONNECTORS.map(({ type, label }, i) => (
|
||||
<Tooltip key={type}>
|
||||
<TooltipTrigger asChild>
|
||||
<Avatar
|
||||
className="size-6"
|
||||
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
|
||||
>
|
||||
<Avatar className="size-6" style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}>
|
||||
<AvatarFallback className="bg-muted text-[10px]">
|
||||
{getConnectorIcon(type, "size-3.5")}
|
||||
</AvatarFallback>
|
||||
|
|
|
|||
|
|
@ -352,27 +352,51 @@ function ReportPanelContent({
|
|||
>
|
||||
{!shareToken && (
|
||||
<>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">Documents</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => handleExport("pdf")} disabled={exporting !== null}>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
Documents
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleExport("pdf")}
|
||||
disabled={exporting !== null}
|
||||
>
|
||||
PDF (.pdf)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleExport("docx")} disabled={exporting !== null}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleExport("docx")}
|
||||
disabled={exporting !== null}
|
||||
>
|
||||
Word (.docx)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleExport("odt")} disabled={exporting !== null}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleExport("odt")}
|
||||
disabled={exporting !== null}
|
||||
>
|
||||
OpenDocument (.odt)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">Web & E-Book</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => handleExport("html")} disabled={exporting !== null}>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
Web & E-Book
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleExport("html")}
|
||||
disabled={exporting !== null}
|
||||
>
|
||||
HTML (.html)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleExport("epub")} disabled={exporting !== null}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleExport("epub")}
|
||||
disabled={exporting !== null}
|
||||
>
|
||||
EPUB (.epub)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">Source & Plain</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => handleExport("latex")} disabled={exporting !== null}>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
Source & Plain
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleExport("latex")}
|
||||
disabled={exporting !== null}
|
||||
>
|
||||
LaTeX (.tex)
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
|
|
@ -381,7 +405,10 @@ function ReportPanelContent({
|
|||
Markdown (.md)
|
||||
</DropdownMenuItem>
|
||||
{!shareToken && (
|
||||
<DropdownMenuItem onClick={() => handleExport("plain")} disabled={exporting !== null}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleExport("plain")}
|
||||
disabled={exporting !== null}
|
||||
>
|
||||
Plain Text (.txt)
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -700,7 +700,12 @@ function PermissionsEditor({
|
|||
tabIndex={0}
|
||||
className="w-full flex items-center justify-between px-3 py-2.5 cursor-pointer hover:bg-muted/40 transition-colors"
|
||||
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">
|
||||
<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"
|
||||
)}
|
||||
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">
|
||||
<span className="text-sm font-medium">{actionLabel}</span>
|
||||
|
|
|
|||
|
|
@ -39,13 +39,7 @@ function AvatarFallback({
|
|||
}
|
||||
|
||||
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group"
|
||||
className={cn("flex -space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <div data-slot="avatar-group" className={cn("flex -space-x-2", className)} {...props} />;
|
||||
}
|
||||
|
||||
function AvatarGroupCount({ className, ...props }: React.ComponentProps<"span">) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay";
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export function useInbox(
|
|||
searchSpaceId: number | null,
|
||||
category: NotificationCategory,
|
||||
prefetchedUnread?: { total_unread: number; recent_unread: number } | null,
|
||||
prefetchedUnreadReady = true,
|
||||
prefetchedUnreadReady = true
|
||||
) {
|
||||
const electricClient = useElectricClient();
|
||||
|
||||
|
|
|
|||
|
|
@ -156,9 +156,7 @@ class NotificationsApiService {
|
|||
* Get unread counts for all categories in a single request.
|
||||
* Replaces 2 separate getUnreadCount calls (comments + status).
|
||||
*/
|
||||
getBatchUnreadCounts = async (
|
||||
searchSpaceId?: number
|
||||
): Promise<GetBatchUnreadCountResponse> => {
|
||||
getBatchUnreadCounts = async (searchSpaceId?: number): Promise<GetBatchUnreadCountResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (searchSpaceId !== undefined) {
|
||||
params.append("search_space_id", String(searchSpaceId));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue