mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 01:36:30 +02:00
Merge commit '056fc0e7ff' into dev_mod
This commit is contained in:
commit
82b5c7f19e
111 changed files with 4056 additions and 2219 deletions
|
|
@ -216,7 +216,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
onPointerDownOutside={(e) => {
|
||||
if (pickerOpen) e.preventDefault();
|
||||
}}
|
||||
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 [&>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"
|
||||
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 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 [&>button]:hover:opacity-100 [&>button]:hover:bg-foreground/10 [&>button>svg]:size-5 select-none"
|
||||
>
|
||||
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
|
||||
{/* YouTube Crawler View - shown when adding YouTube videos */}
|
||||
|
|
|
|||
|
|
@ -144,18 +144,14 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
|
|||
type="button"
|
||||
onClick={handleFormSubmit}
|
||||
disabled={isSubmitting}
|
||||
className="text-xs sm:text-sm min-w-[140px] disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none"
|
||||
className="relative text-xs sm:text-sm min-w-[140px] disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
Connecting
|
||||
</>
|
||||
) : connectorType === "MCP_CONNECTOR" ? (
|
||||
"Connect"
|
||||
) : (
|
||||
`Connect ${getConnectorTypeDisplay(connectorType)}`
|
||||
)}
|
||||
<span className={isSubmitting ? "opacity-0" : ""}>
|
||||
{connectorType === "MCP_CONNECTOR"
|
||||
? "Connect"
|
||||
: `Connect ${getConnectorTypeDisplay(connectorType)}`}
|
||||
</span>
|
||||
{isSubmitting && <Spinner size="sm" className="absolute" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -369,16 +369,10 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
size="sm"
|
||||
onClick={handleDisconnectConfirm}
|
||||
disabled={isDisconnecting}
|
||||
className="text-xs sm:text-sm flex-1 sm:flex-initial h-10 sm:h-auto py-2 sm:py-2"
|
||||
className="relative text-xs sm:text-sm flex-1 sm:flex-initial h-10 sm:h-auto py-2 sm:py-2"
|
||||
>
|
||||
{isDisconnecting ? (
|
||||
<>
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
Disconnecting
|
||||
</>
|
||||
) : (
|
||||
"Confirm Disconnect"
|
||||
)}
|
||||
<span className={isDisconnecting ? "opacity-0" : ""}>Confirm Disconnect</span>
|
||||
{isDisconnecting && <Spinner size="sm" className="absolute" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -415,16 +409,10 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
<Button
|
||||
onClick={onSave}
|
||||
disabled={isSaving || isDisconnecting}
|
||||
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2"
|
||||
className="relative text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
Saving
|
||||
</>
|
||||
) : (
|
||||
"Save Changes"
|
||||
)}
|
||||
<span className={isSaving ? "opacity-0" : ""}>Save Changes</span>
|
||||
{isSaving && <Spinner size="sm" className="absolute" />}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Cable } from "lucide-react";
|
||||
import { Search, Unplug } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -134,9 +134,17 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
|
|||
const hasActiveConnectors =
|
||||
filteredOAuthConnectorTypes.length > 0 || filteredNonOAuthConnectors.length > 0;
|
||||
|
||||
const hasFilteredResults = hasActiveConnectors || standaloneDocuments.length > 0;
|
||||
|
||||
return (
|
||||
<TabsContent value="active" className="m-0">
|
||||
{hasSources ? (
|
||||
{hasSources && !hasFilteredResults && searchQuery ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<Search className="size-8 text-muted-foreground mb-3" />
|
||||
<p className="text-sm text-muted-foreground">No connectors found</p>
|
||||
<p className="text-xs text-muted-foreground/60 mt-1">Try a different search term</p>
|
||||
</div>
|
||||
) : hasSources ? (
|
||||
<div className="space-y-6">
|
||||
{/* Active Connectors Section */}
|
||||
{hasActiveConnectors && (
|
||||
|
|
@ -302,7 +310,7 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
|
|||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted mb-4">
|
||||
<Cable className="size-8 text-muted-foreground" />
|
||||
<Unplug className="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h4 className="text-lg font-semibold">No active sources</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1 max-w-[280px]">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Search } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||
|
|
@ -287,6 +288,18 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
moreIntegrationsOther.length > 0 ||
|
||||
moreIntegrationsCrawlers.length > 0;
|
||||
|
||||
const hasAnyResults = hasDocumentFileConnectors || hasMoreIntegrations;
|
||||
|
||||
if (!hasAnyResults && searchQuery) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<Search className="size-8 text-muted-foreground mb-3" />
|
||||
<p className="text-sm text-muted-foreground">No connectors found</p>
|
||||
<p className="text-xs text-muted-foreground/60 mt-1">Try a different search term</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Document/Files Connectors */}
|
||||
|
|
|
|||
|
|
@ -173,9 +173,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
<Plus className="size-3 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs sm:text-sm font-medium">
|
||||
{isConnecting ? "Connecting" : buttonText}
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm font-medium">{buttonText}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -335,16 +335,10 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId,
|
|||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || isFetchingPlaylist || videoTags.length === 0}
|
||||
className="text-xs sm:text-sm min-w-[140px] disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none"
|
||||
className="relative text-xs sm:text-sm min-w-[140px] disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
{t("processing")}
|
||||
</>
|
||||
) : (
|
||||
t("submit")
|
||||
)}
|
||||
<span className={isSubmitting ? "opacity-0" : ""}>{t("submit")}</span>
|
||||
{isSubmitting && <Spinner size="sm" className="absolute" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -125,18 +125,16 @@ const DocumentUploadPopupContent: FC<{
|
|||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
className="select-none max-w-2xl w-[95vw] sm:w-[640px] h-[min(440px,75dvh)] sm:h-[min(500px,80vh)] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 bg-muted dark:bg-muted text-foreground [&>button]:right-3 sm:[&>button]:right-6 [&>button]:top-3 sm:[&>button]:top-5 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button]:z-[100] [&>button_svg]:size-4 sm:[&>button_svg]:size-5"
|
||||
className="select-none max-w-2xl w-[95vw] sm:w-[640px] h-[min(440px,75dvh)] sm:h-[min(520px,80vh)] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 bg-muted dark:bg-muted text-foreground [&>button]:right-3 sm:[&>button]:right-6 [&>button]:top-5 sm:[&>button]:top-8 [&>button]:opacity-80 [&>button]:hover:opacity-100 [&>button]:hover:bg-foreground/10 [&>button]:z-[100] [&>button>svg]:size-4 sm:[&>button>svg]:size-5"
|
||||
>
|
||||
<DialogTitle className="sr-only">Upload Document</DialogTitle>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-y-auto overscroll-contain">
|
||||
<div className="sticky top-0 z-20 bg-muted px-4 sm:px-6 pt-4 sm:pt-5 pb-10">
|
||||
<div className="sticky top-0 z-20 bg-muted px-4 sm:px-6 pt-6 sm:pt-8 pb-10">
|
||||
<div className="flex items-center gap-2 mb-1 pr-8 sm:pr-0">
|
||||
<h2 className="text-base sm:text-lg font-semibold tracking-tight">
|
||||
Upload Documents
|
||||
</h2>
|
||||
<h2 className="text-xl sm:text-3xl font-semibold tracking-tight">Upload Documents</h2>
|
||||
</div>
|
||||
<p className="text-xs sm:text-sm text-muted-foreground line-clamp-1">
|
||||
<p className="text-xs sm:text-base text-muted-foreground/80 line-clamp-1">
|
||||
Upload and sync your documents to your search space
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
import type { ImageMessagePartComponent } from "@assistant-ui/react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { ImageIcon, ImageOffIcon } from "lucide-react";
|
||||
import NextImage from "next/image";
|
||||
import { memo, type PropsWithChildren, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { cn } from "@/lib/utils";
|
||||
import NextImage from 'next/image';
|
||||
|
||||
const imageVariants = cva("aui-image-root relative overflow-hidden rounded-lg", {
|
||||
variants: {
|
||||
|
|
@ -88,23 +88,23 @@ function ImagePreview({
|
|||
<ImageOffIcon className="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
) : isDataOrBlobUrl(src) ? (
|
||||
// biome-ignore lint/performance/noImgElement: data/blob URLs need plain img
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={cn("block h-auto w-full object-contain", !loaded && "invisible", className)}
|
||||
onLoad={(e) => {
|
||||
if (typeof src === "string") setLoadedSrc(src);
|
||||
onLoad?.(e);
|
||||
}}
|
||||
onError={(e) => {
|
||||
if (typeof src === "string") setErrorSrc(src);
|
||||
onError?.(e);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
// biome-ignore lint/performance/noImgElement: data/blob URLs need plain img
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={cn("block h-auto w-full object-contain", !loaded && "invisible", className)}
|
||||
onLoad={(e) => {
|
||||
if (typeof src === "string") setLoadedSrc(src);
|
||||
onLoad?.(e);
|
||||
}}
|
||||
onError={(e) => {
|
||||
if (typeof src === "string") setErrorSrc(src);
|
||||
onError?.(e);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
// biome-ignore lint/performance/noImgElement: intentional for dynamic external URLs
|
||||
// <img
|
||||
// ref={imgRef}
|
||||
|
|
@ -122,22 +122,22 @@ function ImagePreview({
|
|||
// {...props}
|
||||
// />
|
||||
<NextImage
|
||||
fill
|
||||
src={src || ""}
|
||||
alt={alt}
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 60vw"
|
||||
className={cn("block object-contain", !loaded && "invisible", className)}
|
||||
onLoad={() => {
|
||||
if (typeof src === "string") setLoadedSrc(src);
|
||||
onLoad?.();
|
||||
}}
|
||||
onError={() => {
|
||||
if (typeof src === "string") setErrorSrc(src);
|
||||
onError?.();
|
||||
}}
|
||||
unoptimized={false}
|
||||
{...props}
|
||||
/>
|
||||
fill
|
||||
src={src || ""}
|
||||
alt={alt}
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 60vw"
|
||||
className={cn("block object-contain", !loaded && "invisible", className)}
|
||||
onLoad={() => {
|
||||
if (typeof src === "string") setLoadedSrc(src);
|
||||
onLoad?.();
|
||||
}}
|
||||
onError={() => {
|
||||
if (typeof src === "string") setErrorSrc(src);
|
||||
onError?.();
|
||||
}}
|
||||
unoptimized={false}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -162,8 +162,8 @@ type ImageZoomProps = PropsWithChildren<{
|
|||
alt?: string;
|
||||
}>;
|
||||
function isDataOrBlobUrl(src: string | undefined): boolean {
|
||||
if (!src || typeof src !== "string") return false;
|
||||
return src.startsWith("data:") || src.startsWith("blob:");
|
||||
if (!src || typeof src !== "string") return false;
|
||||
return src.startsWith("data:") || src.startsWith("blob:");
|
||||
}
|
||||
function ImageZoom({ src, alt = "Image preview", children }: ImageZoomProps) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
|
@ -216,38 +216,38 @@ function ImageZoom({ src, alt = "Image preview", children }: ImageZoomProps) {
|
|||
>
|
||||
{/** biome-ignore lint/performance/noImgElement: <explanation> */}
|
||||
{isDataOrBlobUrl(src) ? (
|
||||
// biome-ignore lint/performance/noImgElement: data/blob URLs need plain img
|
||||
<img
|
||||
data-slot="image-zoom-content"
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="aui-image-zoom-content fade-in zoom-in-95 max-h-[90vh] max-w-[90vw] animate-in object-contain duration-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
// biome-ignore lint/performance/noImgElement: data/blob URLs need plain img
|
||||
<img
|
||||
data-slot="image-zoom-content"
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="aui-image-zoom-content fade-in zoom-in-95 max-h-[90vh] max-w-[90vw] animate-in object-contain duration-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<NextImage
|
||||
data-slot="image-zoom-content"
|
||||
fill
|
||||
src={src}
|
||||
alt={alt}
|
||||
sizes="90vw"
|
||||
className="aui-image-zoom-content fade-in zoom-in-95 object-contain duration-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}}
|
||||
unoptimized={false}
|
||||
/>
|
||||
)}
|
||||
data-slot="image-zoom-content"
|
||||
fill
|
||||
src={src}
|
||||
alt={alt}
|
||||
sizes="90vw"
|
||||
className="aui-image-zoom-content fade-in zoom-in-95 object-contain duration-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}}
|
||||
unoptimized={false}
|
||||
/>
|
||||
)}
|
||||
</button>,
|
||||
document.body
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -241,9 +241,7 @@ const ThreadListItemComponent = memo(function ThreadListItemComponent({
|
|||
<MessageSquareIcon className="size-4 shrink-0 text-muted-foreground" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="truncate text-sm font-medium">{thread.title || "New Chat"}</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{relativeTime}
|
||||
</p>
|
||||
<p className="truncate text-xs text-muted-foreground">{relativeTime}</p>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ export const ToolFallback: ToolCallMessagePartComponent = ({
|
|||
);
|
||||
|
||||
const serializedResult = useMemo(
|
||||
() => (result !== undefined && typeof result !== "string" ? JSON.stringify(result, null, 2) : null),
|
||||
() =>
|
||||
result !== undefined && typeof result !== "string" ? JSON.stringify(result, null, 2) : null,
|
||||
[result]
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { ArrowUp, Send, X } from "lucide-react";
|
||||
import { ArrowUp } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/popover";
|
||||
|
|
@ -307,7 +307,6 @@ export function CommentComposer({
|
|||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<X className="mr-1 size-4" />
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -318,14 +317,7 @@ export function CommentComposer({
|
|||
disabled={!canSubmit}
|
||||
className={cn(!canSubmit && "opacity-50", compact && "size-8 shrink-0 rounded-full")}
|
||||
>
|
||||
{compact ? (
|
||||
<ArrowUp className="size-4" />
|
||||
) : (
|
||||
<>
|
||||
<Send className="mr-1 size-4" />
|
||||
{submitLabel}
|
||||
</>
|
||||
)}
|
||||
{compact ? <ArrowUp className="size-4" /> : submitLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { MoreHorizontal, Pencil, Trash2 } from "lucide-react";
|
||||
import { MoreHorizontal, PenLine, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -21,15 +21,15 @@ export function CommentActions({ canEdit, canDelete, onEdit, onDelete }: Comment
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity"
|
||||
className="size-7 text-muted-foreground opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<MoreHorizontal className="size-4 text-muted-foreground" />
|
||||
<MoreHorizontal className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{canEdit && (
|
||||
<DropdownMenuItem onClick={onEdit}>
|
||||
<Pencil className="mr-2 size-4" />
|
||||
<PenLine className="mr-2 size-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ export function CommentItem({
|
|||
<CommentComposer
|
||||
members={members}
|
||||
membersLoading={membersLoading}
|
||||
placeholder="Edit your comment..."
|
||||
placeholder="Edit your comment"
|
||||
submitLabel="Save"
|
||||
isSubmitting={isSubmitting}
|
||||
onSubmit={handleEditSubmit}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,9 @@ export const DocumentNode = React.memo(function DocumentNode({
|
|||
const isProcessing = statusState === "pending" || statusState === "processing";
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [exporting, setExporting] = useState<string | null>(null);
|
||||
const [titleTooltipOpen, setTitleTooltipOpen] = useState(false);
|
||||
const rowRef = useRef<HTMLDivElement>(null);
|
||||
const titleRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
const handleExport = useCallback(
|
||||
(format: string) => {
|
||||
|
|
@ -118,6 +120,14 @@ export const DocumentNode = React.memo(function DocumentNode({
|
|||
[doc, onExport]
|
||||
);
|
||||
|
||||
const handleTitleTooltipOpenChange = useCallback((open: boolean) => {
|
||||
if (open && titleRef.current) {
|
||||
setTitleTooltipOpen(titleRef.current.scrollWidth > titleRef.current.clientWidth);
|
||||
} else {
|
||||
setTitleTooltipOpen(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const attachRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
(rowRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
||||
|
|
@ -197,7 +207,20 @@ export const DocumentNode = React.memo(function DocumentNode({
|
|||
);
|
||||
})()}
|
||||
|
||||
<span className="flex-1 min-w-0 truncate">{doc.title}</span>
|
||||
<Tooltip
|
||||
delayDuration={600}
|
||||
open={titleTooltipOpen}
|
||||
onOpenChange={handleTitleTooltipOpenChange}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<span ref={titleRef} className="flex-1 min-w-0 truncate">
|
||||
{doc.title}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="max-w-xs break-words">
|
||||
{doc.title}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{getDocumentTypeIcon(
|
||||
doc.document_type as DocumentTypeEnum,
|
||||
|
|
@ -259,11 +282,7 @@ export const DocumentNode = React.memo(function DocumentNode({
|
|||
Versions
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
disabled={isProcessing}
|
||||
onClick={() => onDelete(doc)}
|
||||
>
|
||||
<DropdownMenuItem disabled={isProcessing} onClick={() => onDelete(doc)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -305,11 +324,7 @@ export const DocumentNode = React.memo(function DocumentNode({
|
|||
Versions
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
<ContextMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
disabled={isProcessing}
|
||||
onClick={() => onDelete(doc)}
|
||||
>
|
||||
<ContextMenuItem disabled={isProcessing} onClick={() => onDelete(doc)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ interface FolderNodeProps {
|
|||
depth: number;
|
||||
isExpanded: boolean;
|
||||
isRenaming: boolean;
|
||||
childCount: number;
|
||||
selectionState: FolderSelectionState;
|
||||
processingState: "idle" | "processing" | "failed";
|
||||
onToggleSelect: (folderId: number, selectAll: boolean) => void;
|
||||
|
|
@ -101,7 +100,6 @@ export const FolderNode = React.memo(function FolderNode({
|
|||
depth,
|
||||
isExpanded,
|
||||
isRenaming,
|
||||
childCount,
|
||||
selectionState,
|
||||
processingState,
|
||||
onToggleSelect,
|
||||
|
|
@ -336,12 +334,6 @@ export const FolderNode = React.memo(function FolderNode({
|
|||
<span className="flex-1 min-w-0 truncate">{folder.name}</span>
|
||||
)}
|
||||
|
||||
{!isRenaming && childCount > 0 && (
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground tabular-nums">
|
||||
{childCount}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!isRenaming && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
|
|
|||
|
|
@ -86,16 +86,6 @@ export function FolderTreeView({
|
|||
|
||||
const docsByFolder = useMemo(() => groupBy(documents, (d) => d.folderId ?? "root"), [documents]);
|
||||
|
||||
const folderChildCounts = useMemo(() => {
|
||||
const counts: Record<number, number> = {};
|
||||
for (const f of folders) {
|
||||
const children = foldersByParent[f.id] ?? [];
|
||||
const docs = docsByFolder[f.id] ?? [];
|
||||
counts[f.id] = children.length + docs.length;
|
||||
}
|
||||
return counts;
|
||||
}, [folders, foldersByParent, docsByFolder]);
|
||||
|
||||
const [openContextMenuId, setOpenContextMenuId] = useState<string | null>(null);
|
||||
|
||||
// Single subscription for rename state — derived boolean passed to each FolderNode
|
||||
|
|
@ -106,14 +96,26 @@ export function FolderTreeView({
|
|||
);
|
||||
const handleCancelRename = useCallback(() => setRenamingFolderId(null), [setRenamingFolderId]);
|
||||
|
||||
const effectiveActiveTypes = useMemo(() => {
|
||||
if (
|
||||
activeTypes.includes("FILE" as DocumentTypeEnum) &&
|
||||
!activeTypes.includes("LOCAL_FOLDER_FILE" as DocumentTypeEnum)
|
||||
) {
|
||||
return [...activeTypes, "LOCAL_FOLDER_FILE" as DocumentTypeEnum];
|
||||
}
|
||||
return activeTypes;
|
||||
}, [activeTypes]);
|
||||
|
||||
const hasDescendantMatch = useMemo(() => {
|
||||
if (activeTypes.length === 0 && !searchQuery) return null;
|
||||
if (effectiveActiveTypes.length === 0 && !searchQuery) return null;
|
||||
const match: Record<number, boolean> = {};
|
||||
|
||||
function check(folderId: number): boolean {
|
||||
if (match[folderId] !== undefined) return match[folderId];
|
||||
const childDocs = (docsByFolder[folderId] ?? []).some(
|
||||
(d) => activeTypes.length === 0 || activeTypes.includes(d.document_type as DocumentTypeEnum)
|
||||
(d) =>
|
||||
effectiveActiveTypes.length === 0 ||
|
||||
effectiveActiveTypes.includes(d.document_type as DocumentTypeEnum)
|
||||
);
|
||||
if (childDocs) {
|
||||
match[folderId] = true;
|
||||
|
|
@ -134,7 +136,7 @@ export function FolderTreeView({
|
|||
check(f.id);
|
||||
}
|
||||
return match;
|
||||
}, [folders, docsByFolder, foldersByParent, activeTypes, searchQuery]);
|
||||
}, [folders, docsByFolder, foldersByParent, effectiveActiveTypes, searchQuery]);
|
||||
|
||||
const folderSelectionStates = useMemo(() => {
|
||||
const states: Record<number, FolderSelectionState> = {};
|
||||
|
|
@ -204,7 +206,9 @@ export function FolderTreeView({
|
|||
? childFolders.filter((f) => hasDescendantMatch[f.id])
|
||||
: childFolders;
|
||||
const childDocs = (docsByFolder[key] ?? []).filter(
|
||||
(d) => activeTypes.length === 0 || activeTypes.includes(d.document_type as DocumentTypeEnum)
|
||||
(d) =>
|
||||
effectiveActiveTypes.length === 0 ||
|
||||
effectiveActiveTypes.includes(d.document_type as DocumentTypeEnum)
|
||||
);
|
||||
|
||||
const nodes: React.ReactNode[] = [];
|
||||
|
|
@ -226,7 +230,6 @@ export function FolderTreeView({
|
|||
depth={depth}
|
||||
isExpanded={isExpanded}
|
||||
isRenaming={renamingFolderId === f.id}
|
||||
childCount={folderChildCounts[f.id] ?? 0}
|
||||
selectionState={folderSelectionStates[f.id] ?? "none"}
|
||||
processingState={folderProcessingStates[f.id] ?? "idle"}
|
||||
onToggleSelect={onToggleFolderSelect}
|
||||
|
|
@ -289,7 +292,7 @@ export function FolderTreeView({
|
|||
);
|
||||
}
|
||||
|
||||
if (treeNodes.length === 0 && (activeTypes.length > 0 || searchQuery)) {
|
||||
if (treeNodes.length === 0 && (effectiveActiveTypes.length > 0 || searchQuery)) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-4 py-12 text-muted-foreground">
|
||||
<Search className="h-10 w-10" />
|
||||
|
|
|
|||
|
|
@ -11,13 +11,12 @@ import { MarkdownViewer } from "@/components/markdown-viewer";
|
|||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
|
||||
const PlateEditor = dynamic(
|
||||
() => import("@/components/editor/plate-editor").then((m) => ({ default: m.PlateEditor })),
|
||||
{ ssr: false, loading: () => <Skeleton className="h-64 w-full" /> }
|
||||
{ ssr: false, loading: () => <EditorPanelSkeleton /> }
|
||||
);
|
||||
|
||||
const LARGE_DOCUMENT_THRESHOLD = 2 * 1024 * 1024; // 2MB
|
||||
|
|
|
|||
|
|
@ -158,17 +158,18 @@ export function PlateEditor({
|
|||
// When not forced read-only, the user can toggle between editing/viewing.
|
||||
const canToggleMode = !readOnly;
|
||||
|
||||
const contextProviderValue = useMemo(()=> ({
|
||||
onSave,
|
||||
hasUnsavedChanges,
|
||||
isSaving,
|
||||
canToggleMode,
|
||||
}), [onSave, hasUnsavedChanges, isSaving, canToggleMode]);
|
||||
const contextProviderValue = useMemo(
|
||||
() => ({
|
||||
onSave,
|
||||
hasUnsavedChanges,
|
||||
isSaving,
|
||||
canToggleMode,
|
||||
}),
|
||||
[onSave, hasUnsavedChanges, isSaving, canToggleMode]
|
||||
);
|
||||
|
||||
return (
|
||||
<EditorSaveContext.Provider
|
||||
value={contextProviderValue}
|
||||
>
|
||||
<EditorSaveContext.Provider value={contextProviderValue}>
|
||||
<Plate
|
||||
editor={editor}
|
||||
// Only pass readOnly as a controlled prop when forced (permanently read-only).
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
import Image from 'next/image';
|
||||
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay";
|
||||
|
||||
const useCases = [
|
||||
|
|
@ -83,13 +83,13 @@ function UseCaseCard({
|
|||
className="w-full rounded-xl object-cover transition-transform duration-500 group-hover:scale-[1.02]"
|
||||
/>
|
||||
<div className="relative w-full h-48">
|
||||
<Image
|
||||
src={src}
|
||||
alt={title}
|
||||
fill
|
||||
className="rounded-xl object-cover transition-transform duration-500 group-hover:scale-[1.02]"
|
||||
unoptimized={src.endsWith('.gif')}
|
||||
/>
|
||||
<Image
|
||||
src={src}
|
||||
alt={title}
|
||||
fill
|
||||
className="rounded-xl object-cover transition-transform duration-500 group-hover:scale-[1.02]"
|
||||
unoptimized={src.endsWith(".gif")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
|
|
|
|||
|
|
@ -347,35 +347,38 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
|
||||
// Navigation items
|
||||
const navItems: NavItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: "Inbox",
|
||||
url: "#inbox",
|
||||
icon: Inbox,
|
||||
isActive: isInboxSidebarOpen,
|
||||
badge: totalUnreadCount > 0 ? formatInboxCount(totalUnreadCount) : undefined,
|
||||
},
|
||||
{
|
||||
title: "Documents",
|
||||
url: "#documents",
|
||||
icon: SquareLibrary,
|
||||
isActive: isMobile
|
||||
? isDocumentsSidebarOpen
|
||||
: isDocumentsSidebarOpen && !isRightPanelCollapsed,
|
||||
},
|
||||
{
|
||||
title: "Announcements",
|
||||
url: "#announcements",
|
||||
icon: Megaphone,
|
||||
isActive: isAnnouncementsSidebarOpen,
|
||||
badge: announcementUnreadCount > 0 ? formatInboxCount(announcementUnreadCount) : undefined,
|
||||
},
|
||||
],
|
||||
() =>
|
||||
(
|
||||
[
|
||||
{
|
||||
title: "Inbox",
|
||||
url: "#inbox",
|
||||
icon: Inbox,
|
||||
isActive: isInboxSidebarOpen,
|
||||
badge: totalUnreadCount > 0 ? formatInboxCount(totalUnreadCount) : undefined,
|
||||
},
|
||||
isMobile
|
||||
? {
|
||||
title: "Documents",
|
||||
url: "#documents",
|
||||
icon: SquareLibrary,
|
||||
isActive: isDocumentsSidebarOpen,
|
||||
}
|
||||
: null,
|
||||
{
|
||||
title: "Announcements",
|
||||
url: "#announcements",
|
||||
icon: Megaphone,
|
||||
isActive: isAnnouncementsSidebarOpen,
|
||||
badge:
|
||||
announcementUnreadCount > 0 ? formatInboxCount(announcementUnreadCount) : undefined,
|
||||
},
|
||||
] as (NavItem | null)[]
|
||||
).filter((item): item is NavItem => item !== null),
|
||||
[
|
||||
isMobile,
|
||||
isInboxSidebarOpen,
|
||||
isDocumentsSidebarOpen,
|
||||
isRightPanelCollapsed,
|
||||
totalUnreadCount,
|
||||
isAnnouncementsSidebarOpen,
|
||||
announcementUnreadCount,
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-[90vw] sm:max-w-sm p-4 sm:p-5 data-[state=open]:animate-none data-[state=closed]:animate-none">
|
||||
<DialogContent className="max-w-[90vw] sm:max-w-sm p-4 sm:p-5 select-none data-[state=open]:animate-none data-[state=closed]:animate-none">
|
||||
<DialogHeader className="space-y-2 pb-2">
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
|
|
@ -107,7 +107,7 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
|
|||
placeholder={t("name_placeholder")}
|
||||
{...field}
|
||||
autoFocus
|
||||
className="text-sm h-9 sm:h-10"
|
||||
className="text-sm h-9 sm:h-10 select-text"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
|
@ -130,7 +130,7 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
|
|||
<Input
|
||||
placeholder={t("description_placeholder")}
|
||||
{...field}
|
||||
className="text-sm h-9 sm:h-10"
|
||||
className="text-sm h-9 sm:h-10 select-text"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
|||
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
||||
import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { DocumentsSidebar } from "../sidebar";
|
||||
|
||||
|
|
@ -27,7 +26,7 @@ const HitlEditPanelContent = dynamic(
|
|||
import("@/components/hitl-edit-panel/hitl-edit-panel").then((m) => ({
|
||||
default: m.HitlEditPanelContent,
|
||||
})),
|
||||
{ ssr: false, loading: () => <Skeleton className="h-96 w-full" /> }
|
||||
{ ssr: false, loading: () => null }
|
||||
);
|
||||
|
||||
const ReportPanelContent = dynamic(
|
||||
|
|
@ -35,7 +34,7 @@ const ReportPanelContent = dynamic(
|
|||
import("@/components/report-panel/report-panel").then((m) => ({
|
||||
default: m.ReportPanelContent,
|
||||
})),
|
||||
{ ssr: false, loading: () => <Skeleton className="h-96 w-full" /> }
|
||||
{ ssr: false, loading: () => null }
|
||||
);
|
||||
|
||||
interface RightPanelProps {
|
||||
|
|
@ -78,14 +77,14 @@ export function RightPanelExpandButton() {
|
|||
if (!collapsed || !hasContent) return null;
|
||||
|
||||
return (
|
||||
<div className="flex shrink-0 items-center px-1">
|
||||
<div className="flex shrink-0 items-center px-0.5">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => startTransition(() => setCollapsed(false))}
|
||||
className="h-7 w-7 shrink-0"
|
||||
className="h-8 w-8 shrink-0 -m-0.5"
|
||||
>
|
||||
<PanelRight className="h-4 w-4" />
|
||||
<span className="sr-only">Expand panel</span>
|
||||
|
|
|
|||
|
|
@ -376,7 +376,7 @@ export function AllPrivateChatsSidebarContent({
|
|||
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||
</button>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<Tooltip delayDuration={600}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ export function AllSharedChatsSidebarContent({
|
|||
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||
</button>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<Tooltip delayDuration={600}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -530,7 +530,8 @@ export function DocumentsSidebar({
|
|||
const typeCounts = useMemo(() => {
|
||||
const counts: Partial<Record<string, number>> = {};
|
||||
for (const d of treeDocuments) {
|
||||
counts[d.document_type] = (counts[d.document_type] || 0) + 1;
|
||||
const displayType = d.document_type === "LOCAL_FOLDER_FILE" ? "FILE" : d.document_type;
|
||||
counts[displayType] = (counts[displayType] || 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}, [treeDocuments]);
|
||||
|
|
@ -745,7 +746,7 @@ export function DocumentsSidebar({
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-x-hidden pt-0 flex flex-col">
|
||||
<div className="flex-1 min-h-0 pt-0 flex flex-col">
|
||||
<div className="px-4 pb-2">
|
||||
<DocumentsFilters
|
||||
typeCounts={typeCounts}
|
||||
|
|
|
|||
|
|
@ -790,36 +790,23 @@ export function InboxSidebarContent({
|
|||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{isMobile ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 rounded-full"
|
||||
onClick={handleMarkAllAsRead}
|
||||
disabled={totalUnreadCount === 0}
|
||||
>
|
||||
<CheckCheck className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="sr-only">{t("mark_all_read") || "Mark all as read"}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 rounded-full"
|
||||
onClick={handleMarkAllAsRead}
|
||||
disabled={totalUnreadCount === 0}
|
||||
>
|
||||
<CheckCheck className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="sr-only">{t("mark_all_read") || "Mark all as read"}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="z-80">
|
||||
{t("mark_all_read") || "Mark all as read"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 rounded-full"
|
||||
onClick={handleMarkAllAsRead}
|
||||
disabled={totalUnreadCount === 0}
|
||||
>
|
||||
<CheckCheck className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="sr-only">{t("mark_all_read") || "Mark all as read"}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="z-80">
|
||||
{t("mark_all_read") || "Mark all as read"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -932,30 +919,8 @@ export function InboxSidebarContent({
|
|||
)}
|
||||
style={{ contentVisibility: "auto", containIntrinsicSize: "0 80px" }}
|
||||
>
|
||||
{isMobile ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleItemClick(item)}
|
||||
disabled={isMarkingAsRead}
|
||||
className="flex items-center gap-3 flex-1 min-w-0 text-left overflow-hidden"
|
||||
>
|
||||
<div className="shrink-0">{getStatusIcon(item)}</div>
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<p
|
||||
className={cn(
|
||||
"text-xs font-medium line-clamp-2",
|
||||
!item.read && "font-semibold"
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground line-clamp-2 mt-0.5">
|
||||
{convertRenderedToDisplay(item.message)}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
) : (
|
||||
<Tooltip>
|
||||
{activeTab === "status" ? (
|
||||
<Tooltip delayDuration={600}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -986,6 +951,28 @@ export function InboxSidebarContent({
|
|||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleItemClick(item)}
|
||||
disabled={isMarkingAsRead}
|
||||
className="flex items-center gap-3 flex-1 min-w-0 text-left overflow-hidden"
|
||||
>
|
||||
<div className="shrink-0">{getStatusIcon(item)}</div>
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<p
|
||||
className={cn(
|
||||
"text-xs font-medium line-clamp-2",
|
||||
!item.read && "font-semibold"
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground line-clamp-2 mt-0.5">
|
||||
{convertRenderedToDisplay(item.message)}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end gap-1.5 shrink-0 w-10">
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProp
|
|||
<Progress value={usagePercentage} className="h-1.5" />
|
||||
<Link
|
||||
href={`/dashboard/${searchSpaceId}/more-pages`}
|
||||
className="group flex w-full items-center justify-between rounded-md px-1.5 py-1 -mx-1.5 transition-colors hover:bg-accent"
|
||||
className="group flex w-[calc(100%+0.75rem)] items-center justify-between rounded-md px-1.5 py-1 -mx-1.5 transition-colors hover:bg-accent"
|
||||
>
|
||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground group-hover:text-accent-foreground">
|
||||
<Zap className="h-3 w-3 shrink-0" />
|
||||
|
|
@ -48,7 +48,7 @@ export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProp
|
|||
{pageBuyingEnabled && (
|
||||
<Link
|
||||
href={`/dashboard/${searchSpaceId}/buy-pages`}
|
||||
className="group flex w-full items-center justify-between rounded-md px-1.5 py-1 -mx-1.5 transition-colors hover:bg-accent"
|
||||
className="group flex w-[calc(100%+0.75rem)] items-center justify-between rounded-md px-1.5 py-1 -mx-1.5 transition-colors hover:bg-accent"
|
||||
>
|
||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground group-hover:text-accent-foreground">
|
||||
<CreditCard className="h-3 w-3 shrink-0" />
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import { createCodePlugin } from "@streamdown/code";
|
|||
import { createMathPlugin } from "@streamdown/math";
|
||||
import { Streamdown, type StreamdownProps } from "streamdown";
|
||||
import "katex/dist/katex.min.css";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from 'next/image';
|
||||
import { is } from "drizzle-orm";
|
||||
import Image from "next/image";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const code = createCodePlugin({
|
||||
themes: ["nord", "nord"],
|
||||
|
|
@ -130,30 +130,31 @@ export function MarkdownViewer({ content, className, maxLength }: MarkdownViewer
|
|||
),
|
||||
hr: ({ ...props }) => <hr className="my-4 border-muted" {...props} />,
|
||||
img: ({ src, alt, width: _w, height: _h, ...props }) => {
|
||||
const isDataOrUnknownUrl = typeof src === "string" && (src.startsWith("data:") || !src.startsWith("http"));
|
||||
const isDataOrUnknownUrl =
|
||||
typeof src === "string" && (src.startsWith("data:") || !src.startsWith("http"));
|
||||
|
||||
return isDataOrUnknownUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="max-w-full h-auto my-4 rounded"
|
||||
alt={alt || "markdown image"}
|
||||
src={src}
|
||||
loading="lazy"
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
className="max-w-full h-auto my-4 rounded"
|
||||
alt={alt || "markdown image"}
|
||||
src={typeof src === "string" ? src : ""}
|
||||
width={_w || 800}
|
||||
height={_h || 600}
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 60vw"
|
||||
unoptimized={isDataOrUnknownUrl}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
return isDataOrUnknownUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="max-w-full h-auto my-4 rounded"
|
||||
alt={alt || "markdown image"}
|
||||
src={src}
|
||||
loading="lazy"
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
className="max-w-full h-auto my-4 rounded"
|
||||
alt={alt || "markdown image"}
|
||||
src={typeof src === "string" ? src : ""}
|
||||
width={_w || 800}
|
||||
height={_h || 600}
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 60vw"
|
||||
unoptimized={isDataOrUnknownUrl}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
table: ({ ...props }) => (
|
||||
<div className="overflow-x-auto my-4 rounded-lg border border-border w-full">
|
||||
<table className="w-full divide-y divide-border" {...props} />
|
||||
|
|
|
|||
|
|
@ -163,21 +163,16 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
|||
)}
|
||||
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 md:w-auto md:px-3 md:gap-2 relative bg-muted hover:bg-muted/80 border-0 select-none"
|
||||
>
|
||||
<CurrentIcon className="h-4 w-4" />
|
||||
<span className="hidden md:inline text-sm">{buttonLabel}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Share settings</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 md:w-auto md:px-3 md:gap-2 relative bg-muted hover:bg-muted/80 border-0 select-none"
|
||||
>
|
||||
<CurrentIcon className="h-4 w-4" />
|
||||
<span className="hidden md:inline text-sm">{buttonLabel}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-[280px] md:w-[320px] p-0 rounded-lg shadow-lg border-border/60 dark:bg-neutral-900 dark:border dark:border-white/5 select-none"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { Bot, Check, ChevronDown, Edit3, ImageIcon, Plus, Zap } from "lucide-react";
|
||||
import { Bot, Check, ChevronDown, Edit3, ImageIcon, Plus, Search, Zap } from "lucide-react";
|
||||
import { type UIEvent, useCallback, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
|
|
@ -344,7 +344,7 @@ export function ModelSelector({
|
|||
>
|
||||
<CommandEmpty className="py-8 text-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Bot className="size-8 text-muted-foreground" />
|
||||
<Search className="size-8 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">No models found</p>
|
||||
<p className="text-xs text-muted-foreground/60">Try a different search term</p>
|
||||
</div>
|
||||
|
|
@ -531,8 +531,9 @@ export function ModelSelector({
|
|||
>
|
||||
<CommandEmpty className="py-8 text-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<ImageIcon className="size-8 text-muted-foreground" />
|
||||
<Search className="size-8 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">No image models found</p>
|
||||
<p className="text-xs text-muted-foreground/60">Try a different search term</p>
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import { useTranslations } from "next-intl";
|
|||
import { useMemo } from "react";
|
||||
import { ApiKeyContent } from "@/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent";
|
||||
import { CommunityPromptsContent } from "@/app/dashboard/[search_space_id]/user-settings/components/CommunityPromptsContent";
|
||||
import { DesktopContent } from "@/app/dashboard/[search_space_id]/user-settings/components/DesktopContent";
|
||||
import { ProfileContent } from "@/app/dashboard/[search_space_id]/user-settings/components/ProfileContent";
|
||||
import { PromptsContent } from "@/app/dashboard/[search_space_id]/user-settings/components/PromptsContent";
|
||||
import { PurchaseHistoryContent } from "@/app/dashboard/[search_space_id]/user-settings/components/PurchaseHistoryContent";
|
||||
import { DesktopContent } from "@/app/dashboard/[search_space_id]/user-settings/components/DesktopContent";
|
||||
import { userSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
|
||||
import { SettingsDialog } from "@/components/settings/settings-dialog";
|
||||
import { usePlatform } from "@/hooks/use-platform";
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ export function ImageConfigDialog({
|
|||
className="relative text-sm h-9 min-w-[120px]"
|
||||
>
|
||||
<span className={isSubmitting ? "opacity-0" : ""}>
|
||||
{mode === "edit" ? "Save Changes" : "Create & Use"}
|
||||
{mode === "edit" ? "Save Changes" : "Add Model"}
|
||||
</span>
|
||||
{isSubmitting && <Spinner size="sm" className="absolute" />}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ export function ModelConfigDialog({
|
|||
className="relative text-sm h-9 min-w-[120px]"
|
||||
>
|
||||
<span className={isSubmitting ? "opacity-0" : ""}>
|
||||
{mode === "edit" ? "Save Changes" : "Create & Use"}
|
||||
{mode === "edit" ? "Save Changes" : "Add Model"}
|
||||
</span>
|
||||
{isSubmitting && <Spinner size="sm" className="absolute" />}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@ const FILE_TYPE_CONFIG: Record<string, Record<string, string[]>> = {
|
|||
"application/rtf": [".rtf"],
|
||||
"application/xml": [".xml"],
|
||||
"application/epub+zip": [".epub"],
|
||||
"text/html": [".html", ".htm", ".web"],
|
||||
"image/gif": [".gif"],
|
||||
"image/svg+xml": [".svg"],
|
||||
...audioFileTypes,
|
||||
|
|
@ -470,8 +469,9 @@ export function DocumentUploadTab({
|
|||
</button>
|
||||
))
|
||||
) : (
|
||||
<div
|
||||
className="flex flex-col items-center gap-4 py-12 px-4 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-col items-center gap-4 py-12 px-4 cursor-pointer w-full bg-transparent border-none"
|
||||
onClick={() => {
|
||||
if (!isElectron) fileInputRef.current?.click();
|
||||
}}
|
||||
|
|
@ -483,10 +483,16 @@ export function DocumentUploadTab({
|
|||
</p>
|
||||
<p className="text-sm text-muted-foreground">{t("file_size_limit")}</p>
|
||||
</div>
|
||||
<div className="w-full mt-1" onClick={(e) => e.stopPropagation()}>
|
||||
{/* biome-ignore lint/a11y/useSemanticElements: wrapper to stop click propagation to parent button */}
|
||||
<div
|
||||
className="w-full mt-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
role="group"
|
||||
>
|
||||
{renderBrowseButton({ fullWidth: true })}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -681,9 +687,13 @@ export function DocumentUploadTab({
|
|||
</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-3 pb-3">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{supportedExtensions.map((ext) => (
|
||||
<Badge key={ext} variant="outline" className="text-[10px] px-1.5 py-0">
|
||||
<Badge
|
||||
key={ext}
|
||||
variant="secondary"
|
||||
className="rounded border-0 bg-neutral-200/80 dark:bg-neutral-700/60 text-muted-foreground text-[10px] px-2 py-0.5 font-normal"
|
||||
>
|
||||
{ext}
|
||||
</Badge>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { Code2, Database, ExternalLink, File, FileText, Globe, Newspaper } from "lucide-react";
|
||||
import NextImage from "next/image";
|
||||
import * as React from "react";
|
||||
import { openSafeNavigationHref, resolveSafeNavigationHref } from "../shared/media";
|
||||
import { cn, Popover, PopoverContent, PopoverTrigger } from "./_adapter";
|
||||
import { Citation } from "./citation";
|
||||
import type { CitationType, CitationVariant, SerializableCitation } from "./schema";
|
||||
import NextImage from 'next/image';
|
||||
|
||||
|
||||
const TYPE_ICONS: Record<CitationType, LucideIcon> = {
|
||||
webpage: Globe,
|
||||
|
|
@ -264,9 +263,9 @@ function OverflowItem({ citation, onClick }: OverflowItemProps) {
|
|||
className="size-4.5 rounded-full object-cover"
|
||||
unoptimized={true}
|
||||
/>
|
||||
) : (
|
||||
) : (
|
||||
<TypeIcon className="text-muted-foreground size-3" aria-hidden="true" />
|
||||
)}
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="group-hover:decoration-foreground/30 truncate text-sm font-medium group-hover:underline group-hover:underline-offset-2">
|
||||
{citation.title}
|
||||
|
|
@ -341,18 +340,18 @@ function StackedCitations({ id, citations, className, onNavigate }: StackedCitat
|
|||
style={{ zIndex: maxIcons - index }}
|
||||
>
|
||||
{citation.favicon ? (
|
||||
<NextImage
|
||||
src={citation.favicon}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width={18}
|
||||
height={18}
|
||||
className="size-4.5 rounded-full object-cover"
|
||||
unoptimized={true}
|
||||
/>
|
||||
) : (
|
||||
<TypeIcon className="text-muted-foreground size-3" aria-hidden="true" />
|
||||
)}
|
||||
<NextImage
|
||||
src={citation.favicon}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width={18}
|
||||
height={18}
|
||||
className="size-4.5 rounded-full object-cover"
|
||||
unoptimized={true}
|
||||
/>
|
||||
) : (
|
||||
<TypeIcon className="text-muted-foreground size-3" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { Code2, Database, ExternalLink, File, FileText, Globe, Newspaper } from "lucide-react";
|
||||
import NextImage from "next/image";
|
||||
import * as React from "react";
|
||||
import { openSafeNavigationHref, sanitizeHref } from "../shared/media";
|
||||
import { cn, Popover, PopoverContent, PopoverTrigger } from "./_adapter";
|
||||
import type { CitationType, CitationVariant, SerializableCitation } from "./schema";
|
||||
import NextImage from 'next/image';
|
||||
|
||||
const FALLBACK_LOCALE = "en-US";
|
||||
|
||||
|
|
@ -115,18 +115,18 @@ export function Citation(props: CitationProps) {
|
|||
};
|
||||
|
||||
const iconElement = favicon ? (
|
||||
<NextImage
|
||||
src={favicon}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width={16}
|
||||
height={16}
|
||||
className="bg-muted size-3.5 shrink-0 rounded object-cover"
|
||||
unoptimized={true}
|
||||
/>
|
||||
) : (
|
||||
<TypeIcon className="size-3.5 shrink-0 opacity-60" aria-hidden="true" />
|
||||
);
|
||||
<NextImage
|
||||
src={favicon}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width={16}
|
||||
height={16}
|
||||
className="bg-muted size-3.5 shrink-0 rounded object-cover"
|
||||
unoptimized={true}
|
||||
/>
|
||||
) : (
|
||||
<TypeIcon className="size-3.5 shrink-0 opacity-60" aria-hidden="true" />
|
||||
);
|
||||
|
||||
const { open, handleMouseEnter, handleMouseLeave } = useHoverPopover();
|
||||
|
||||
|
|
|
|||
|
|
@ -202,7 +202,10 @@ const Tabs = forwardRef<
|
|||
},
|
||||
[onValueChange, value]
|
||||
);
|
||||
const contextValue = useMemo(() => ({ activeValue, onValueChange: handleValueChange }), [activeValue, handleValueChange]);
|
||||
const contextValue = useMemo(
|
||||
() => ({ activeValue, onValueChange: handleValueChange }),
|
||||
[activeValue, handleValueChange]
|
||||
);
|
||||
return (
|
||||
<TabsContext.Provider value={contextValue}>
|
||||
<div ref={ref} className={cn("tabs-container", className)} {...props}>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export function FloatingToolbar({
|
|||
{...rootProps}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"scrollbar-hide absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md print:hidden dark:bg-neutral-900 dark:border-white/5",
|
||||
"scrollbar-hide absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md print:hidden",
|
||||
"max-w-[80vw]",
|
||||
className
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ export function InsertToolbarButton(props: DropdownMenuProps) {
|
|||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="z-[100] flex max-h-[60vh] min-w-0 flex-col overflow-y-auto dark:bg-neutral-900 dark:border dark:border-white/5"
|
||||
className="z-[100] flex max-h-[60vh] min-w-0 flex-col overflow-y-auto"
|
||||
align="start"
|
||||
>
|
||||
{groups.map(({ group, items }) => (
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ export function SlashInputElement({ children, ...props }: PlateElementProps) {
|
|||
<InlineCombobox element={props.element} trigger="/">
|
||||
<InlineComboboxInput />
|
||||
|
||||
<InlineComboboxContent className="dark:bg-neutral-900 dark:border dark:border-white/5">
|
||||
<InlineComboboxContent>
|
||||
<InlineComboboxEmpty>No results found.</InlineComboboxEmpty>
|
||||
|
||||
{slashCommandGroups.map(({ heading, items }) => (
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { toggleVariants } from "@/components/ui/toggle";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
|
|
@ -28,8 +28,8 @@ function ToggleGroup({
|
|||
VariantProps<typeof toggleVariants> & {
|
||||
spacing?: number;
|
||||
}) {
|
||||
const contextValue = useMemo(() => ({variant, size, spacing }), [variant, size, spacing]);
|
||||
|
||||
const contextValue = useMemo(() => ({ variant, size, spacing }), [variant, size, spacing]);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
|
|
@ -43,9 +43,7 @@ function ToggleGroup({
|
|||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
<ToggleGroupContext.Provider value={contextValue}>{children}</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,26 @@
|
|||
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import type * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
function useIsTouchDevice() {
|
||||
const [isTouch, setIsTouch] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const update = () => setIsTouch(mql.matches);
|
||||
update();
|
||||
mql.addEventListener("change", update);
|
||||
return () => mql.removeEventListener("change", update);
|
||||
}, []);
|
||||
|
||||
return isTouch;
|
||||
}
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
disableHoverableContent = true,
|
||||
|
|
@ -20,10 +37,21 @@ function TooltipProvider({
|
|||
);
|
||||
}
|
||||
|
||||
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
function Tooltip({
|
||||
open,
|
||||
onOpenChange,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
const isMobile = useIsTouchDevice();
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
<TooltipPrimitive.Root
|
||||
data-slot="tooltip"
|
||||
open={isMobile ? false : open}
|
||||
onOpenChange={isMobile ? undefined : onOpenChange}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export function TurnIntoToolbarButton({
|
|||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="z-[100] ignore-click-outside/toolbar min-w-0 max-h-[60vh] overflow-y-auto dark:bg-neutral-900 dark:border dark:border-white/5"
|
||||
className="z-[100] ignore-click-outside/toolbar min-w-0 max-h-[60vh] overflow-y-auto"
|
||||
onCloseAutoFocus={(e) => {
|
||||
e.preventDefault();
|
||||
editor.tf.focus();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue