Merge remote-tracking branch 'upstream/dev' into electon-desktop

This commit is contained in:
CREDO23 2026-03-29 03:10:51 +02:00
commit b8a1d1f594
165 changed files with 17921 additions and 8767 deletions

View file

@ -4,7 +4,6 @@ import { useAtomValue, useSetAtom } from "jotai";
import { AlertTriangle, Cable, Settings } from "lucide-react";
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
import {
globalNewLLMConfigsAtom,
@ -22,6 +21,7 @@ import { Tabs, TabsContent } from "@/components/ui/tabs";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { useConnectorsSync } from "@/hooks/use-connectors-sync";
import { PICKER_CLOSE_EVENT, PICKER_OPEN_EVENT } from "@/hooks/use-google-picker";
import { useZeroDocumentTypeCounts } from "@/hooks/use-zero-document-type-counts";
import { cn } from "@/lib/utils";
import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header";
import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view";
@ -72,9 +72,9 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
const llmConfigLoading = preferencesLoading || globalConfigsLoading;
// Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min)
const { data: documentTypeCounts, isFetching: documentTypesLoading } =
useAtomValue(documentTypeCountsAtom);
// Real-time document type counts via Zero (updates instantly as docs are indexed)
const documentTypeCounts = useZeroDocumentTypeCounts(searchSpaceId);
const documentTypesLoading = documentTypeCounts === undefined;
// Read status inbox items from shared atom (populated by LayoutDataProvider)
// instead of creating a duplicate useInbox("status") hook.

View file

@ -867,6 +867,9 @@ export const useConnectorDialog = () => {
setIsOpen(false);
setIsFromOAuth(false);
setIndexingConfig(null);
setIndexingConnector(null);
setIndexingConnectorConfig(null);
refreshConnectors();
queryClient.invalidateQueries({
@ -898,6 +901,9 @@ export const useConnectorDialog = () => {
const handleSkipIndexing = useCallback(() => {
setIsOpen(false);
setIsFromOAuth(false);
setIndexingConfig(null);
setIndexingConnector(null);
setIndexingConnectorConfig(null);
}, [setIsOpen]);
// Handle starting edit mode

View file

@ -28,16 +28,14 @@ export const InlineCitation: FC<InlineCitationProps> = ({ chunkId, isDocsChunk =
url=""
isDocsChunk={isDocsChunk}
>
<span
<button
type="button"
onClick={() => setIsOpen(true)}
onKeyDown={(e) => e.key === "Enter" && setIsOpen(true)}
className="text-[10px] font-bold bg-primary/80 hover:bg-primary text-primary-foreground rounded-full min-w-4 h-4 px-1 inline-flex items-center justify-center align-super cursor-pointer transition-colors ml-0.5"
title={`View source chunk #${chunkId}`}
role="button"
tabIndex={0}
>
{chunkId}
</span>
</button>
</SourceDetailPanel>
);
};

View file

@ -0,0 +1,104 @@
"use client";
import { CheckIcon, CopyIcon } from "lucide-react";
import type { CSSProperties } from "react";
import { memo, useEffect, useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { materialDark, materialLight } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn, copyToClipboard } from "@/lib/utils";
type MarkdownCodeBlockProps = {
className?: string;
language: string;
codeText: string;
isDarkMode: boolean;
};
function stripThemeBackgrounds(
theme: Record<string, CSSProperties>
): Record<string, CSSProperties> {
const cleaned: Record<string, CSSProperties> = {};
for (const key of Object.keys(theme)) {
const { background, backgroundColor, ...rest } = theme[key] as CSSProperties & {
background?: string;
backgroundColor?: string;
};
cleaned[key] = rest;
}
return cleaned;
}
const cleanMaterialDark = stripThemeBackgrounds(materialDark);
const cleanMaterialLight = stripThemeBackgrounds(materialLight);
function MarkdownCodeBlockComponent({
className,
language,
codeText,
isDarkMode,
}: MarkdownCodeBlockProps) {
const [hasCopied, setHasCopied] = useState(false);
useEffect(() => {
if (!hasCopied) return;
const timer = setTimeout(() => setHasCopied(false), 2000);
return () => clearTimeout(timer);
}, [hasCopied]);
return (
<div className="mt-4 overflow-hidden rounded-2xl" style={{ background: "var(--syntax-bg)" }}>
<div className="flex items-center justify-between gap-4 px-4 py-2 font-semibold text-muted-foreground text-sm">
<span className="lowercase text-xs">{language}</span>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
type="button"
onClick={async () => {
const ok = await copyToClipboard(codeText);
if (ok) setHasCopied(true);
}}
aria-label={hasCopied ? "Copied code" : "Copy code"}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon className="!size-3" /> : <CopyIcon className="!size-3" />}
</Button>
</div>
<SyntaxHighlighter
style={isDarkMode ? cleanMaterialDark : cleanMaterialLight}
language={language}
PreTag="div"
customStyle={{ margin: 0, background: "transparent" }}
className={cn(className)}
>
{codeText}
</SyntaxHighlighter>
</div>
);
}
export const MarkdownCodeBlock = memo(MarkdownCodeBlockComponent);
export function MarkdownCodeBlockSkeleton() {
return (
<div
className="mt-4 overflow-hidden rounded-2xl border"
style={{ background: "var(--syntax-bg)" }}
>
<div className="flex items-center justify-between gap-4 border-b px-4 py-2">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-8 w-8 rounded-md" />
</div>
<div className="space-y-2 p-4">
<Skeleton className="h-4 w-11/12" />
<Skeleton className="h-4 w-10/12" />
<Skeleton className="h-4 w-8/12" />
<Skeleton className="h-4 w-9/12" />
</div>
</div>
);
}

View file

@ -7,19 +7,17 @@ import {
unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
useIsMarkdownCodeBlock,
} from "@assistant-ui/react-markdown";
import { CheckIcon, CopyIcon, ExternalLinkIcon } from "lucide-react";
import { ExternalLinkIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { useTheme } from "next-themes";
import type { CSSProperties } from "react";
import { type FC, memo, type ReactNode, useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { materialDark, materialLight } from "react-syntax-highlighter/dist/esm/styles/prism";
import { memo, type ReactNode } from "react";
import rehypeKatex from "rehype-katex";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import { ImagePreview, ImageRoot, ImageZoom } from "@/components/assistant-ui/image";
import "katex/dist/katex.min.css";
import { InlineCitation, UrlCitation } from "@/components/assistant-ui/inline-citation";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
@ -30,22 +28,32 @@ import {
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
function stripThemeBackgrounds(
theme: Record<string, CSSProperties>
): Record<string, CSSProperties> {
const cleaned: Record<string, CSSProperties> = {};
for (const key of Object.keys(theme)) {
const { background, backgroundColor, ...rest } = theme[key] as CSSProperties & {
background?: string;
backgroundColor?: string;
};
cleaned[key] = rest;
}
return cleaned;
function MarkdownCodeBlockSkeleton() {
return (
<div
className="mt-4 overflow-hidden rounded-2xl border"
style={{ background: "var(--syntax-bg)" }}
>
<div className="flex items-center justify-between gap-4 border-b px-4 py-2">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-8 w-8 rounded-md" />
</div>
<div className="space-y-2 p-4">
<Skeleton className="h-4 w-11/12" />
<Skeleton className="h-4 w-10/12" />
<Skeleton className="h-4 w-8/12" />
<Skeleton className="h-4 w-9/12" />
</div>
</div>
);
}
const cleanMaterialDark = stripThemeBackgrounds(materialDark);
const cleanMaterialLight = stripThemeBackgrounds(materialLight);
const LazyMarkdownCodeBlock = dynamic(
() => import("./markdown-code-block").then((mod) => mod.MarkdownCodeBlock),
{
loading: () => <MarkdownCodeBlockSkeleton />,
}
);
// Storage for URL citations replaced during preprocess to avoid GFM autolink interference.
// Populated in preprocessMarkdown, consumed in parseTextWithCitations.
@ -178,39 +186,6 @@ const MarkdownTextImpl = () => {
export const MarkdownText = memo(MarkdownTextImpl);
const InlineCodeHeader: FC<{ language: string; code: string }> = ({ language, code }) => {
const { isCopied, copyToClipboard } = useCopyToClipboard();
const onCopy = () => {
if (!code || isCopied) return;
copyToClipboard(code);
};
return (
<div className="flex items-center justify-between gap-4 px-4 py-2 font-semibold text-muted-foreground text-sm">
<span className="lowercase text-xs">{language}</span>
<TooltipIconButton tooltip="Copy" onClick={onCopy}>
{!isCopied && <CopyIcon />}
{isCopied && <CheckIcon />}
</TooltipIconButton>
</div>
);
};
const useCopyToClipboard = ({ copiedDuration = 3000 }: { copiedDuration?: number } = {}) => {
const [isCopied, setIsCopied] = useState<boolean>(false);
const copyToClipboard = (value: string) => {
if (!value) return;
navigator.clipboard.writeText(value).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), copiedDuration);
});
};
return { isCopied, copyToClipboard };
};
/**
* Helper to process children and replace citation patterns with components
*/
@ -421,24 +396,20 @@ const defaultComponents = memoizeMarkdownComponents({
<code
className={cn("aui-md-inline-code rounded border bg-muted font-semibold", className)}
{...props}
/>
>
{children}
</code>
);
}
const language = /language-(\w+)/.exec(className || "")?.[1] ?? "text";
const codeString = String(children).replace(/\n$/, "");
const syntaxStyle = resolvedTheme === "dark" ? cleanMaterialDark : cleanMaterialLight;
return (
<div className="mt-4 overflow-hidden rounded-2xl" style={{ background: "var(--syntax-bg)" }}>
<InlineCodeHeader language={language} code={codeString} />
<SyntaxHighlighter
style={syntaxStyle}
language={language}
PreTag="div"
customStyle={{ margin: 0, background: "transparent" }}
>
{codeString}
</SyntaxHighlighter>
</div>
<LazyMarkdownCodeBlock
className={className}
language={language}
codeText={codeString}
isDarkMode={resolvedTheme === "dark"}
/>
);
},
strong: ({ className, children, ...props }) => (

View file

@ -225,17 +225,13 @@ function ThreadListItemComponent({
onDelete,
}: ThreadListItemComponentProps) {
return (
<div
<button
type="button"
className={cn(
"group flex items-center gap-2 rounded-lg px-3 py-2 transition-colors cursor-pointer",
"group flex w-full items-center gap-2 rounded-lg px-3 py-2 transition-colors cursor-pointer text-left",
isActive ? "bg-accent text-accent-foreground" : "hover:bg-muted/50"
)}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") onClick();
}}
role="button"
tabIndex={0}
>
<MessageSquareIcon className="size-4 shrink-0 text-muted-foreground" />
<div className="flex-1 min-w-0">
@ -274,7 +270,7 @@ function ThreadListItemComponent({
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</button>
);
}

View file

@ -113,7 +113,7 @@ const ThreadContent: FC = () => {
>
<ThreadPrimitive.Viewport
turnAnchor="top"
className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-y-auto px-4 pt-4"
className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-y-scroll px-4 pt-4"
>
<AuiIf condition={({ thread }) => thread.isEmpty}>
<ThreadWelcome />
@ -1119,6 +1119,8 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
{hasWebSearchTool && (
<button
type="button"
aria-label={isWebSearchEnabled ? "Disable web search" : "Enable web search"}
aria-pressed={isWebSearchEnabled}
onClick={() => toggleTool("web_search")}
className={cn(
"rounded-full transition-all flex items-center gap-1 px-2 py-1 border h-8 select-none",
@ -1237,7 +1239,7 @@ interface ToolGroup {
const TOOL_GROUPS: ToolGroup[] = [
{
label: "Research",
tools: ["search_knowledge_base", "search_surfsense_docs", "scrape_webpage"],
tools: ["search_surfsense_docs", "scrape_webpage"],
},
{
label: "Generate",