mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
chore: linting
This commit is contained in:
parent
c979609041
commit
db652116d6
28 changed files with 491 additions and 476 deletions
|
|
@ -228,7 +228,7 @@ async def create_surfsense_deep_agent(
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.warning(f"Failed to discover available connectors/document types: {e}")
|
logging.warning(f"Failed to discover available connectors/document types: {e}")
|
||||||
|
|
||||||
# Build dependencies dict for the tools registry
|
# Build dependencies dict for the tools registry
|
||||||
visibility = thread_visibility or ChatVisibility.PRIVATE
|
visibility = thread_visibility or ChatVisibility.PRIVATE
|
||||||
dependencies = {
|
dependencies = {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ from typing import Any
|
||||||
|
|
||||||
from langchain_core.tools import BaseTool
|
from langchain_core.tools import BaseTool
|
||||||
|
|
||||||
|
from app.db import ChatVisibility
|
||||||
|
|
||||||
from .display_image import create_display_image_tool
|
from .display_image import create_display_image_tool
|
||||||
from .generate_image import create_generate_image_tool
|
from .generate_image import create_generate_image_tool
|
||||||
from .knowledge_base import create_search_knowledge_base_tool
|
from .knowledge_base import create_search_knowledge_base_tool
|
||||||
|
|
@ -57,8 +59,6 @@ from .shared_memory import (
|
||||||
)
|
)
|
||||||
from .user_memory import create_recall_memory_tool, create_save_memory_tool
|
from .user_memory import create_recall_memory_tool, create_save_memory_tool
|
||||||
|
|
||||||
from app.db import ChatVisibility
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Tool Definition
|
# Tool Definition
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,9 @@ async def recall_shared_memory(
|
||||||
}
|
}
|
||||||
for m in rows
|
for m in rows
|
||||||
]
|
]
|
||||||
created_by_ids = list({m["created_by_id"] for m in memory_list if m["created_by_id"]})
|
created_by_ids = list(
|
||||||
|
{m["created_by_id"] for m in memory_list if m["created_by_id"]}
|
||||||
|
)
|
||||||
created_by_map: dict[str, str] = {}
|
created_by_map: dict[str, str] = {}
|
||||||
if created_by_ids:
|
if created_by_ids:
|
||||||
uuids = [UUID(uid) for uid in created_by_ids]
|
uuids = [UUID(uid) for uid in created_by_ids]
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@ from app.db import (
|
||||||
)
|
)
|
||||||
from app.schemas import (
|
from app.schemas import (
|
||||||
DocumentRead,
|
DocumentRead,
|
||||||
|
DocumentsCreate,
|
||||||
DocumentStatusBatchResponse,
|
DocumentStatusBatchResponse,
|
||||||
DocumentStatusItemRead,
|
DocumentStatusItemRead,
|
||||||
DocumentsCreate,
|
|
||||||
DocumentStatusSchema,
|
DocumentStatusSchema,
|
||||||
DocumentTitleRead,
|
DocumentTitleRead,
|
||||||
DocumentTitleSearchResponse,
|
DocumentTitleSearchResponse,
|
||||||
|
|
|
||||||
|
|
@ -1326,4 +1326,3 @@ async def regenerate_response(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail=f"An unexpected error occurred during regeneration: {e!s}",
|
detail=f"An unexpected error occurred during regeneration: {e!s}",
|
||||||
) from None
|
) from None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ from .chunks import ChunkBase, ChunkCreate, ChunkRead, ChunkUpdate
|
||||||
from .documents import (
|
from .documents import (
|
||||||
DocumentBase,
|
DocumentBase,
|
||||||
DocumentRead,
|
DocumentRead,
|
||||||
|
DocumentsCreate,
|
||||||
DocumentStatusBatchResponse,
|
DocumentStatusBatchResponse,
|
||||||
DocumentStatusItemRead,
|
DocumentStatusItemRead,
|
||||||
DocumentsCreate,
|
|
||||||
DocumentStatusSchema,
|
DocumentStatusSchema,
|
||||||
DocumentTitleRead,
|
DocumentTitleRead,
|
||||||
DocumentTitleSearchResponse,
|
DocumentTitleSearchResponse,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ from app.services.connector_service import ConnectorService
|
||||||
from app.services.new_streaming_service import VercelStreamingService
|
from app.services.new_streaming_service import VercelStreamingService
|
||||||
from app.utils.content_utils import bootstrap_history_from_db
|
from app.utils.content_utils import bootstrap_history_from_db
|
||||||
|
|
||||||
|
|
||||||
def format_mentioned_documents_as_context(documents: list[Document]) -> str:
|
def format_mentioned_documents_as_context(documents: list[Document]) -> str:
|
||||||
"""
|
"""
|
||||||
Format mentioned documents as context for the agent.
|
Format mentioned documents as context for the agent.
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,16 @@ Message content in new_chat_messages can be stored in various formats:
|
||||||
These utilities help extract and transform content for different use cases.
|
These utilities help extract and transform content for different use cases.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from langchain_core.messages import AIMessage, HumanMessage
|
from langchain_core.messages import AIMessage, HumanMessage
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.db import ChatVisibility
|
||||||
|
|
||||||
|
|
||||||
def extract_text_content(content: str | dict | list) -> str:
|
def extract_text_content(content: str | dict | list) -> str:
|
||||||
"""Extract plain text content from various message formats."""
|
"""Extract plain text content from various message formats."""
|
||||||
|
|
@ -39,7 +44,7 @@ def extract_text_content(content: str | dict | list) -> str:
|
||||||
async def bootstrap_history_from_db(
|
async def bootstrap_history_from_db(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
thread_id: int,
|
thread_id: int,
|
||||||
thread_visibility: "ChatVisibility | None" = None,
|
thread_visibility: ChatVisibility | None = None,
|
||||||
) -> list[HumanMessage | AIMessage]:
|
) -> list[HumanMessage | AIMessage]:
|
||||||
"""
|
"""
|
||||||
Load message history from database and convert to LangChain format.
|
Load message history from database and convert to LangChain format.
|
||||||
|
|
@ -80,8 +85,8 @@ async def bootstrap_history_from_db(
|
||||||
if msg.role == "user":
|
if msg.role == "user":
|
||||||
if is_shared:
|
if is_shared:
|
||||||
author_name = (
|
author_name = (
|
||||||
(msg.author.display_name if msg.author else None) or "A team member"
|
msg.author.display_name if msg.author else None
|
||||||
)
|
) or "A team member"
|
||||||
text_content = f"**[{author_name}]:** {text_content}"
|
text_content = f"**[{author_name}]:** {text_content}"
|
||||||
langchain_messages.append(HumanMessage(content=text_content))
|
langchain_messages.append(HumanMessage(content=text_content))
|
||||||
elif msg.role == "assistant":
|
elif msg.role == "assistant":
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useRef, useState, useEffect } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||||
|
|
||||||
export function getDocumentTypeIcon(type: string, className?: string): React.ReactNode {
|
export function getDocumentTypeIcon(type: string, className?: string): React.ReactNode {
|
||||||
return getConnectorIcon(type, className);
|
return getConnectorIcon(type, className);
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import React, { useRef, useState, useEffect, useCallback } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
|
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
|
||||||
import { JsonMetadataViewer } from "@/components/json-metadata-viewer";
|
import { JsonMetadataViewer } from "@/components/json-metadata-viewer";
|
||||||
import { MarkdownViewer } from "@/components/markdown-viewer";
|
import { MarkdownViewer } from "@/components/markdown-viewer";
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -20,6 +19,7 @@ import {
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import type { IncentiveTaskInfo } from "@/contracts/types/incentive-tasks.types";
|
import type { IncentiveTaskInfo } from "@/contracts/types/incentive-tasks.types";
|
||||||
import { incentiveTasksApiService } from "@/lib/apis/incentive-tasks-api.service";
|
import { incentiveTasksApiService } from "@/lib/apis/incentive-tasks-api.service";
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -15,6 +14,7 @@ import {
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { logout } from "@/lib/auth-utils";
|
import { logout } from "@/lib/auth-utils";
|
||||||
import { cleanupElectric } from "@/lib/electric/client";
|
import { cleanupElectric } from "@/lib/electric/client";
|
||||||
import { resetUser, trackLogout } from "@/lib/posthog/events";
|
import { resetUser, trackLogout } from "@/lib/posthog/events";
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,9 @@ const Composer: FC = () => {
|
||||||
const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom);
|
const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom);
|
||||||
const [showDocumentPopover, setShowDocumentPopover] = useState(false);
|
const [showDocumentPopover, setShowDocumentPopover] = useState(false);
|
||||||
const [mentionQuery, setMentionQuery] = useState("");
|
const [mentionQuery, setMentionQuery] = useState("");
|
||||||
const [uploadedMentionDocs, setUploadedMentionDocs] = useState<Record<number, UploadedMentionDoc>>({});
|
const [uploadedMentionDocs, setUploadedMentionDocs] = useState<
|
||||||
|
Record<number, UploadedMentionDoc>
|
||||||
|
>({});
|
||||||
const [isUploadingDocs, setIsUploadingDocs] = useState(false);
|
const [isUploadingDocs, setIsUploadingDocs] = useState(false);
|
||||||
const editorRef = useRef<InlineMentionEditorRef>(null);
|
const editorRef = useRef<InlineMentionEditorRef>(null);
|
||||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -755,11 +757,7 @@ const ComposerAction: FC<ComposerActionProps> = ({
|
||||||
|
|
||||||
{blockingUploadedMentionsCount > 0 && (
|
{blockingUploadedMentionsCount > 0 && (
|
||||||
<div className="flex items-center gap-1.5 text-muted-foreground text-xs">
|
<div className="flex items-center gap-1.5 text-muted-foreground text-xs">
|
||||||
{hasFailedUploadedMentions ? (
|
{hasFailedUploadedMentions ? <FileWarning className="size-3" /> : <Spinner size="xs" />}
|
||||||
<FileWarning className="size-3" />
|
|
||||||
) : (
|
|
||||||
<Spinner size="xs" />
|
|
||||||
)}
|
|
||||||
<span>
|
<span>
|
||||||
{hasFailedUploadedMentions
|
{hasFailedUploadedMentions
|
||||||
? "Remove or retry failed uploads"
|
? "Remove or retry failed uploads"
|
||||||
|
|
@ -788,11 +786,11 @@ const ComposerAction: FC<ComposerActionProps> = ({
|
||||||
? "Waiting for uploaded files to finish indexing"
|
? "Waiting for uploaded files to finish indexing"
|
||||||
: isUploadingDocs
|
: isUploadingDocs
|
||||||
? "Uploading documents..."
|
? "Uploading documents..."
|
||||||
: !hasModelConfigured
|
: !hasModelConfigured
|
||||||
? "Please select a model from the header to start chatting"
|
? "Please select a model from the header to start chatting"
|
||||||
: isComposerEmpty
|
: isComposerEmpty
|
||||||
? "Enter a message to send"
|
? "Enter a message to send"
|
||||||
: "Send message"
|
: "Send message"
|
||||||
}
|
}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
||||||
|
|
@ -86,19 +86,19 @@ export function HeroSection() {
|
||||||
|
|
||||||
<h2 className="relative z-50 mx-auto mb-4 mt-4 max-w-4xl text-balance text-center text-3xl font-semibold tracking-tight text-gray-700 md:text-7xl dark:text-neutral-300">
|
<h2 className="relative z-50 mx-auto mb-4 mt-4 max-w-4xl text-balance text-center text-3xl font-semibold tracking-tight text-gray-700 md:text-7xl dark:text-neutral-300">
|
||||||
<Balancer>
|
<Balancer>
|
||||||
{isNotebookLMVariant ? (
|
{isNotebookLMVariant ? (
|
||||||
<div className="relative mx-auto inline-block w-max filter-[drop-shadow(0px_1px_3px_rgba(27,37,80,0.14))]">
|
<div className="relative mx-auto inline-block w-max filter-[drop-shadow(0px_1px_3px_rgba(27,37,80,0.14))]">
|
||||||
<div className="text-black [text-shadow:0_0_rgba(0,0,0,0.1)] dark:text-white">
|
<div className="text-black [text-shadow:0_0_rgba(0,0,0,0.1)] dark:text-white">
|
||||||
<span className="">NotebookLM with Superpowers</span>
|
<span className="">NotebookLM with Superpowers</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<div className="relative mx-auto inline-block w-max filter-[drop-shadow(0px_1px_3px_rgba(27,37,80,0.14))]">
|
||||||
<div className="relative mx-auto inline-block w-max filter-[drop-shadow(0px_1px_3px_rgba(27,37,80,0.14))]">
|
<div className="text-black [text-shadow:0_0_rgba(0,0,0,0.1)] dark:text-white">
|
||||||
<div className="text-black [text-shadow:0_0_rgba(0,0,0,0.1)] dark:text-white">
|
<span className="">NotebookLM for Teams</span>
|
||||||
<span className="">NotebookLM for Teams</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</Balancer>
|
</Balancer>
|
||||||
</h2>
|
</h2>
|
||||||
{/* // TODO:aCTUAL DESCRITION */}
|
{/* // TODO:aCTUAL DESCRITION */}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { FileJson } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { defaultStyles, JsonView } from "react-json-view-lite";
|
import { defaultStyles, JsonView } from "react-json-view-lite";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -10,6 +9,7 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import "react-json-view-lite/dist/index.css";
|
import "react-json-view-lite/dist/index.css";
|
||||||
|
|
||||||
interface JsonMetadataViewerProps {
|
interface JsonMetadataViewerProps {
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,15 @@ import { Input } from "@/components/ui/input";
|
||||||
import { isPageLimitExceededMetadata } from "@/contracts/types/inbox.types";
|
import { isPageLimitExceededMetadata } from "@/contracts/types/inbox.types";
|
||||||
import { useInbox } from "@/hooks/use-inbox";
|
import { useInbox } from "@/hooks/use-inbox";
|
||||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||||
import { deleteThread, fetchThreads, updateThread } from "@/lib/chat/thread-persistence";
|
|
||||||
import { logout } from "@/lib/auth-utils";
|
import { logout } from "@/lib/auth-utils";
|
||||||
|
import { deleteThread, fetchThreads, updateThread } from "@/lib/chat/thread-persistence";
|
||||||
import { cleanupElectric } from "@/lib/electric/client";
|
import { cleanupElectric } from "@/lib/electric/client";
|
||||||
import { resetUser, trackLogout } from "@/lib/posthog/events";
|
import { resetUser, trackLogout } from "@/lib/posthog/events";
|
||||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||||
import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types";
|
import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types";
|
||||||
import { CreateSearchSpaceDialog } from "../ui/dialogs";
|
import { CreateSearchSpaceDialog } from "../ui/dialogs";
|
||||||
import { LayoutShell } from "../ui/shell";
|
import { LayoutShell } from "../ui/shell";
|
||||||
|
|
||||||
interface LayoutDataProviderProps {
|
interface LayoutDataProviderProps {
|
||||||
searchSpaceId: string;
|
searchSpaceId: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Settings, Trash2, Users } from "lucide-react";
|
import { Settings, Trash2, Users } from "lucide-react";
|
||||||
import { useCallback, useRef, useState } from "react";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,16 @@ interface LayoutShellProps {
|
||||||
inbox?: InboxProps;
|
inbox?: InboxProps;
|
||||||
isLoadingChats?: boolean;
|
isLoadingChats?: boolean;
|
||||||
// All chats panel props
|
// All chats panel props
|
||||||
allSharedChatsPanel?: { open: boolean; onOpenChange: (open: boolean) => void; searchSpaceId: string };
|
allSharedChatsPanel?: {
|
||||||
allPrivateChatsPanel?: { open: boolean; onOpenChange: (open: boolean) => void; searchSpaceId: string };
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
searchSpaceId: string;
|
||||||
|
};
|
||||||
|
allPrivateChatsPanel?: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
searchSpaceId: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LayoutShell({
|
export function LayoutShell({
|
||||||
|
|
@ -129,7 +137,11 @@ export function LayoutShell({
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
const { isCollapsed, setIsCollapsed, toggleCollapsed } = useSidebarState(defaultCollapsed);
|
const { isCollapsed, setIsCollapsed, toggleCollapsed } = useSidebarState(defaultCollapsed);
|
||||||
const { sidebarWidth, handleMouseDown: onResizeMouseDown, isDragging: isResizing } = useSidebarResize();
|
const {
|
||||||
|
sidebarWidth,
|
||||||
|
handleMouseDown: onResizeMouseDown,
|
||||||
|
isDragging: isResizing,
|
||||||
|
} = useSidebarResize();
|
||||||
|
|
||||||
// Memoize context value to prevent unnecessary re-renders
|
// Memoize context value to prevent unnecessary re-renders
|
||||||
const sidebarContextValue = useMemo(
|
const sidebarContextValue = useMemo(
|
||||||
|
|
|
||||||
|
|
@ -204,217 +204,214 @@ export function AllPrivateChatsSidebar({
|
||||||
ariaLabel={t("chats") || "Private Chats"}
|
ariaLabel={t("chats") || "Private Chats"}
|
||||||
>
|
>
|
||||||
<div className="shrink-0 p-4 pb-2 space-y-3">
|
<div className="shrink-0 p-4 pb-2 space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<User className="h-5 w-5 text-primary" />
|
<User className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">{t("chats") || "Private Chats"}</h2>
|
<h2 className="text-lg font-semibold">{t("chats") || "Private Chats"}</h2>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder={t("search_chats") || "Search chats..."}
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="pl-9 pr-8 h-9"
|
|
||||||
/>
|
|
||||||
{searchQuery && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
|
|
||||||
onClick={handleClearSearch}
|
|
||||||
>
|
|
||||||
<X className="h-3.5 w-3.5" />
|
|
||||||
<span className="sr-only">{t("clear_search") || "Clear search"}</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isSearchMode && (
|
|
||||||
<Tabs
|
|
||||||
value={showArchived ? "archived" : "active"}
|
|
||||||
onValueChange={(value) => setShowArchived(value === "archived")}
|
|
||||||
className="shrink-0 mx-4"
|
|
||||||
>
|
|
||||||
<TabsList className="w-full h-auto p-0 bg-transparent rounded-none border-b">
|
|
||||||
<TabsTrigger
|
|
||||||
value="active"
|
|
||||||
className="flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
|
||||||
>
|
|
||||||
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors">
|
|
||||||
<MessageCircleMore className="h-4 w-4" />
|
|
||||||
<span>Active</span>
|
|
||||||
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
|
||||||
{activeCount}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="archived"
|
|
||||||
className="flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
|
||||||
>
|
|
||||||
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors">
|
|
||||||
<ArchiveIcon className="h-4 w-4" />
|
|
||||||
<span>Archived</span>
|
|
||||||
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
|
||||||
{archivedCount}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="space-y-1">
|
|
||||||
{[75, 90, 55, 80, 65, 85].map((titleWidth, i) => (
|
|
||||||
<div
|
|
||||||
key={`skeleton-${i}`}
|
|
||||||
className="flex items-center gap-2 rounded-md px-2 py-1.5"
|
|
||||||
>
|
|
||||||
<Skeleton className="h-4 w-4 shrink-0 rounded" />
|
|
||||||
<Skeleton className="h-4 rounded" style={{ width: `${titleWidth}%` }} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className="text-center py-8 text-sm text-destructive">
|
|
||||||
{t("error_loading_chats") || "Error loading chats"}
|
|
||||||
</div>
|
|
||||||
) : threads.length > 0 ? (
|
|
||||||
<div className="space-y-1">
|
|
||||||
{threads.map((thread) => {
|
|
||||||
const isDeleting = deletingThreadId === thread.id;
|
|
||||||
const isArchiving = archivingThreadId === thread.id;
|
|
||||||
const isBusy = isDeleting || isArchiving;
|
|
||||||
const isActive = currentChatId === thread.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={thread.id}
|
|
||||||
className={cn(
|
|
||||||
"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
|
||||||
"hover:bg-accent hover:text-accent-foreground",
|
|
||||||
"transition-colors cursor-pointer",
|
|
||||||
isActive && "bg-accent text-accent-foreground",
|
|
||||||
isBusy && "opacity-50 pointer-events-none"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isMobile ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleThreadClick(thread.id)}
|
|
||||||
disabled={isBusy}
|
|
||||||
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
|
|
||||||
>
|
|
||||||
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
||||||
<span className="truncate">{thread.title || "New Chat"}</span>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleThreadClick(thread.id)}
|
|
||||||
disabled={isBusy}
|
|
||||||
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
|
|
||||||
>
|
|
||||||
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
||||||
<span className="truncate">{thread.title || "New Chat"}</span>
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" align="start">
|
|
||||||
<p>
|
|
||||||
{t("updated") || "Updated"}:{" "}
|
|
||||||
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DropdownMenu
|
|
||||||
open={openDropdownId === thread.id}
|
|
||||||
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
|
|
||||||
>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={cn(
|
|
||||||
"h-6 w-6 shrink-0",
|
|
||||||
"md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
|
|
||||||
"transition-opacity"
|
|
||||||
)}
|
|
||||||
disabled={isBusy}
|
|
||||||
>
|
|
||||||
{isDeleting ? (
|
|
||||||
<Spinner size="xs" />
|
|
||||||
) : (
|
|
||||||
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
<span className="sr-only">{t("more_options") || "More options"}</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
|
||||||
disabled={isArchiving}
|
|
||||||
>
|
|
||||||
{thread.archived ? (
|
|
||||||
<>
|
|
||||||
<RotateCcwIcon className="mr-2 h-4 w-4" />
|
|
||||||
<span>{t("unarchive") || "Restore"}</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArchiveIcon className="mr-2 h-4 w-4" />
|
|
||||||
<span>{t("archive") || "Archive"}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => handleDeleteThread(thread.id)}
|
|
||||||
className="text-destructive focus:text-destructive"
|
|
||||||
>
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
|
||||||
<span>{t("delete") || "Delete"}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : isSearchMode ? (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<Search className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t("no_chats_found") || "No chats found"}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
|
||||||
{t("try_different_search") || "Try a different search term"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<User className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{showArchived
|
|
||||||
? t("no_archived_chats") || "No archived chats"
|
|
||||||
: t("no_chats") || "No private chats"}
|
|
||||||
</p>
|
|
||||||
{!showArchived && (
|
|
||||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
|
||||||
{t("start_new_chat_hint") || "Start a new chat from the chat page"}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t("search_chats") || "Search chats..."}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-9 pr-8 h-9"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
|
||||||
|
onClick={handleClearSearch}
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
<span className="sr-only">{t("clear_search") || "Clear search"}</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isSearchMode && (
|
||||||
|
<Tabs
|
||||||
|
value={showArchived ? "archived" : "active"}
|
||||||
|
onValueChange={(value) => setShowArchived(value === "archived")}
|
||||||
|
className="shrink-0 mx-4"
|
||||||
|
>
|
||||||
|
<TabsList className="w-full h-auto p-0 bg-transparent rounded-none border-b">
|
||||||
|
<TabsTrigger
|
||||||
|
value="active"
|
||||||
|
className="flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||||||
|
>
|
||||||
|
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors">
|
||||||
|
<MessageCircleMore className="h-4 w-4" />
|
||||||
|
<span>Active</span>
|
||||||
|
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
||||||
|
{activeCount}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="archived"
|
||||||
|
className="flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||||||
|
>
|
||||||
|
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors">
|
||||||
|
<ArchiveIcon className="h-4 w-4" />
|
||||||
|
<span>Archived</span>
|
||||||
|
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
||||||
|
{archivedCount}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{[75, 90, 55, 80, 65, 85].map((titleWidth, i) => (
|
||||||
|
<div key={`skeleton-${i}`} className="flex items-center gap-2 rounded-md px-2 py-1.5">
|
||||||
|
<Skeleton className="h-4 w-4 shrink-0 rounded" />
|
||||||
|
<Skeleton className="h-4 rounded" style={{ width: `${titleWidth}%` }} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="text-center py-8 text-sm text-destructive">
|
||||||
|
{t("error_loading_chats") || "Error loading chats"}
|
||||||
|
</div>
|
||||||
|
) : threads.length > 0 ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{threads.map((thread) => {
|
||||||
|
const isDeleting = deletingThreadId === thread.id;
|
||||||
|
const isArchiving = archivingThreadId === thread.id;
|
||||||
|
const isBusy = isDeleting || isArchiving;
|
||||||
|
const isActive = currentChatId === thread.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={thread.id}
|
||||||
|
className={cn(
|
||||||
|
"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
||||||
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
|
"transition-colors cursor-pointer",
|
||||||
|
isActive && "bg-accent text-accent-foreground",
|
||||||
|
isBusy && "opacity-50 pointer-events-none"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isMobile ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleThreadClick(thread.id)}
|
||||||
|
disabled={isBusy}
|
||||||
|
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
|
||||||
|
>
|
||||||
|
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleThreadClick(thread.id)}
|
||||||
|
disabled={isBusy}
|
||||||
|
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
|
||||||
|
>
|
||||||
|
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" align="start">
|
||||||
|
<p>
|
||||||
|
{t("updated") || "Updated"}:{" "}
|
||||||
|
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DropdownMenu
|
||||||
|
open={openDropdownId === thread.id}
|
||||||
|
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
|
||||||
|
>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
"h-6 w-6 shrink-0",
|
||||||
|
"md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
|
||||||
|
"transition-opacity"
|
||||||
|
)}
|
||||||
|
disabled={isBusy}
|
||||||
|
>
|
||||||
|
{isDeleting ? (
|
||||||
|
<Spinner size="xs" />
|
||||||
|
) : (
|
||||||
|
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<span className="sr-only">{t("more_options") || "More options"}</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-40 z-80">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||||
|
disabled={isArchiving}
|
||||||
|
>
|
||||||
|
{thread.archived ? (
|
||||||
|
<>
|
||||||
|
<RotateCcwIcon className="mr-2 h-4 w-4" />
|
||||||
|
<span>{t("unarchive") || "Restore"}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArchiveIcon className="mr-2 h-4 w-4" />
|
||||||
|
<span>{t("archive") || "Archive"}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDeleteThread(thread.id)}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
<span>{t("delete") || "Delete"}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : isSearchMode ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Search className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("no_chats_found") || "No chats found"}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||||
|
{t("try_different_search") || "Try a different search term"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<User className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{showArchived
|
||||||
|
? t("no_archived_chats") || "No archived chats"
|
||||||
|
: t("no_chats") || "No private chats"}
|
||||||
|
</p>
|
||||||
|
{!showArchived && (
|
||||||
|
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||||
|
{t("start_new_chat_hint") || "Start a new chat from the chat page"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SidebarSlideOutPanel>
|
</SidebarSlideOutPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -204,217 +204,214 @@ export function AllSharedChatsSidebar({
|
||||||
ariaLabel={t("shared_chats") || "Shared Chats"}
|
ariaLabel={t("shared_chats") || "Shared Chats"}
|
||||||
>
|
>
|
||||||
<div className="shrink-0 p-4 pb-2 space-y-3">
|
<div className="shrink-0 p-4 pb-2 space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Users className="h-5 w-5 text-primary" />
|
<Users className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">{t("shared_chats") || "Shared Chats"}</h2>
|
<h2 className="text-lg font-semibold">{t("shared_chats") || "Shared Chats"}</h2>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder={t("search_chats") || "Search chats..."}
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="pl-9 pr-8 h-9"
|
|
||||||
/>
|
|
||||||
{searchQuery && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
|
|
||||||
onClick={handleClearSearch}
|
|
||||||
>
|
|
||||||
<X className="h-3.5 w-3.5" />
|
|
||||||
<span className="sr-only">{t("clear_search") || "Clear search"}</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isSearchMode && (
|
|
||||||
<Tabs
|
|
||||||
value={showArchived ? "archived" : "active"}
|
|
||||||
onValueChange={(value) => setShowArchived(value === "archived")}
|
|
||||||
className="shrink-0 mx-4"
|
|
||||||
>
|
|
||||||
<TabsList className="w-full h-auto p-0 bg-transparent rounded-none border-b">
|
|
||||||
<TabsTrigger
|
|
||||||
value="active"
|
|
||||||
className="flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
|
||||||
>
|
|
||||||
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors">
|
|
||||||
<MessageCircleMore className="h-4 w-4" />
|
|
||||||
<span>Active</span>
|
|
||||||
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
|
||||||
{activeCount}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="archived"
|
|
||||||
className="flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
|
||||||
>
|
|
||||||
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors">
|
|
||||||
<ArchiveIcon className="h-4 w-4" />
|
|
||||||
<span>Archived</span>
|
|
||||||
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
|
||||||
{archivedCount}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="space-y-1">
|
|
||||||
{[75, 90, 55, 80, 65, 85].map((titleWidth, i) => (
|
|
||||||
<div
|
|
||||||
key={`skeleton-${i}`}
|
|
||||||
className="flex items-center gap-2 rounded-md px-2 py-1.5"
|
|
||||||
>
|
|
||||||
<Skeleton className="h-4 w-4 shrink-0 rounded" />
|
|
||||||
<Skeleton className="h-4 rounded" style={{ width: `${titleWidth}%` }} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className="text-center py-8 text-sm text-destructive">
|
|
||||||
{t("error_loading_chats") || "Error loading chats"}
|
|
||||||
</div>
|
|
||||||
) : threads.length > 0 ? (
|
|
||||||
<div className="space-y-1">
|
|
||||||
{threads.map((thread) => {
|
|
||||||
const isDeleting = deletingThreadId === thread.id;
|
|
||||||
const isArchiving = archivingThreadId === thread.id;
|
|
||||||
const isBusy = isDeleting || isArchiving;
|
|
||||||
const isActive = currentChatId === thread.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={thread.id}
|
|
||||||
className={cn(
|
|
||||||
"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
|
||||||
"hover:bg-accent hover:text-accent-foreground",
|
|
||||||
"transition-colors cursor-pointer",
|
|
||||||
isActive && "bg-accent text-accent-foreground",
|
|
||||||
isBusy && "opacity-50 pointer-events-none"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isMobile ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleThreadClick(thread.id)}
|
|
||||||
disabled={isBusy}
|
|
||||||
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
|
|
||||||
>
|
|
||||||
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
||||||
<span className="truncate">{thread.title || "New Chat"}</span>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleThreadClick(thread.id)}
|
|
||||||
disabled={isBusy}
|
|
||||||
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
|
|
||||||
>
|
|
||||||
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
||||||
<span className="truncate">{thread.title || "New Chat"}</span>
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" align="start">
|
|
||||||
<p>
|
|
||||||
{t("updated") || "Updated"}:{" "}
|
|
||||||
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DropdownMenu
|
|
||||||
open={openDropdownId === thread.id}
|
|
||||||
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
|
|
||||||
>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={cn(
|
|
||||||
"h-6 w-6 shrink-0",
|
|
||||||
"md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
|
|
||||||
"transition-opacity"
|
|
||||||
)}
|
|
||||||
disabled={isBusy}
|
|
||||||
>
|
|
||||||
{isDeleting ? (
|
|
||||||
<Spinner size="xs" />
|
|
||||||
) : (
|
|
||||||
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
<span className="sr-only">{t("more_options") || "More options"}</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
|
||||||
disabled={isArchiving}
|
|
||||||
>
|
|
||||||
{thread.archived ? (
|
|
||||||
<>
|
|
||||||
<RotateCcwIcon className="mr-2 h-4 w-4" />
|
|
||||||
<span>{t("unarchive") || "Restore"}</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArchiveIcon className="mr-2 h-4 w-4" />
|
|
||||||
<span>{t("archive") || "Archive"}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => handleDeleteThread(thread.id)}
|
|
||||||
className="text-destructive focus:text-destructive"
|
|
||||||
>
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
|
||||||
<span>{t("delete") || "Delete"}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : isSearchMode ? (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<Search className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t("no_chats_found") || "No chats found"}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
|
||||||
{t("try_different_search") || "Try a different search term"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<Users className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{showArchived
|
|
||||||
? t("no_archived_chats") || "No archived chats"
|
|
||||||
: t("no_shared_chats") || "No shared chats"}
|
|
||||||
</p>
|
|
||||||
{!showArchived && (
|
|
||||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
|
||||||
Share a chat to collaborate with your team
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t("search_chats") || "Search chats..."}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-9 pr-8 h-9"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
|
||||||
|
onClick={handleClearSearch}
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
<span className="sr-only">{t("clear_search") || "Clear search"}</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isSearchMode && (
|
||||||
|
<Tabs
|
||||||
|
value={showArchived ? "archived" : "active"}
|
||||||
|
onValueChange={(value) => setShowArchived(value === "archived")}
|
||||||
|
className="shrink-0 mx-4"
|
||||||
|
>
|
||||||
|
<TabsList className="w-full h-auto p-0 bg-transparent rounded-none border-b">
|
||||||
|
<TabsTrigger
|
||||||
|
value="active"
|
||||||
|
className="flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||||||
|
>
|
||||||
|
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors">
|
||||||
|
<MessageCircleMore className="h-4 w-4" />
|
||||||
|
<span>Active</span>
|
||||||
|
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
||||||
|
{activeCount}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="archived"
|
||||||
|
className="flex-1 rounded-none border-b-2 border-transparent px-1 py-2 text-xs font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||||||
|
>
|
||||||
|
<span className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors">
|
||||||
|
<ArchiveIcon className="h-4 w-4" />
|
||||||
|
<span>Archived</span>
|
||||||
|
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
||||||
|
{archivedCount}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{[75, 90, 55, 80, 65, 85].map((titleWidth, i) => (
|
||||||
|
<div key={`skeleton-${i}`} className="flex items-center gap-2 rounded-md px-2 py-1.5">
|
||||||
|
<Skeleton className="h-4 w-4 shrink-0 rounded" />
|
||||||
|
<Skeleton className="h-4 rounded" style={{ width: `${titleWidth}%` }} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="text-center py-8 text-sm text-destructive">
|
||||||
|
{t("error_loading_chats") || "Error loading chats"}
|
||||||
|
</div>
|
||||||
|
) : threads.length > 0 ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{threads.map((thread) => {
|
||||||
|
const isDeleting = deletingThreadId === thread.id;
|
||||||
|
const isArchiving = archivingThreadId === thread.id;
|
||||||
|
const isBusy = isDeleting || isArchiving;
|
||||||
|
const isActive = currentChatId === thread.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={thread.id}
|
||||||
|
className={cn(
|
||||||
|
"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
||||||
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
|
"transition-colors cursor-pointer",
|
||||||
|
isActive && "bg-accent text-accent-foreground",
|
||||||
|
isBusy && "opacity-50 pointer-events-none"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isMobile ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleThreadClick(thread.id)}
|
||||||
|
disabled={isBusy}
|
||||||
|
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
|
||||||
|
>
|
||||||
|
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleThreadClick(thread.id)}
|
||||||
|
disabled={isBusy}
|
||||||
|
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
|
||||||
|
>
|
||||||
|
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" align="start">
|
||||||
|
<p>
|
||||||
|
{t("updated") || "Updated"}:{" "}
|
||||||
|
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DropdownMenu
|
||||||
|
open={openDropdownId === thread.id}
|
||||||
|
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
|
||||||
|
>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
"h-6 w-6 shrink-0",
|
||||||
|
"md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
|
||||||
|
"transition-opacity"
|
||||||
|
)}
|
||||||
|
disabled={isBusy}
|
||||||
|
>
|
||||||
|
{isDeleting ? (
|
||||||
|
<Spinner size="xs" />
|
||||||
|
) : (
|
||||||
|
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<span className="sr-only">{t("more_options") || "More options"}</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-40 z-80">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||||
|
disabled={isArchiving}
|
||||||
|
>
|
||||||
|
{thread.archived ? (
|
||||||
|
<>
|
||||||
|
<RotateCcwIcon className="mr-2 h-4 w-4" />
|
||||||
|
<span>{t("unarchive") || "Restore"}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArchiveIcon className="mr-2 h-4 w-4" />
|
||||||
|
<span>{t("archive") || "Archive"}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDeleteThread(thread.id)}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
<span>{t("delete") || "Delete"}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : isSearchMode ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Search className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("no_chats_found") || "No chats found"}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||||
|
{t("try_different_search") || "Try a different search term"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Users className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{showArchived
|
||||||
|
? t("no_archived_chats") || "No archived chats"
|
||||||
|
: t("no_shared_chats") || "No shared chats"}
|
||||||
|
</p>
|
||||||
|
{!showArchived && (
|
||||||
|
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||||
|
Share a chat to collaborate with your team
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SidebarSlideOutPanel>
|
</SidebarSlideOutPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ import {
|
||||||
isNewMentionMetadata,
|
isNewMentionMetadata,
|
||||||
isPageLimitExceededMetadata,
|
isPageLimitExceededMetadata,
|
||||||
} from "@/contracts/types/inbox.types";
|
} from "@/contracts/types/inbox.types";
|
||||||
import type { InboxItem } from "@/hooks/use-inbox";
|
|
||||||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||||
|
import type { InboxItem } from "@/hooks/use-inbox";
|
||||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||||
import { notificationsApiService } from "@/lib/apis/notifications-api.service";
|
import { notificationsApiService } from "@/lib/apis/notifications-api.service";
|
||||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
|
||||||
import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
|
import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
|
||||||
|
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
||||||
import { ChatListItem } from "./ChatListItem";
|
import { ChatListItem } from "./ChatListItem";
|
||||||
import { NavSection } from "./NavSection";
|
import { NavSection } from "./NavSection";
|
||||||
import { PageUsageDisplay } from "./PageUsageDisplay";
|
import { PageUsageDisplay } from "./PageUsageDisplay";
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { useSidebarContextSafe } from "../../hooks";
|
import { useSidebarContextSafe } from "../../hooks";
|
||||||
|
|
||||||
const SIDEBAR_COLLAPSED_WIDTH = 60;
|
const SIDEBAR_COLLAPSED_WIDTH = 60;
|
||||||
|
|
@ -40,8 +40,8 @@ export function SidebarSlideOutPanel({
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{open && (
|
{open && (
|
||||||
<>
|
<>
|
||||||
{/* Click-away layer - covers the full container including the sidebar */}
|
{/* Click-away layer - covers the full container including the sidebar */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ import {
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { useLocaleContext } from "@/contexts/LocaleContext";
|
import { useLocaleContext } from "@/contexts/LocaleContext";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { User } from "../../types/layout.types";
|
import type { User } from "../../types/layout.types";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
import { AssistantRuntimeProvider } from "@assistant-ui/react";
|
import { AssistantRuntimeProvider } from "@assistant-ui/react";
|
||||||
import { Navbar } from "@/components/homepage/navbar";
|
import { Navbar } from "@/components/homepage/navbar";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
|
||||||
import { DisplayImageToolUI } from "@/components/tool-ui/display-image";
|
import { DisplayImageToolUI } from "@/components/tool-ui/display-image";
|
||||||
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
||||||
import { LinkPreviewToolUI } from "@/components/tool-ui/link-preview";
|
import { LinkPreviewToolUI } from "@/components/tool-ui/link-preview";
|
||||||
import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage";
|
import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { usePublicChat } from "@/hooks/use-public-chat";
|
import { usePublicChat } from "@/hooks/use-public-chat";
|
||||||
import { usePublicChatRuntime } from "@/hooks/use-public-chat-runtime";
|
import { usePublicChatRuntime } from "@/hooks/use-public-chat-runtime";
|
||||||
import { PublicChatFooter } from "./public-chat-footer";
|
import { PublicChatFooter } from "./public-chat-footer";
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,8 @@ import { Separator } from "@/components/ui/separator";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import {
|
import {
|
||||||
IMAGE_GEN_PROVIDERS,
|
|
||||||
getImageGenModelsByProvider,
|
getImageGenModelsByProvider,
|
||||||
|
IMAGE_GEN_PROVIDERS,
|
||||||
} from "@/contracts/enums/image-gen-providers";
|
} from "@/contracts/enums/image-gen-providers";
|
||||||
import type { ImageGenerationConfig } from "@/contracts/types/new-llm-config.types";
|
import type { ImageGenerationConfig } from "@/contracts/types/new-llm-config.types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ import {
|
||||||
getDocumentRequest,
|
getDocumentRequest,
|
||||||
getDocumentResponse,
|
getDocumentResponse,
|
||||||
getDocumentsRequest,
|
getDocumentsRequest,
|
||||||
|
getDocumentsResponse,
|
||||||
getDocumentsStatusRequest,
|
getDocumentsStatusRequest,
|
||||||
getDocumentsStatusResponse,
|
getDocumentsStatusResponse,
|
||||||
getDocumentsResponse,
|
|
||||||
getDocumentTypeCountsRequest,
|
getDocumentTypeCountsRequest,
|
||||||
getDocumentTypeCountsResponse,
|
getDocumentTypeCountsResponse,
|
||||||
getSurfsenseDocsByChunkResponse,
|
getSurfsenseDocsByChunkResponse,
|
||||||
|
|
@ -151,7 +151,10 @@ class DocumentsApiService {
|
||||||
document_ids: document_ids.join(","),
|
document_ids: document_ids.join(","),
|
||||||
});
|
});
|
||||||
|
|
||||||
return baseApiService.get(`/api/v1/documents/status?${params.toString()}`, getDocumentsStatusResponse);
|
return baseApiService.get(
|
||||||
|
`/api/v1/documents/status?${params.toString()}`,
|
||||||
|
getDocumentsStatusResponse
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@ import {
|
||||||
type CreateImageGenConfigRequest,
|
type CreateImageGenConfigRequest,
|
||||||
createImageGenConfigRequest,
|
createImageGenConfigRequest,
|
||||||
createImageGenConfigResponse,
|
createImageGenConfigResponse,
|
||||||
|
deleteImageGenConfigResponse,
|
||||||
|
getGlobalImageGenConfigsResponse,
|
||||||
|
getImageGenConfigsResponse,
|
||||||
type UpdateImageGenConfigRequest,
|
type UpdateImageGenConfigRequest,
|
||||||
updateImageGenConfigRequest,
|
updateImageGenConfigRequest,
|
||||||
updateImageGenConfigResponse,
|
updateImageGenConfigResponse,
|
||||||
deleteImageGenConfigResponse,
|
|
||||||
getImageGenConfigsResponse,
|
|
||||||
getGlobalImageGenConfigsResponse,
|
|
||||||
} from "@/contracts/types/new-llm-config.types";
|
} from "@/contracts/types/new-llm-config.types";
|
||||||
import { ValidationError } from "../error";
|
import { ValidationError } from "../error";
|
||||||
import { baseApiService } from "./base-api.service";
|
import { baseApiService } from "./base-api.service";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue