mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-26 21:39:43 +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 { 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'll upgrade your account instantly.
|
<span className="font-semibold text-foreground">6,000 additional pages</span> at no
|
||||||
|
cost. Contact us and we'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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")}>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 & E-Book</DropdownMenuLabel>
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||||
<DropdownMenuItem onClick={() => handleExport("html")} disabled={exporting !== null}>
|
Web & 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 & Plain</DropdownMenuLabel>
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||||
<DropdownMenuItem onClick={() => handleExport("latex")} disabled={exporting !== null}>
|
Source & 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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">) {
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue