2025-12-19 16:42:58 +02:00
|
|
|
import {
|
2026-03-24 02:22:51 +05:30
|
|
|
AuiIf,
|
2025-12-19 16:42:58 +02:00
|
|
|
ComposerPrimitive,
|
|
|
|
|
MessagePrimitive,
|
|
|
|
|
ThreadPrimitive,
|
2026-03-24 02:22:51 +05:30
|
|
|
useAui,
|
|
|
|
|
useAuiState,
|
2025-12-19 16:42:58 +02:00
|
|
|
} from "@assistant-ui/react";
|
2025-12-23 16:04:39 +05:30
|
|
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
2025-12-19 16:42:58 +02:00
|
|
|
import {
|
2025-12-23 01:16:25 -08:00
|
|
|
AlertCircle,
|
2025-12-19 16:42:58 +02:00
|
|
|
ArrowUpIcon,
|
2026-04-27 18:49:43 +02:00
|
|
|
Camera,
|
2026-03-29 02:54:48 +02:00
|
|
|
ChevronDown,
|
|
|
|
|
ChevronUp,
|
|
|
|
|
Clipboard,
|
2026-03-17 01:09:15 +05:30
|
|
|
Globe,
|
2026-03-15 16:27:33 +05:30
|
|
|
Plus,
|
2026-03-15 16:39:56 +05:30
|
|
|
Settings2,
|
2025-12-19 16:42:58 +02:00
|
|
|
SquareIcon,
|
2026-03-10 16:17:12 +05:30
|
|
|
Unplug,
|
2026-03-15 16:27:33 +05:30
|
|
|
Upload,
|
2026-03-21 11:38:42 +05:30
|
|
|
Wrench,
|
2026-03-10 16:16:24 +05:30
|
|
|
X,
|
2025-12-19 16:42:58 +02:00
|
|
|
} from "lucide-react";
|
2026-05-17 23:29:41 +05:30
|
|
|
import { AnimatePresence, motion } from "motion/react";
|
2026-03-21 11:38:42 +05:30
|
|
|
import Image from "next/image";
|
2025-12-23 16:04:39 +05:30
|
|
|
import { useParams } from "next/navigation";
|
2026-04-14 01:50:37 +05:30
|
|
|
import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
2026-03-11 12:30:20 +05:30
|
|
|
import {
|
|
|
|
|
agentToolsAtom,
|
|
|
|
|
disabledToolsAtom,
|
|
|
|
|
hydrateDisabledToolsAtom,
|
|
|
|
|
toggleToolAtom,
|
|
|
|
|
} from "@/atoms/agent-tools/agent-tools.atoms";
|
2026-01-22 16:43:08 -08:00
|
|
|
import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom";
|
2026-04-29 19:15:46 +05:30
|
|
|
import { currentThreadAtom } from "@/atoms/chat/current-thread.atom";
|
2026-05-09 22:15:51 -07:00
|
|
|
import {
|
|
|
|
|
type MentionedDocumentInfo,
|
|
|
|
|
mentionedDocumentsAtom,
|
|
|
|
|
} from "@/atoms/chat/mentioned-documents.atom";
|
2026-04-24 19:17:43 +02:00
|
|
|
import { pendingUserImageDataUrlsAtom } from "@/atoms/chat/pending-user-images.atom";
|
2026-04-29 19:15:46 +05:30
|
|
|
import {
|
|
|
|
|
clearPremiumAlertForThreadAtom,
|
|
|
|
|
premiumAlertByThreadAtom,
|
|
|
|
|
} from "@/atoms/chat/premium-alert.atom";
|
2026-03-10 16:16:24 +05:30
|
|
|
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
|
2026-03-10 14:45:37 +05:30
|
|
|
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
2026-01-20 18:39:50 +02:00
|
|
|
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
2025-12-23 01:16:25 -08:00
|
|
|
import {
|
|
|
|
|
globalNewLLMConfigsAtom,
|
|
|
|
|
llmPreferencesAtom,
|
|
|
|
|
newLLMConfigsAtom,
|
|
|
|
|
} from "@/atoms/new-llm-config/new-llm-config-query.atoms";
|
|
|
|
|
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
2026-01-16 15:09:51 +02:00
|
|
|
import { AssistantMessage } from "@/components/assistant-ui/assistant-message";
|
2026-01-20 18:39:50 +02:00
|
|
|
import { ChatSessionStatus } from "@/components/assistant-ui/chat-session-status";
|
2026-05-01 03:09:53 +05:30
|
|
|
import { ChatViewport } from "@/components/assistant-ui/chat-viewport";
|
2026-03-10 17:36:26 -07:00
|
|
|
import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup";
|
2026-03-15 16:39:56 +05:30
|
|
|
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
|
2025-12-25 13:44:18 +05:30
|
|
|
import {
|
|
|
|
|
InlineMentionEditor,
|
|
|
|
|
type InlineMentionEditorRef,
|
|
|
|
|
} from "@/components/assistant-ui/inline-mention-editor";
|
2025-12-19 16:42:58 +02:00
|
|
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
2026-01-15 00:05:53 -08:00
|
|
|
import { UserMessage } from "@/components/assistant-ui/user-message";
|
2025-12-24 07:06:35 +02:00
|
|
|
import {
|
2025-12-26 00:41:14 +05:30
|
|
|
DocumentMentionPicker,
|
|
|
|
|
type DocumentMentionPickerRef,
|
2025-12-25 11:42:12 -08:00
|
|
|
} from "@/components/new-chat/document-mention-picker";
|
2026-03-30 01:50:41 +05:30
|
|
|
import { PromptPicker, type PromptPickerRef } from "@/components/new-chat/prompt-picker";
|
2026-03-10 16:16:24 +05:30
|
|
|
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
|
2025-12-19 16:42:58 +02:00
|
|
|
import { Button } from "@/components/ui/button";
|
2026-05-03 22:52:43 +05:30
|
|
|
import {
|
|
|
|
|
Drawer,
|
|
|
|
|
DrawerContent,
|
|
|
|
|
DrawerHandle,
|
|
|
|
|
DrawerHeader,
|
|
|
|
|
DrawerTitle,
|
|
|
|
|
} from "@/components/ui/drawer";
|
2026-03-15 16:27:33 +05:30
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
2026-05-17 00:57:35 +05:30
|
|
|
DropdownMenuPortal,
|
|
|
|
|
DropdownMenuSub,
|
|
|
|
|
DropdownMenuSubContent,
|
|
|
|
|
DropdownMenuSubTrigger,
|
2026-03-15 16:27:33 +05:30
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from "@/components/ui/dropdown-menu";
|
2026-04-09 00:31:36 +05:30
|
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
2026-03-11 12:30:20 +05:30
|
|
|
import { Switch } from "@/components/ui/switch";
|
2026-03-10 16:16:24 +05:30
|
|
|
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
2026-03-21 13:20:13 +05:30
|
|
|
import {
|
|
|
|
|
CONNECTOR_ICON_TO_TYPES,
|
|
|
|
|
CONNECTOR_TOOL_ICON_PATHS,
|
2026-04-29 07:40:11 -07:00
|
|
|
getToolDisplayName,
|
2026-03-21 13:20:13 +05:30
|
|
|
getToolIcon,
|
|
|
|
|
} from "@/contracts/enums/toolIcons";
|
2026-02-27 17:19:25 -08:00
|
|
|
import { useBatchCommentsPreload } from "@/hooks/use-comments";
|
2026-03-23 19:29:08 +02:00
|
|
|
import { useCommentsSync } from "@/hooks/use-comments-sync";
|
2026-03-11 12:04:22 +05:30
|
|
|
import { useMediaQuery } from "@/hooks/use-media-query";
|
2026-04-07 00:43:40 -07:00
|
|
|
import { useElectronAPI } from "@/hooks/use-platform";
|
2026-04-24 19:17:43 +02:00
|
|
|
import { captureDisplayToPngDataUrl } from "@/lib/chat/display-media-capture";
|
2026-04-30 18:42:38 -07:00
|
|
|
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
|
2026-04-14 21:26:00 -07:00
|
|
|
import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/lib/layout-events";
|
2025-12-22 23:29:49 +02:00
|
|
|
import { cn } from "@/lib/utils";
|
2025-12-22 22:54:22 +05:30
|
|
|
|
2026-04-09 00:22:30 +05:30
|
|
|
const COMPOSER_PLACEHOLDER = "Ask anything, type / for prompts, type @ to mention docs";
|
2026-02-01 17:26:50 -05:00
|
|
|
|
2026-03-24 02:23:05 +05:30
|
|
|
export const Thread: FC = () => {
|
|
|
|
|
return <ThreadContent />;
|
2026-01-19 14:37:06 +02:00
|
|
|
};
|
|
|
|
|
|
2026-03-06 22:38:49 +05:30
|
|
|
const ThreadContent: FC = () => {
|
2026-01-19 14:37:06 +02:00
|
|
|
return (
|
|
|
|
|
<ThreadPrimitive.Root
|
2026-03-17 01:09:15 +05:30
|
|
|
className="aui-root aui-thread-root @container flex h-full min-h-0 flex-col bg-main-panel"
|
2026-01-19 14:37:06 +02:00
|
|
|
style={{
|
2026-05-17 02:57:00 +05:30
|
|
|
["--thread-max-width" as string]: "42rem",
|
2026-01-19 14:37:06 +02:00
|
|
|
}}
|
|
|
|
|
>
|
2026-05-01 03:09:53 +05:30
|
|
|
<ChatViewport
|
|
|
|
|
footer={
|
2026-05-01 03:10:21 +05:30
|
|
|
<AuiIf condition={({ thread }) => !thread.isEmpty}>
|
|
|
|
|
<PremiumQuotaPinnedAlert />
|
|
|
|
|
<Composer />
|
|
|
|
|
</AuiIf>
|
2026-05-01 03:09:53 +05:30
|
|
|
}
|
2025-12-19 16:42:58 +02:00
|
|
|
>
|
2026-03-24 02:22:51 +05:30
|
|
|
<AuiIf condition={({ thread }) => thread.isEmpty}>
|
2026-01-19 14:37:06 +02:00
|
|
|
<ThreadWelcome />
|
2026-03-24 02:22:51 +05:30
|
|
|
</AuiIf>
|
2025-12-19 16:42:58 +02:00
|
|
|
|
2026-01-19 14:37:06 +02:00
|
|
|
<ThreadPrimitive.Messages
|
|
|
|
|
components={{
|
|
|
|
|
UserMessage,
|
|
|
|
|
EditComposer,
|
|
|
|
|
AssistantMessage,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2026-05-01 03:09:53 +05:30
|
|
|
</ChatViewport>
|
2026-01-19 14:37:06 +02:00
|
|
|
</ThreadPrimitive.Root>
|
2025-12-19 16:42:58 +02:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-29 19:15:46 +05:30
|
|
|
const PremiumQuotaPinnedAlert: FC = () => {
|
|
|
|
|
const currentThreadState = useAtomValue(currentThreadAtom);
|
|
|
|
|
const alertsByThread = useAtomValue(premiumAlertByThreadAtom);
|
|
|
|
|
const clearPremiumAlertForThread = useSetAtom(clearPremiumAlertForThreadAtom);
|
|
|
|
|
|
|
|
|
|
const currentThreadId = currentThreadState?.id;
|
|
|
|
|
if (!currentThreadId) return null;
|
|
|
|
|
|
|
|
|
|
const alert = alertsByThread[currentThreadId];
|
|
|
|
|
if (!alert) return null;
|
|
|
|
|
|
2025-12-19 16:42:58 +02:00
|
|
|
return (
|
2026-04-29 21:58:17 +05:30
|
|
|
<div className="mx-0 overflow-hidden rounded-2xl border-input bg-muted px-4 py-4 text-foreground select-none">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<AlertCircle className="size-4 shrink-0 text-muted-foreground" />
|
2026-04-29 19:15:46 +05:30
|
|
|
<div className="min-w-0 flex-1">
|
2026-04-29 20:17:45 +05:30
|
|
|
<p className="text-sm">{alert.message}</p>
|
2026-04-29 19:15:46 +05:30
|
|
|
</div>
|
2026-05-14 13:49:33 +05:30
|
|
|
<Button
|
2026-04-29 19:15:46 +05:30
|
|
|
type="button"
|
2026-05-14 13:49:33 +05:30
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="size-6 text-muted-foreground hover:bg-transparent hover:text-accent-foreground"
|
2026-04-29 19:15:46 +05:30
|
|
|
aria-label="Dismiss premium quota alert"
|
|
|
|
|
onClick={() => clearPremiumAlertForThread(currentThreadId)}
|
|
|
|
|
>
|
|
|
|
|
<X className="size-4" />
|
2026-05-14 13:49:33 +05:30
|
|
|
</Button>
|
2026-04-29 19:15:46 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-19 16:42:58 +02:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-16 11:32:06 -08:00
|
|
|
const getTimeBasedGreeting = (user?: { display_name?: string | null; email?: string }): string => {
|
2025-12-22 18:38:08 +05:30
|
|
|
const hour = new Date().getHours();
|
2025-12-23 01:16:25 -08:00
|
|
|
|
2026-01-16 11:32:06 -08:00
|
|
|
// Extract first name: prefer display_name, fall back to email extraction
|
|
|
|
|
let firstName: string | null = null;
|
|
|
|
|
|
|
|
|
|
if (user?.display_name?.trim()) {
|
|
|
|
|
// Use display_name if available and not empty
|
|
|
|
|
// Extract first name from display_name (take first word)
|
|
|
|
|
const nameParts = user.display_name.trim().split(/\s+/);
|
|
|
|
|
firstName = nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1).toLowerCase();
|
|
|
|
|
} else if (user?.email) {
|
|
|
|
|
// Fall back to email extraction if display_name is not available
|
|
|
|
|
firstName =
|
|
|
|
|
user.email.split("@")[0].split(".")[0].charAt(0).toUpperCase() +
|
|
|
|
|
user.email.split("@")[0].split(".")[0].slice(1);
|
|
|
|
|
}
|
2025-12-23 01:16:25 -08:00
|
|
|
|
2025-12-22 18:38:08 +05:30
|
|
|
// Array of greeting variations for each time period
|
2025-12-25 12:18:45 +05:30
|
|
|
const morningGreetings = ["Good morning", "Fresh start today", "Morning", "Hey there"];
|
2025-12-23 01:16:25 -08:00
|
|
|
|
|
|
|
|
const afternoonGreetings = ["Good afternoon", "Afternoon", "Hey there", "Hi there"];
|
|
|
|
|
|
|
|
|
|
const eveningGreetings = ["Good evening", "Evening", "Hey there", "Hi there"];
|
|
|
|
|
|
|
|
|
|
const nightGreetings = ["Good night", "Evening", "Hey there", "Winding down"];
|
|
|
|
|
|
2025-12-25 12:18:45 +05:30
|
|
|
const lateNightGreetings = ["Still up", "Night owl mode", "Up past bedtime", "Hi there"];
|
2025-12-23 01:16:25 -08:00
|
|
|
|
2025-12-22 18:38:08 +05:30
|
|
|
// Select a random greeting based on time
|
|
|
|
|
let greeting: string;
|
2025-12-22 23:57:16 +05:30
|
|
|
if (hour < 5) {
|
|
|
|
|
// Late night: midnight to 5 AM
|
|
|
|
|
greeting = lateNightGreetings[Math.floor(Math.random() * lateNightGreetings.length)];
|
|
|
|
|
} else if (hour < 12) {
|
2025-12-22 18:38:08 +05:30
|
|
|
greeting = morningGreetings[Math.floor(Math.random() * morningGreetings.length)];
|
2025-12-22 23:57:16 +05:30
|
|
|
} else if (hour < 18) {
|
2025-12-22 18:38:08 +05:30
|
|
|
greeting = afternoonGreetings[Math.floor(Math.random() * afternoonGreetings.length)];
|
2025-12-22 23:57:16 +05:30
|
|
|
} else if (hour < 22) {
|
2025-12-22 18:38:08 +05:30
|
|
|
greeting = eveningGreetings[Math.floor(Math.random() * eveningGreetings.length)];
|
|
|
|
|
} else {
|
2025-12-22 23:57:16 +05:30
|
|
|
// Night: 10 PM to midnight
|
2025-12-22 18:38:08 +05:30
|
|
|
greeting = nightGreetings[Math.floor(Math.random() * nightGreetings.length)];
|
|
|
|
|
}
|
2025-12-23 01:16:25 -08:00
|
|
|
|
2025-12-22 18:38:08 +05:30
|
|
|
// Add personalization with first name if available
|
|
|
|
|
if (firstName) {
|
|
|
|
|
return `${greeting}, ${firstName}!`;
|
|
|
|
|
}
|
2025-12-23 01:16:25 -08:00
|
|
|
|
2025-12-22 18:38:08 +05:30
|
|
|
return `${greeting}!`;
|
2025-12-19 16:42:58 +02:00
|
|
|
};
|
|
|
|
|
|
2025-12-22 18:38:08 +05:30
|
|
|
const ThreadWelcome: FC = () => {
|
|
|
|
|
const { data: user } = useAtomValue(currentUserAtom);
|
2025-12-24 07:06:35 +02:00
|
|
|
|
2025-12-23 14:24:36 +05:30
|
|
|
// Memoize greeting so it doesn't change on re-renders (only on user change)
|
2026-01-16 11:32:06 -08:00
|
|
|
const greeting = useMemo(() => getTimeBasedGreeting(user), [user]);
|
2025-12-23 01:16:25 -08:00
|
|
|
|
2025-12-19 16:42:58 +02:00
|
|
|
return (
|
2025-12-22 18:38:08 +05:30
|
|
|
<div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative">
|
2026-03-11 16:37:56 -07:00
|
|
|
{/* Greeting positioned above the composer */}
|
2025-12-23 19:10:58 -08:00
|
|
|
<div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center">
|
2026-05-17 02:57:00 +05:30
|
|
|
<h1 className="aui-thread-welcome-message-inner text-3xl md:text-[2.625rem] select-none">
|
2026-03-17 04:40:46 +05:30
|
|
|
{greeting}
|
|
|
|
|
</h1>
|
2025-12-22 18:38:08 +05:30
|
|
|
</div>
|
2025-12-23 02:21:41 +05:30
|
|
|
{/* Composer - top edge fixed, expands downward only */}
|
2026-03-11 16:37:56 -07:00
|
|
|
<div className="w-full flex items-start justify-center absolute top-[calc(50%-3.5rem)] left-0 right-0">
|
2025-12-22 18:38:08 +05:30
|
|
|
<Composer />
|
|
|
|
|
</div>
|
2025-12-19 16:42:58 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-10 16:16:24 +05:30
|
|
|
const BANNER_CONNECTORS = [
|
|
|
|
|
{ type: "GOOGLE_DRIVE_CONNECTOR", label: "Google Drive" },
|
|
|
|
|
{ type: "GOOGLE_GMAIL_CONNECTOR", label: "Gmail" },
|
|
|
|
|
{ type: "NOTION_CONNECTOR", label: "Notion" },
|
|
|
|
|
{ type: "YOUTUBE_CONNECTOR", label: "YouTube" },
|
|
|
|
|
{ type: "SLACK_CONNECTOR", label: "Slack" },
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
const BANNER_DISMISSED_KEY = "surfsense-connect-tools-banner-dismissed";
|
|
|
|
|
|
2026-05-17 16:46:34 +05:30
|
|
|
const ConnectToolsBanner: FC<{
|
|
|
|
|
isThreadEmpty: boolean;
|
|
|
|
|
onVisibleChange?: (visible: boolean) => void;
|
|
|
|
|
}> = ({ isThreadEmpty, onVisibleChange }) => {
|
2026-03-10 16:16:24 +05:30
|
|
|
const { data: connectors } = useAtomValue(connectorsAtom);
|
|
|
|
|
const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom);
|
|
|
|
|
const [dismissed, setDismissed] = useState(() => {
|
|
|
|
|
if (typeof window === "undefined") return false;
|
|
|
|
|
return localStorage.getItem(BANNER_DISMISSED_KEY) === "true";
|
|
|
|
|
});
|
2026-05-17 23:29:41 +05:30
|
|
|
const [dismissRequested, setDismissRequested] = useState(false);
|
2026-03-10 16:16:24 +05:30
|
|
|
|
|
|
|
|
const hasConnectors = (connectors?.length ?? 0) > 0;
|
2026-05-17 16:46:34 +05:30
|
|
|
const isVisible = !dismissed && !hasConnectors && isThreadEmpty;
|
2026-05-17 23:29:41 +05:30
|
|
|
const shouldShowTray = isVisible && !dismissRequested;
|
2026-03-10 16:16:24 +05:30
|
|
|
|
2026-05-17 16:46:34 +05:30
|
|
|
useEffect(() => {
|
|
|
|
|
onVisibleChange?.(isVisible);
|
|
|
|
|
}, [isVisible, onVisibleChange]);
|
|
|
|
|
|
2026-03-10 16:16:24 +05:30
|
|
|
const handleDismiss = (e: React.MouseEvent) => {
|
|
|
|
|
e.stopPropagation();
|
2026-05-17 23:29:41 +05:30
|
|
|
setDismissRequested(true);
|
2026-03-10 16:16:24 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-17 23:29:41 +05:30
|
|
|
<AnimatePresence
|
|
|
|
|
initial={false}
|
|
|
|
|
onExitComplete={() => {
|
|
|
|
|
if (!dismissRequested) return;
|
|
|
|
|
setDismissed(true);
|
|
|
|
|
localStorage.setItem(BANNER_DISMISSED_KEY, "true");
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{shouldShowTray ? (
|
|
|
|
|
<motion.div
|
|
|
|
|
key="connect-tools-tray"
|
|
|
|
|
initial={{ opacity: 0, y: -10 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
exit={{ opacity: 0, y: -14 }}
|
|
|
|
|
transition={{ duration: 0.18, ease: "easeOut" }}
|
|
|
|
|
className="relative z-0 -mt-5 flex min-w-0 items-center gap-2 rounded-b-3xl border border-input bg-muted/40 px-4 pt-7 pb-3 shadow-sm shadow-black/5 dark:shadow-black/10"
|
|
|
|
|
>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-7 min-w-0 cursor-pointer justify-start gap-2 rounded-md px-0 text-[13px] font-normal text-muted-foreground select-none hover:bg-transparent hover:text-foreground"
|
|
|
|
|
onClick={() => setConnectorDialogOpen(true)}
|
2026-05-17 16:46:34 +05:30
|
|
|
>
|
2026-05-17 23:29:41 +05:30
|
|
|
<Unplug className="size-4 shrink-0" />
|
|
|
|
|
<span className="truncate">Connect your tools</span>
|
|
|
|
|
</Button>
|
|
|
|
|
<div className="min-w-0 flex-1" />
|
|
|
|
|
<AvatarGroup className="shrink-0">
|
|
|
|
|
{BANNER_CONNECTORS.map(({ type }, i) => (
|
|
|
|
|
<Avatar
|
|
|
|
|
key={type}
|
|
|
|
|
className="size-5"
|
|
|
|
|
style={{ zIndex: BANNER_CONNECTORS.length - i }}
|
|
|
|
|
>
|
|
|
|
|
<AvatarFallback className="bg-accent text-[10px]">
|
|
|
|
|
{getConnectorIcon(type, "size-3")}
|
|
|
|
|
</AvatarFallback>
|
|
|
|
|
</Avatar>
|
|
|
|
|
))}
|
|
|
|
|
</AvatarGroup>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={handleDismiss}
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="size-7 shrink-0 cursor-pointer rounded-md text-muted-foreground hover:bg-transparent hover:text-foreground"
|
|
|
|
|
aria-label="Dismiss"
|
|
|
|
|
>
|
|
|
|
|
<X className="size-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</motion.div>
|
|
|
|
|
) : null}
|
|
|
|
|
</AnimatePresence>
|
2026-03-10 16:16:24 +05:30
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-24 19:17:43 +02:00
|
|
|
const PendingScreenImageStrip: FC = () => {
|
|
|
|
|
const [urls, setUrls] = useAtom(pendingUserImageDataUrlsAtom);
|
|
|
|
|
if (urls.length === 0) return null;
|
|
|
|
|
return (
|
|
|
|
|
<div className="mx-3 mt-2 flex flex-wrap gap-2">
|
|
|
|
|
{urls.map((url, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={url}
|
|
|
|
|
className="group relative h-14 w-14 shrink-0 overflow-hidden rounded-md border border-border/50 bg-muted"
|
|
|
|
|
>
|
2026-05-14 15:02:46 +05:30
|
|
|
<Image
|
|
|
|
|
src={url}
|
|
|
|
|
alt="Pending screenshot preview"
|
|
|
|
|
fill
|
|
|
|
|
sizes="56px"
|
|
|
|
|
className="object-cover"
|
|
|
|
|
draggable={false}
|
|
|
|
|
unoptimized
|
|
|
|
|
/>
|
2026-05-14 13:49:33 +05:30
|
|
|
<Button
|
2026-04-24 19:17:43 +02:00
|
|
|
type="button"
|
|
|
|
|
onClick={() => setUrls((prev) => prev.filter((_, i) => i !== index))}
|
2026-05-14 13:49:33 +05:30
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="absolute right-0.5 top-0.5 size-5 rounded-full bg-background/90 text-muted-foreground shadow-sm transition-opacity hover:bg-background/90 hover:text-accent-foreground sm:opacity-0 sm:group-hover:opacity-100"
|
2026-04-24 19:17:43 +02:00
|
|
|
aria-label="Remove screenshot"
|
|
|
|
|
>
|
|
|
|
|
<X className="size-3" />
|
2026-05-14 13:49:33 +05:30
|
|
|
</Button>
|
2026-04-24 19:17:43 +02:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-29 02:54:48 +02:00
|
|
|
const ClipboardChip: FC<{ text: string; onDismiss: () => void }> = ({ text, onDismiss }) => {
|
|
|
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
|
const isLong = text.length > 120;
|
|
|
|
|
const preview = isLong ? `${text.slice(0, 120)}…` : text;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="mx-3 mt-2 rounded-lg border border-border/40 bg-background/60">
|
|
|
|
|
<div className="flex items-center gap-2 px-3 py-2">
|
|
|
|
|
<Clipboard className="size-4 shrink-0 text-muted-foreground" />
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">From clipboard</span>
|
|
|
|
|
<div className="flex-1" />
|
|
|
|
|
{isLong && (
|
2026-05-14 13:49:33 +05:30
|
|
|
<Button
|
2026-03-29 02:54:48 +02:00
|
|
|
type="button"
|
|
|
|
|
onClick={() => setExpanded((v) => !v)}
|
2026-05-14 13:49:33 +05:30
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="size-5 text-muted-foreground hover:bg-transparent hover:text-accent-foreground"
|
2026-03-29 02:54:48 +02:00
|
|
|
>
|
|
|
|
|
{expanded ? <ChevronUp className="size-3.5" /> : <ChevronDown className="size-3.5" />}
|
2026-05-14 13:49:33 +05:30
|
|
|
</Button>
|
2026-03-29 02:54:48 +02:00
|
|
|
)}
|
2026-05-14 13:49:33 +05:30
|
|
|
<Button
|
2026-03-29 02:54:48 +02:00
|
|
|
type="button"
|
|
|
|
|
onClick={onDismiss}
|
2026-05-14 13:49:33 +05:30
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="size-5 text-muted-foreground hover:bg-transparent hover:text-accent-foreground"
|
2026-03-29 02:54:48 +02:00
|
|
|
>
|
|
|
|
|
<X className="size-3.5" />
|
2026-05-14 13:49:33 +05:30
|
|
|
</Button>
|
2026-03-29 02:54:48 +02:00
|
|
|
</div>
|
|
|
|
|
<div className="px-3 pb-2">
|
|
|
|
|
<p className="text-xs text-foreground/80 whitespace-pre-wrap wrap-break-word leading-relaxed">
|
|
|
|
|
{expanded ? text : preview}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-19 16:42:58 +02:00
|
|
|
const Composer: FC = () => {
|
2026-01-18 20:13:51 +05:30
|
|
|
// Document mention state (atoms persist across component remounts)
|
2025-12-23 15:13:03 +05:30
|
|
|
const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom);
|
2025-12-22 23:17:48 +02:00
|
|
|
const [showDocumentPopover, setShowDocumentPopover] = useState(false);
|
2026-03-29 00:07:08 +02:00
|
|
|
const [showPromptPicker, setShowPromptPicker] = useState(false);
|
2025-12-23 15:13:03 +05:30
|
|
|
const [mentionQuery, setMentionQuery] = useState("");
|
2026-03-28 23:20:10 +02:00
|
|
|
const [actionQuery, setActionQuery] = useState("");
|
2025-12-25 13:44:18 +05:30
|
|
|
const editorRef = useRef<InlineMentionEditorRef>(null);
|
2026-05-09 22:15:51 -07:00
|
|
|
const prevMentionedDocsRef = useRef<Map<string, MentionedDocumentInfo>>(new Map());
|
2025-12-26 00:41:14 +05:30
|
|
|
const documentPickerRef = useRef<DocumentMentionPickerRef>(null);
|
2026-03-29 00:07:08 +02:00
|
|
|
const promptPickerRef = useRef<PromptPickerRef>(null);
|
2026-01-20 18:39:50 +02:00
|
|
|
const { search_space_id, chat_id } = useParams();
|
2026-03-24 02:22:51 +05:30
|
|
|
const aui = useAui();
|
2025-12-25 14:19:22 +05:30
|
|
|
const hasAutoFocusedRef = useRef(false);
|
2026-04-08 05:21:39 +05:30
|
|
|
|
2026-04-07 00:43:40 -07:00
|
|
|
const electronAPI = useElectronAPI();
|
2026-03-29 00:45:11 +02:00
|
|
|
const [clipboardInitialText, setClipboardInitialText] = useState<string | undefined>();
|
|
|
|
|
const clipboardLoadedRef = useRef(false);
|
2026-03-24 19:28:41 +02:00
|
|
|
useEffect(() => {
|
2026-04-07 00:43:40 -07:00
|
|
|
if (!electronAPI || clipboardLoadedRef.current) return;
|
2026-03-29 00:45:11 +02:00
|
|
|
clipboardLoadedRef.current = true;
|
2026-04-14 01:50:37 +05:30
|
|
|
electronAPI.getQuickAskText().then((text: string) => {
|
2026-03-27 20:07:55 +02:00
|
|
|
if (text) {
|
2026-03-29 00:45:11 +02:00
|
|
|
setClipboardInitialText(text);
|
2026-03-27 20:07:55 +02:00
|
|
|
}
|
2026-03-24 19:28:41 +02:00
|
|
|
});
|
2026-04-07 00:43:40 -07:00
|
|
|
}, [electronAPI]);
|
2026-03-24 19:28:41 +02:00
|
|
|
|
2026-03-24 02:22:51 +05:30
|
|
|
const isThreadEmpty = useAuiState(({ thread }) => thread.isEmpty);
|
|
|
|
|
const isThreadRunning = useAuiState(({ thread }) => thread.isRunning);
|
2026-05-17 16:46:34 +05:30
|
|
|
const [connectToolsTrayVisible, setConnectToolsTrayVisible] = useState(false);
|
2025-12-25 17:52:48 +05:30
|
|
|
|
2026-04-07 02:49:24 -07:00
|
|
|
const currentPlaceholder = COMPOSER_PLACEHOLDER;
|
2026-02-01 17:26:50 -05:00
|
|
|
|
2026-01-20 19:48:28 +02:00
|
|
|
// Live collaboration state
|
2026-01-20 18:39:50 +02:00
|
|
|
const { data: currentUser } = useAtomValue(currentUserAtom);
|
|
|
|
|
const { data: members } = useAtomValue(membersAtom);
|
|
|
|
|
const threadId = useMemo(() => {
|
|
|
|
|
if (Array.isArray(chat_id) && chat_id.length > 0) {
|
|
|
|
|
return Number.parseInt(chat_id[0], 10) || null;
|
|
|
|
|
}
|
|
|
|
|
return typeof chat_id === "string" ? Number.parseInt(chat_id, 10) || null : null;
|
|
|
|
|
}, [chat_id]);
|
2026-01-22 19:04:23 +02:00
|
|
|
const sessionState = useAtomValue(chatSessionStateAtom);
|
|
|
|
|
const isAiResponding = sessionState?.isAiResponding ?? false;
|
|
|
|
|
const respondingToUserId = sessionState?.respondingToUserId ?? null;
|
2026-01-20 18:39:50 +02:00
|
|
|
const isBlockedByOtherUser = isAiResponding && respondingToUserId !== currentUser?.id;
|
|
|
|
|
|
2026-03-23 19:29:08 +02:00
|
|
|
// Sync comments for the entire thread via Zero (one subscription per thread)
|
|
|
|
|
useCommentsSync(threadId);
|
2026-01-22 17:57:20 +02:00
|
|
|
|
2026-02-27 17:19:25 -08:00
|
|
|
// Batch-prefetch comments for all assistant messages so individual useComments
|
|
|
|
|
// hooks never fire their own network requests (eliminates N+1 API calls).
|
|
|
|
|
// Return a primitive string from the selector so useSyncExternalStore can
|
|
|
|
|
// compare snapshots by value and avoid infinite re-render loops.
|
2026-03-24 02:22:51 +05:30
|
|
|
const assistantIdsKey = useAuiState(({ thread }) =>
|
2026-02-27 17:19:25 -08:00
|
|
|
thread.messages
|
|
|
|
|
.filter((m) => m.role === "assistant" && m.id?.startsWith("msg-"))
|
2026-03-06 15:59:45 +05:30
|
|
|
.map((m) => m.id?.replace("msg-", ""))
|
2026-02-27 17:19:25 -08:00
|
|
|
.join(",")
|
|
|
|
|
);
|
|
|
|
|
const assistantDbMessageIds = useMemo(
|
|
|
|
|
() => (assistantIdsKey ? assistantIdsKey.split(",").map(Number) : []),
|
|
|
|
|
[assistantIdsKey]
|
|
|
|
|
);
|
|
|
|
|
useBatchCommentsPreload(assistantDbMessageIds);
|
|
|
|
|
|
2026-01-18 20:13:51 +05:30
|
|
|
// Auto-focus editor on new chat page after mount
|
2025-12-25 14:19:22 +05:30
|
|
|
useEffect(() => {
|
|
|
|
|
if (isThreadEmpty && !hasAutoFocusedRef.current && editorRef.current) {
|
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
|
|
|
editorRef.current?.focus();
|
|
|
|
|
hasAutoFocusedRef.current = true;
|
|
|
|
|
}, 100);
|
|
|
|
|
return () => clearTimeout(timeoutId);
|
|
|
|
|
}
|
|
|
|
|
}, [isThreadEmpty]);
|
|
|
|
|
|
2026-03-10 15:40:17 +05:30
|
|
|
// Close document picker when a slide-out panel (inbox, shared/private chats) opens
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handler = () => {
|
|
|
|
|
setShowDocumentPopover(false);
|
|
|
|
|
setMentionQuery("");
|
|
|
|
|
};
|
|
|
|
|
window.addEventListener(SLIDEOUT_PANEL_OPENED_EVENT, handler);
|
|
|
|
|
return () => window.removeEventListener(SLIDEOUT_PANEL_OPENED_EVENT, handler);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-01-18 20:13:51 +05:30
|
|
|
// Sync editor text with assistant-ui composer runtime
|
2025-12-25 13:44:18 +05:30
|
|
|
const handleEditorChange = useCallback(
|
|
|
|
|
(text: string) => {
|
2026-03-24 02:22:51 +05:30
|
|
|
aui.composer().setText(text);
|
2025-12-25 13:44:18 +05:30
|
|
|
},
|
2026-03-24 02:22:51 +05:30
|
|
|
[aui]
|
2025-12-25 13:44:18 +05:30
|
|
|
);
|
2025-12-23 15:13:03 +05:30
|
|
|
|
2026-01-18 20:13:51 +05:30
|
|
|
// Open document picker when @ mention is triggered
|
2025-12-25 13:44:18 +05:30
|
|
|
const handleMentionTrigger = useCallback((query: string) => {
|
|
|
|
|
setShowDocumentPopover(true);
|
|
|
|
|
setMentionQuery(query);
|
|
|
|
|
}, []);
|
2025-12-22 23:17:48 +02:00
|
|
|
|
2026-01-18 20:13:51 +05:30
|
|
|
// Close document picker and reset query
|
2025-12-25 13:44:18 +05:30
|
|
|
const handleMentionClose = useCallback(() => {
|
|
|
|
|
if (showDocumentPopover) {
|
|
|
|
|
setShowDocumentPopover(false);
|
2025-12-23 15:13:03 +05:30
|
|
|
setMentionQuery("");
|
2025-12-23 14:24:36 +05:30
|
|
|
}
|
2025-12-25 13:44:18 +05:30
|
|
|
}, [showDocumentPopover]);
|
2025-12-23 14:24:36 +05:30
|
|
|
|
2026-03-28 23:20:10 +02:00
|
|
|
// Open action picker when / is triggered
|
|
|
|
|
const handleActionTrigger = useCallback((query: string) => {
|
2026-03-29 00:07:08 +02:00
|
|
|
setShowPromptPicker(true);
|
2026-03-28 23:20:10 +02:00
|
|
|
setActionQuery(query);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Close action picker and reset query
|
|
|
|
|
const handleActionClose = useCallback(() => {
|
2026-03-29 00:07:08 +02:00
|
|
|
if (showPromptPicker) {
|
|
|
|
|
setShowPromptPicker(false);
|
2026-03-28 23:20:10 +02:00
|
|
|
setActionQuery("");
|
|
|
|
|
}
|
2026-03-29 00:07:08 +02:00
|
|
|
}, [showPromptPicker]);
|
2026-03-28 23:20:10 +02:00
|
|
|
|
|
|
|
|
const handleActionSelect = useCallback(
|
|
|
|
|
(action: { name: string; prompt: string; mode: "transform" | "explore" }) => {
|
2026-03-29 02:54:48 +02:00
|
|
|
let userText = editorRef.current?.getText() ?? "";
|
|
|
|
|
const trigger = `/${actionQuery}`;
|
|
|
|
|
if (userText.endsWith(trigger)) {
|
|
|
|
|
userText = userText.slice(0, -trigger.length).trimEnd();
|
|
|
|
|
}
|
|
|
|
|
const finalPrompt = action.prompt.includes("{selection}")
|
|
|
|
|
? action.prompt.replace("{selection}", () => userText)
|
2026-03-30 01:50:41 +05:30
|
|
|
: userText
|
|
|
|
|
? `${action.prompt}\n\n${userText}`
|
|
|
|
|
: action.prompt;
|
2026-04-07 00:43:40 -07:00
|
|
|
editorRef.current?.setText(finalPrompt);
|
2026-03-29 02:54:48 +02:00
|
|
|
aui.composer().setText(finalPrompt);
|
2026-03-29 00:07:08 +02:00
|
|
|
setShowPromptPicker(false);
|
2026-03-28 23:20:10 +02:00
|
|
|
setActionQuery("");
|
2026-03-29 02:54:48 +02:00
|
|
|
},
|
2026-04-07 00:43:40 -07:00
|
|
|
[actionQuery, aui]
|
2026-03-29 02:54:48 +02:00
|
|
|
);
|
2026-03-29 00:45:11 +02:00
|
|
|
|
2026-03-29 02:54:48 +02:00
|
|
|
const handleQuickAskSelect = useCallback(
|
|
|
|
|
(action: { name: string; prompt: string; mode: "transform" | "explore" }) => {
|
|
|
|
|
if (!clipboardInitialText) return;
|
2026-04-07 00:43:40 -07:00
|
|
|
electronAPI?.setQuickAskMode(action.mode);
|
2026-03-29 02:54:48 +02:00
|
|
|
const finalPrompt = action.prompt.includes("{selection}")
|
|
|
|
|
? action.prompt.replace("{selection}", () => clipboardInitialText)
|
|
|
|
|
: `${action.prompt}\n\n${clipboardInitialText}`;
|
2026-04-07 00:43:40 -07:00
|
|
|
editorRef.current?.setText(finalPrompt);
|
2026-03-29 02:54:48 +02:00
|
|
|
aui.composer().setText(finalPrompt);
|
|
|
|
|
setShowPromptPicker(false);
|
|
|
|
|
setActionQuery("");
|
|
|
|
|
setClipboardInitialText(undefined);
|
2026-03-28 23:20:10 +02:00
|
|
|
},
|
2026-04-07 00:43:40 -07:00
|
|
|
[clipboardInitialText, electronAPI, aui]
|
2026-03-28 23:20:10 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Keyboard navigation for document/action picker (arrow keys, Enter, Escape)
|
2025-12-25 13:44:18 +05:30
|
|
|
const handleKeyDown = useCallback(
|
|
|
|
|
(e: React.KeyboardEvent) => {
|
2026-03-29 00:07:08 +02:00
|
|
|
if (showPromptPicker) {
|
2026-03-28 23:20:10 +02:00
|
|
|
if (e.key === "ArrowDown") {
|
|
|
|
|
e.preventDefault();
|
2026-03-29 00:07:08 +02:00
|
|
|
promptPickerRef.current?.moveDown();
|
2026-03-28 23:20:10 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "ArrowUp") {
|
|
|
|
|
e.preventDefault();
|
2026-03-29 00:07:08 +02:00
|
|
|
promptPickerRef.current?.moveUp();
|
2026-03-28 23:20:10 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "Enter") {
|
|
|
|
|
e.preventDefault();
|
2026-03-29 00:07:08 +02:00
|
|
|
promptPickerRef.current?.selectHighlighted();
|
2026-03-28 23:20:10 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "Escape") {
|
|
|
|
|
e.preventDefault();
|
2026-03-29 00:07:08 +02:00
|
|
|
setShowPromptPicker(false);
|
2026-03-28 23:20:10 +02:00
|
|
|
setActionQuery("");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-23 15:13:03 +05:30
|
|
|
if (showDocumentPopover) {
|
2025-12-25 13:44:18 +05:30
|
|
|
if (e.key === "ArrowDown") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
documentPickerRef.current?.moveDown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "ArrowUp") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
documentPickerRef.current?.moveUp();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "Enter") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
documentPickerRef.current?.selectHighlighted();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "Escape") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setShowDocumentPopover(false);
|
|
|
|
|
setMentionQuery("");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-23 15:13:03 +05:30
|
|
|
}
|
2025-12-25 13:44:18 +05:30
|
|
|
},
|
2026-03-29 00:07:08 +02:00
|
|
|
[showDocumentPopover, showPromptPicker]
|
2025-12-25 13:44:18 +05:30
|
|
|
);
|
2025-12-23 14:24:36 +05:30
|
|
|
|
2025-12-25 13:44:18 +05:30
|
|
|
const handleSubmit = useCallback(() => {
|
2026-03-29 02:45:59 -07:00
|
|
|
if (isThreadRunning || isBlockedByOtherUser) return;
|
2026-04-14 02:38:49 +05:30
|
|
|
if (showDocumentPopover || showPromptPicker) return;
|
|
|
|
|
|
|
|
|
|
if (clipboardInitialText) {
|
|
|
|
|
const userText = editorRef.current?.getText() ?? "";
|
|
|
|
|
const combined = userText ? `${userText}\n\n${clipboardInitialText}` : clipboardInitialText;
|
|
|
|
|
aui.composer().setText(combined);
|
|
|
|
|
setClipboardInitialText(undefined);
|
|
|
|
|
}
|
2026-03-29 02:45:59 -07:00
|
|
|
|
|
|
|
|
aui.composer().send();
|
|
|
|
|
editorRef.current?.clear();
|
|
|
|
|
setMentionedDocuments([]);
|
2025-12-25 19:32:18 +05:30
|
|
|
}, [
|
|
|
|
|
showDocumentPopover,
|
2026-03-29 00:07:08 +02:00
|
|
|
showPromptPicker,
|
2025-12-25 19:32:18 +05:30
|
|
|
isThreadRunning,
|
2026-01-20 18:39:50 +02:00
|
|
|
isBlockedByOtherUser,
|
2026-03-29 02:54:48 +02:00
|
|
|
clipboardInitialText,
|
2026-03-24 02:22:51 +05:30
|
|
|
aui,
|
2025-12-25 19:32:18 +05:30
|
|
|
setMentionedDocuments,
|
|
|
|
|
]);
|
2025-12-25 13:44:18 +05:30
|
|
|
|
|
|
|
|
const handleDocumentRemove = useCallback(
|
2026-01-13 06:14:58 +02:00
|
|
|
(docId: number, docType?: string) => {
|
2026-04-29 04:19:07 +05:30
|
|
|
setMentionedDocuments((prev) => {
|
|
|
|
|
if (!docType) {
|
|
|
|
|
// Defensive fallback: keep UI in sync even when chip type is unavailable.
|
|
|
|
|
return prev.filter((doc) => doc.id !== docId);
|
|
|
|
|
}
|
|
|
|
|
const removedKey = getMentionDocKey({ id: docId, document_type: docType });
|
|
|
|
|
return prev.filter((doc) => getMentionDocKey(doc) !== removedKey);
|
|
|
|
|
});
|
2025-12-25 13:44:18 +05:30
|
|
|
},
|
2026-03-06 23:33:51 +05:30
|
|
|
[setMentionedDocuments]
|
2025-12-25 13:44:18 +05:30
|
|
|
);
|
2025-12-22 23:17:48 +02:00
|
|
|
|
2025-12-25 13:44:18 +05:30
|
|
|
const handleDocumentsMention = useCallback(
|
2026-05-09 22:15:51 -07:00
|
|
|
(mentions: MentionedDocumentInfo[]) => {
|
2026-04-29 04:12:42 +05:30
|
|
|
const editorMentionedDocs = editorRef.current?.getMentionedDocuments() ?? [];
|
|
|
|
|
const editorDocKeys = new Set(editorMentionedDocs.map((doc) => getMentionDocKey(doc)));
|
2025-12-23 14:24:36 +05:30
|
|
|
|
2026-05-09 22:15:51 -07:00
|
|
|
for (const mention of mentions) {
|
|
|
|
|
const key = getMentionDocKey(mention);
|
2026-04-29 04:12:42 +05:30
|
|
|
if (editorDocKeys.has(key)) continue;
|
2026-05-09 22:15:51 -07:00
|
|
|
editorRef.current?.insertMentionChip(mention);
|
2025-12-22 23:17:48 +02:00
|
|
|
}
|
2025-12-24 07:06:35 +02:00
|
|
|
|
2025-12-25 13:44:18 +05:30
|
|
|
setMentionedDocuments((prev) => {
|
2026-04-29 04:12:42 +05:30
|
|
|
const existingKeySet = new Set(prev.map((d) => getMentionDocKey(d)));
|
2026-05-09 22:15:51 -07:00
|
|
|
const uniqueNew = mentions.filter((m) => !existingKeySet.has(getMentionDocKey(m)));
|
|
|
|
|
return [...prev, ...uniqueNew];
|
2025-12-25 13:44:18 +05:30
|
|
|
});
|
2025-12-23 14:24:36 +05:30
|
|
|
|
2025-12-25 13:44:18 +05:30
|
|
|
setMentionQuery("");
|
|
|
|
|
},
|
2026-04-29 04:12:42 +05:30
|
|
|
[setMentionedDocuments]
|
2025-12-25 13:44:18 +05:30
|
|
|
);
|
2025-12-22 23:17:48 +02:00
|
|
|
|
2026-04-28 17:50:21 +05:30
|
|
|
useEffect(() => {
|
2026-04-28 18:20:53 +05:30
|
|
|
const editor = editorRef.current;
|
2026-04-29 04:12:42 +05:30
|
|
|
const nextDocsMap = new Map(mentionedDocuments.map((doc) => [getMentionDocKey(doc), doc]));
|
|
|
|
|
const prevDocsMap = prevMentionedDocsRef.current;
|
2026-04-28 17:50:21 +05:30
|
|
|
|
2026-04-29 04:12:42 +05:30
|
|
|
if (!editor) {
|
|
|
|
|
prevMentionedDocsRef.current = nextDocsMap;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-28 17:50:21 +05:30
|
|
|
|
2026-04-29 04:12:42 +05:30
|
|
|
const editorKeys = new Set(editor.getMentionedDocuments().map(getMentionDocKey));
|
2026-04-28 17:50:21 +05:30
|
|
|
|
2026-04-29 04:12:42 +05:30
|
|
|
for (const [key, doc] of nextDocsMap) {
|
|
|
|
|
if (prevDocsMap.has(key) || editorKeys.has(key)) continue;
|
2026-05-09 22:15:51 -07:00
|
|
|
editor.insertMentionChip(doc, { removeTriggerText: false });
|
2026-04-28 17:50:21 +05:30
|
|
|
}
|
|
|
|
|
|
2026-04-29 04:12:42 +05:30
|
|
|
for (const [key, doc] of prevDocsMap) {
|
|
|
|
|
if (!nextDocsMap.has(key)) {
|
2026-04-28 18:20:53 +05:30
|
|
|
editor.removeDocumentChip(doc.id, doc.document_type);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-29 04:12:42 +05:30
|
|
|
|
|
|
|
|
prevMentionedDocsRef.current = nextDocsMap;
|
2026-04-28 18:20:53 +05:30
|
|
|
}, [mentionedDocuments]);
|
2026-04-28 17:50:21 +05:30
|
|
|
|
2025-12-19 16:42:58 +02:00
|
|
|
return (
|
2026-04-29 03:55:13 +05:30
|
|
|
<ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col gap-2 rounded-2xl">
|
2026-01-20 18:39:50 +02:00
|
|
|
<ChatSessionStatus
|
|
|
|
|
isAiResponding={isAiResponding}
|
|
|
|
|
respondingToUserId={respondingToUserId}
|
|
|
|
|
currentUserId={currentUser?.id ?? null}
|
|
|
|
|
members={members ?? []}
|
|
|
|
|
/>
|
2026-04-14 01:50:37 +05:30
|
|
|
{showDocumentPopover && (
|
|
|
|
|
<div className="absolute bottom-full left-0 z-[9999] mb-2">
|
|
|
|
|
<DocumentMentionPicker
|
|
|
|
|
ref={documentPickerRef}
|
|
|
|
|
searchSpaceId={Number(search_space_id)}
|
|
|
|
|
onSelectionChange={handleDocumentsMention}
|
|
|
|
|
onDone={() => {
|
|
|
|
|
setShowDocumentPopover(false);
|
|
|
|
|
setMentionQuery("");
|
|
|
|
|
}}
|
|
|
|
|
initialSelectedDocuments={mentionedDocuments}
|
|
|
|
|
externalSearch={mentionQuery}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{showPromptPicker && (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"absolute left-0 z-[9999]",
|
|
|
|
|
clipboardInitialText ? "top-full mt-2" : "bottom-full mb-2"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<PromptPicker
|
|
|
|
|
ref={promptPickerRef}
|
|
|
|
|
onSelect={clipboardInitialText ? handleQuickAskSelect : handleActionSelect}
|
|
|
|
|
onDone={() => {
|
|
|
|
|
setShowPromptPicker(false);
|
|
|
|
|
setActionQuery("");
|
|
|
|
|
}}
|
|
|
|
|
externalSearch={actionQuery}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-05-17 16:46:34 +05:30
|
|
|
<div className="flex w-full flex-col">
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"aui-composer-attachment-dropzone relative z-10 flex w-full flex-col overflow-hidden rounded-3xl border border-input bg-muted pt-2 shadow-sm shadow-black/5 outline-none transition-shadow dark:shadow-black/10",
|
|
|
|
|
connectToolsTrayVisible && "rounded-b-3xl shadow-none dark:shadow-none"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<PendingScreenImageStrip />
|
|
|
|
|
{clipboardInitialText && (
|
|
|
|
|
<ClipboardChip
|
|
|
|
|
text={clipboardInitialText}
|
|
|
|
|
onDismiss={() => setClipboardInitialText(undefined)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<div className="aui-composer-input-wrapper px-4 pt-3 pb-6">
|
|
|
|
|
<InlineMentionEditor
|
|
|
|
|
ref={editorRef}
|
|
|
|
|
placeholder={currentPlaceholder}
|
|
|
|
|
onMentionTrigger={handleMentionTrigger}
|
|
|
|
|
onMentionClose={handleMentionClose}
|
|
|
|
|
onActionTrigger={handleActionTrigger}
|
|
|
|
|
onActionClose={handleActionClose}
|
|
|
|
|
onChange={handleEditorChange}
|
|
|
|
|
onDocumentRemove={handleDocumentRemove}
|
|
|
|
|
onSubmit={handleSubmit}
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
className="min-h-[24px]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<ComposerAction isBlockedByOtherUser={isBlockedByOtherUser} />
|
|
|
|
|
<ConnectorIndicator showTrigger={false} />
|
2025-12-23 14:24:36 +05:30
|
|
|
</div>
|
2026-05-17 16:46:34 +05:30
|
|
|
<ConnectToolsBanner
|
|
|
|
|
isThreadEmpty={isThreadEmpty}
|
|
|
|
|
onVisibleChange={setConnectToolsTrayVisible}
|
|
|
|
|
/>
|
2026-02-09 16:46:54 -08:00
|
|
|
</div>
|
2025-12-19 16:42:58 +02:00
|
|
|
</ComposerPrimitive.Root>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-20 19:48:28 +02:00
|
|
|
interface ComposerActionProps {
|
|
|
|
|
isBlockedByOtherUser?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 04:46:48 +05:30
|
|
|
const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false }) => {
|
2026-02-09 16:46:54 -08:00
|
|
|
const mentionedDocuments = useAtomValue(mentionedDocumentsAtom);
|
2026-03-11 12:04:22 +05:30
|
|
|
const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom);
|
2026-03-10 17:36:26 -07:00
|
|
|
const [toolsPopoverOpen, setToolsPopoverOpen] = useState(false);
|
2026-03-11 12:04:22 +05:30
|
|
|
const isDesktop = useMediaQuery("(min-width: 640px)");
|
2026-03-15 16:27:33 +05:30
|
|
|
const { openDialog: openUploadDialog } = useDocumentUploadDialog();
|
2026-04-24 19:17:43 +02:00
|
|
|
const pendingScreenImages = useAtomValue(pendingUserImageDataUrlsAtom);
|
|
|
|
|
const setPendingScreenImages = useSetAtom(pendingUserImageDataUrlsAtom);
|
|
|
|
|
const electronAPI = useElectronAPI();
|
|
|
|
|
|
2026-03-24 02:22:51 +05:30
|
|
|
const isComposerTextEmpty = useAuiState(({ composer }) => {
|
2025-12-22 18:38:08 +05:30
|
|
|
const text = composer.text?.trim() || "";
|
|
|
|
|
return text.length === 0;
|
|
|
|
|
});
|
2026-04-24 19:17:43 +02:00
|
|
|
const isComposerEmpty =
|
|
|
|
|
isComposerTextEmpty && mentionedDocuments.length === 0 && pendingScreenImages.length === 0;
|
|
|
|
|
|
|
|
|
|
const handleScreenCapture = useCallback(async () => {
|
2026-04-27 18:49:43 +02:00
|
|
|
const url = electronAPI?.captureFullScreen
|
|
|
|
|
? await electronAPI.captureFullScreen()
|
|
|
|
|
: await captureDisplayToPngDataUrl();
|
2026-04-24 19:17:43 +02:00
|
|
|
if (url) setPendingScreenImages((prev) => [...prev, url]);
|
2026-04-27 18:49:43 +02:00
|
|
|
}, [electronAPI, setPendingScreenImages]);
|
2025-12-22 18:38:08 +05:30
|
|
|
|
2025-12-23 01:16:25 -08:00
|
|
|
const { data: userConfigs } = useAtomValue(newLLMConfigsAtom);
|
|
|
|
|
const { data: globalConfigs } = useAtomValue(globalNewLLMConfigsAtom);
|
|
|
|
|
const { data: preferences } = useAtomValue(llmPreferencesAtom);
|
|
|
|
|
|
2026-03-10 17:36:26 -07:00
|
|
|
const { data: agentTools } = useAtomValue(agentToolsAtom);
|
|
|
|
|
const disabledTools = useAtomValue(disabledToolsAtom);
|
2026-04-01 23:09:57 +00:00
|
|
|
const disabledToolsSet = useMemo(() => new Set(disabledTools), [disabledTools]);
|
2026-03-10 17:36:26 -07:00
|
|
|
const toggleTool = useSetAtom(toggleToolAtom);
|
2026-03-21 11:38:42 +05:30
|
|
|
const setDisabledTools = useSetAtom(disabledToolsAtom);
|
2026-03-10 17:36:26 -07:00
|
|
|
const hydrateDisabled = useSetAtom(hydrateDisabledToolsAtom);
|
2026-03-17 01:09:15 +05:30
|
|
|
|
2026-03-21 12:41:06 +05:30
|
|
|
const { data: connectors } = useAtomValue(connectorsAtom);
|
|
|
|
|
const connectedTypes = useMemo(
|
|
|
|
|
() => new Set<string>((connectors ?? []).map((c) => c.connector_type)),
|
|
|
|
|
[connectors]
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-21 11:38:42 +05:30
|
|
|
const toggleToolGroup = useCallback(
|
|
|
|
|
(toolNames: string[]) => {
|
2026-04-01 23:09:57 +00:00
|
|
|
const allDisabled = toolNames.every((name) => disabledToolsSet.has(name));
|
2026-03-21 11:38:42 +05:30
|
|
|
if (allDisabled) {
|
|
|
|
|
setDisabledTools((prev) => prev.filter((t) => !toolNames.includes(t)));
|
|
|
|
|
} else {
|
|
|
|
|
setDisabledTools((prev) => [...new Set([...prev, ...toolNames])]);
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-04-01 23:09:57 +00:00
|
|
|
[disabledToolsSet, setDisabledTools]
|
2026-03-21 11:38:42 +05:30
|
|
|
);
|
|
|
|
|
|
2026-03-17 01:09:15 +05:30
|
|
|
const hasWebSearchTool = agentTools?.some((t) => t.name === "web_search") ?? false;
|
2026-04-01 23:09:57 +00:00
|
|
|
const isWebSearchEnabled = hasWebSearchTool && !disabledToolsSet.has("web_search");
|
2026-03-17 01:09:15 +05:30
|
|
|
const filteredTools = useMemo(
|
|
|
|
|
() => agentTools?.filter((t) => t.name !== "web_search"),
|
|
|
|
|
[agentTools]
|
|
|
|
|
);
|
2026-03-17 15:18:58 +05:30
|
|
|
const groupedTools = useMemo(() => {
|
|
|
|
|
if (!filteredTools) return [];
|
|
|
|
|
const toolsByName = new Map(filteredTools.map((t) => [t.name, t]));
|
2026-03-21 11:38:42 +05:30
|
|
|
const result: { label: string; tools: typeof filteredTools; connectorIcon?: string }[] = [];
|
2026-03-17 15:18:58 +05:30
|
|
|
const placed = new Set<string>();
|
|
|
|
|
|
|
|
|
|
for (const group of TOOL_GROUPS) {
|
2026-03-21 12:41:06 +05:30
|
|
|
if (group.connectorIcon) {
|
|
|
|
|
const requiredTypes = CONNECTOR_ICON_TO_TYPES[group.connectorIcon];
|
|
|
|
|
const isConnected = requiredTypes?.some((t) => connectedTypes.has(t));
|
|
|
|
|
if (!isConnected) {
|
|
|
|
|
for (const name of group.tools) placed.add(name);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 15:18:58 +05:30
|
|
|
const matched = group.tools.flatMap((name) => {
|
|
|
|
|
const tool = toolsByName.get(name);
|
|
|
|
|
if (!tool) return [];
|
|
|
|
|
placed.add(name);
|
|
|
|
|
return [tool];
|
|
|
|
|
});
|
|
|
|
|
if (matched.length > 0) {
|
2026-03-21 11:38:42 +05:30
|
|
|
result.push({ label: group.label, tools: matched, connectorIcon: group.connectorIcon });
|
2026-03-17 15:18:58 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ungrouped = filteredTools.filter((t) => !placed.has(t.name));
|
|
|
|
|
if (ungrouped.length > 0) {
|
|
|
|
|
result.push({ label: "Other", tools: ungrouped });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
2026-03-21 12:41:06 +05:30
|
|
|
}, [filteredTools, connectedTypes]);
|
2026-03-17 15:18:58 +05:30
|
|
|
|
2026-03-10 17:36:26 -07:00
|
|
|
useEffect(() => {
|
|
|
|
|
hydrateDisabled();
|
|
|
|
|
}, [hydrateDisabled]);
|
|
|
|
|
|
2025-12-23 01:16:25 -08:00
|
|
|
const hasModelConfigured = useMemo(() => {
|
|
|
|
|
if (!preferences) return false;
|
|
|
|
|
const agentLlmId = preferences.agent_llm_id;
|
|
|
|
|
if (agentLlmId === null || agentLlmId === undefined) return false;
|
|
|
|
|
|
2026-01-29 15:28:31 -08:00
|
|
|
if (agentLlmId <= 0) {
|
2025-12-23 01:16:25 -08:00
|
|
|
return globalConfigs?.some((c) => c.id === agentLlmId) ?? false;
|
|
|
|
|
}
|
|
|
|
|
return userConfigs?.some((c) => c.id === agentLlmId) ?? false;
|
|
|
|
|
}, [preferences, globalConfigs, userConfigs]);
|
|
|
|
|
|
2026-03-07 04:46:48 +05:30
|
|
|
const isSendDisabled = isComposerEmpty || !hasModelConfigured || isBlockedByOtherUser;
|
2025-12-22 18:38:08 +05:30
|
|
|
|
2025-12-19 16:42:58 +02:00
|
|
|
return (
|
2026-05-14 23:22:32 +05:30
|
|
|
<div className="aui-composer-action-wrapper relative mx-3 mb-3 flex items-center justify-between">
|
2025-12-22 23:57:16 +05:30
|
|
|
<div className="flex items-center gap-1">
|
2026-03-15 16:27:33 +05:30
|
|
|
{!isDesktop ? (
|
|
|
|
|
<>
|
|
|
|
|
<DropdownMenu>
|
2026-03-15 16:39:56 +05:30
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
2026-05-17 02:57:00 +05:30
|
|
|
className="size-9 rounded-full p-1 font-semibold text-xs text-muted-foreground dark:border-muted-foreground/15 hover:bg-accent hover:text-accent-foreground"
|
2026-03-15 16:39:56 +05:30
|
|
|
aria-label="More actions"
|
|
|
|
|
data-joyride="connector-icon"
|
|
|
|
|
>
|
2026-05-17 02:57:00 +05:30
|
|
|
<Plus className="size-5" />
|
2026-03-15 16:39:56 +05:30
|
|
|
</Button>
|
|
|
|
|
</DropdownMenuTrigger>
|
2026-03-17 03:36:32 +05:30
|
|
|
<DropdownMenuContent side="bottom" align="start" sideOffset={8}>
|
2026-03-15 16:27:33 +05:30
|
|
|
<DropdownMenuItem onSelect={() => setToolsPopoverOpen(true)}>
|
2026-03-15 16:39:56 +05:30
|
|
|
<Settings2 className="size-4" />
|
2026-03-15 16:27:33 +05:30
|
|
|
Manage Tools
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuItem onSelect={() => openUploadDialog()}>
|
|
|
|
|
<Upload className="size-4" />
|
|
|
|
|
Upload Files
|
|
|
|
|
</DropdownMenuItem>
|
2026-05-17 02:57:00 +05:30
|
|
|
{hasWebSearchTool && (
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onSelect={(event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
toggleTool("web_search");
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Globe className="size-4" />
|
|
|
|
|
<span className="flex-1">Web Search</span>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={isWebSearchEnabled}
|
|
|
|
|
tabIndex={-1}
|
|
|
|
|
className="pointer-events-none h-4 w-7 shrink-0 border [&>span]:h-3 [&>span]:w-3 [&>span[data-state=checked]]:translate-x-3"
|
|
|
|
|
/>
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
2026-05-03 22:52:43 +05:30
|
|
|
<DropdownMenuItem onSelect={() => setConnectorDialogOpen(true)}>
|
|
|
|
|
<Unplug className="size-4" />
|
|
|
|
|
Manage Connectors
|
|
|
|
|
</DropdownMenuItem>
|
2026-03-15 16:27:33 +05:30
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
2026-05-03 22:52:43 +05:30
|
|
|
<Drawer
|
|
|
|
|
open={toolsPopoverOpen}
|
|
|
|
|
onOpenChange={setToolsPopoverOpen}
|
|
|
|
|
shouldScaleBackground={false}
|
|
|
|
|
>
|
|
|
|
|
<DrawerContent className="h-[85vh] max-h-[85vh] z-80" overlayClassName="z-80">
|
2026-03-15 16:39:56 +05:30
|
|
|
<DrawerHandle />
|
2026-05-03 22:52:43 +05:30
|
|
|
<DrawerHeader className="px-4 pb-3 pt-2">
|
|
|
|
|
<DrawerTitle className="flex items-center justify-center gap-2 text-base font-semibold">
|
|
|
|
|
Manage Tools
|
|
|
|
|
</DrawerTitle>
|
|
|
|
|
</DrawerHeader>
|
2026-05-17 00:57:35 +05:30
|
|
|
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-thin pb-6">
|
2026-03-21 13:20:13 +05:30
|
|
|
{groupedTools
|
|
|
|
|
.filter((g) => !g.connectorIcon)
|
|
|
|
|
.map((group) => (
|
|
|
|
|
<div key={group.label}>
|
|
|
|
|
<div className="px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none">
|
|
|
|
|
{group.label}
|
|
|
|
|
</div>
|
|
|
|
|
{group.tools.map((tool) => {
|
2026-04-01 23:09:57 +00:00
|
|
|
const isDisabled = disabledToolsSet.has(tool.name);
|
2026-03-21 13:20:13 +05:30
|
|
|
const ToolIcon = getToolIcon(tool.name);
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={tool.name}
|
2026-05-13 23:53:09 +05:30
|
|
|
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-accent hover:text-accent-foreground transition-colors"
|
2026-03-21 13:20:13 +05:30
|
|
|
>
|
|
|
|
|
<ToolIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
|
|
|
<span className="flex-1 min-w-0 text-sm font-medium truncate">
|
|
|
|
|
{formatToolName(tool.name)}
|
|
|
|
|
</span>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={!isDisabled}
|
|
|
|
|
onCheckedChange={() => toggleTool(tool.name)}
|
|
|
|
|
className="shrink-0"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-03-15 16:39:56 +05:30
|
|
|
</div>
|
2026-03-21 13:20:13 +05:30
|
|
|
))}
|
2026-03-21 11:38:42 +05:30
|
|
|
{groupedTools.some((g) => g.connectorIcon) && (
|
|
|
|
|
<div>
|
2026-03-17 15:19:16 +05:30
|
|
|
<div className="px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none">
|
2026-03-21 11:38:42 +05:30
|
|
|
Connector Actions
|
2026-03-17 15:19:16 +05:30
|
|
|
</div>
|
2026-03-21 13:20:13 +05:30
|
|
|
{groupedTools
|
|
|
|
|
.filter((g) => g.connectorIcon)
|
|
|
|
|
.map((group) => {
|
|
|
|
|
const iconKey = group.connectorIcon ?? "";
|
|
|
|
|
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
|
|
|
|
|
const toolNames = group.tools.map((t) => t.name);
|
2026-04-01 23:09:57 +00:00
|
|
|
const allDisabled = toolNames.every((n) => disabledToolsSet.has(n));
|
2026-03-21 13:20:13 +05:30
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={group.label}
|
2026-05-13 23:53:09 +05:30
|
|
|
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-accent hover:text-accent-foreground transition-colors"
|
2026-03-21 13:20:13 +05:30
|
|
|
>
|
|
|
|
|
{iconInfo ? (
|
|
|
|
|
<Image
|
|
|
|
|
src={iconInfo.src}
|
|
|
|
|
alt={iconInfo.alt}
|
|
|
|
|
width={18}
|
|
|
|
|
height={18}
|
|
|
|
|
className="size-[18px] shrink-0 select-none pointer-events-none"
|
|
|
|
|
draggable={false}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<Wrench className="size-4 shrink-0 text-muted-foreground" />
|
|
|
|
|
)}
|
|
|
|
|
<span className="flex-1 min-w-0 text-sm font-medium truncate">
|
|
|
|
|
{group.label}
|
|
|
|
|
</span>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={!allDisabled}
|
|
|
|
|
onCheckedChange={() => toggleToolGroup(toolNames)}
|
|
|
|
|
className="shrink-0"
|
2026-03-21 11:38:42 +05:30
|
|
|
/>
|
2026-03-21 13:20:13 +05:30
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-03-15 16:27:33 +05:30
|
|
|
</div>
|
2026-03-21 11:38:42 +05:30
|
|
|
)}
|
2026-04-09 18:10:34 +05:30
|
|
|
{!filteredTools?.length && (
|
|
|
|
|
<div className="px-4 pt-3 pb-2">
|
|
|
|
|
<Skeleton className="h-3 w-16 mb-2" />
|
|
|
|
|
{["t1", "t2", "t3", "t4"].map((k) => (
|
|
|
|
|
<div key={k} className="flex items-center gap-3 py-2">
|
|
|
|
|
<Skeleton className="size-4 rounded shrink-0" />
|
|
|
|
|
<Skeleton className="h-3.5 flex-1" />
|
|
|
|
|
<Skeleton className="h-5 w-9 rounded-full shrink-0" />
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
<Skeleton className="h-3 w-24 mt-3 mb-2" />
|
|
|
|
|
{["c1", "c2", "c3"].map((k) => (
|
|
|
|
|
<div key={k} className="flex items-center gap-3 py-2">
|
|
|
|
|
<Skeleton className="size-4 rounded shrink-0" />
|
|
|
|
|
<Skeleton className="h-3.5 flex-1" />
|
|
|
|
|
<Skeleton className="h-5 w-9 rounded-full shrink-0" />
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</DrawerContent>
|
|
|
|
|
</Drawer>
|
2026-03-15 16:27:33 +05:30
|
|
|
</>
|
|
|
|
|
) : (
|
2026-05-17 00:57:35 +05:30
|
|
|
<DropdownMenu onOpenChange={(open) => !open && setToolsPopoverOpen(false)}>
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
2026-03-15 16:27:33 +05:30
|
|
|
<TooltipIconButton
|
2026-05-17 00:57:35 +05:30
|
|
|
tooltip="More actions"
|
2026-03-15 16:27:33 +05:30
|
|
|
side="bottom"
|
2026-03-17 15:09:24 +05:30
|
|
|
disableTooltip={toolsPopoverOpen}
|
2026-03-15 16:27:33 +05:30
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
2026-05-17 02:57:00 +05:30
|
|
|
className="size-9 rounded-full p-1 font-semibold text-xs text-muted-foreground dark:border-muted-foreground/15 hover:bg-accent hover:text-accent-foreground"
|
2026-05-17 00:57:35 +05:30
|
|
|
aria-label="More actions"
|
2026-03-15 16:27:33 +05:30
|
|
|
data-joyride="connector-icon"
|
|
|
|
|
>
|
2026-05-17 02:57:00 +05:30
|
|
|
<Plus className="size-5" />
|
2026-03-15 16:27:33 +05:30
|
|
|
</TooltipIconButton>
|
2026-05-17 00:57:35 +05:30
|
|
|
</DropdownMenuTrigger>
|
2026-05-17 02:57:00 +05:30
|
|
|
<DropdownMenuContent
|
|
|
|
|
className="w-48"
|
|
|
|
|
side="bottom"
|
|
|
|
|
align="start"
|
|
|
|
|
sideOffset={8}
|
|
|
|
|
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
|
|
|
>
|
2026-05-17 00:57:35 +05:30
|
|
|
<DropdownMenuItem onSelect={() => openUploadDialog()}>
|
|
|
|
|
<Upload className="h-4 w-4" />
|
|
|
|
|
Upload Files
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuItem onSelect={() => void handleScreenCapture()}>
|
|
|
|
|
<Camera className="h-4 w-4" />
|
|
|
|
|
Take a screenshot
|
|
|
|
|
</DropdownMenuItem>
|
2026-05-17 02:57:00 +05:30
|
|
|
{hasWebSearchTool && (
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onSelect={(event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
toggleTool("web_search");
|
|
|
|
|
}}
|
|
|
|
|
className={cn(
|
|
|
|
|
"hover:bg-accent hover:text-accent-foreground",
|
|
|
|
|
isWebSearchEnabled && "text-primary"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Globe className="h-4 w-4" />
|
|
|
|
|
<span className="flex-1 min-w-0 truncate">Web Search</span>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={isWebSearchEnabled}
|
|
|
|
|
tabIndex={-1}
|
|
|
|
|
className="pointer-events-none h-4 w-7 shrink-0 border [&>span]:h-3 [&>span]:w-3 [&>span[data-state=checked]]:translate-x-3"
|
|
|
|
|
/>
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
2026-05-17 00:57:35 +05:30
|
|
|
<DropdownMenuSub open={toolsPopoverOpen} onOpenChange={setToolsPopoverOpen}>
|
|
|
|
|
<DropdownMenuSubTrigger>
|
|
|
|
|
<Settings2 className="h-4 w-4" />
|
|
|
|
|
Manage Tools
|
|
|
|
|
</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuPortal>
|
|
|
|
|
<DropdownMenuSubContent
|
|
|
|
|
alignOffset={-192}
|
|
|
|
|
collisionPadding={8}
|
|
|
|
|
className="w-60 h-56 gap-1 overflow-y-auto overscroll-none"
|
|
|
|
|
>
|
2026-03-21 13:20:13 +05:30
|
|
|
{groupedTools
|
2026-05-17 00:57:35 +05:30
|
|
|
.filter((g) => !g.connectorIcon)
|
|
|
|
|
.map((group) => (
|
|
|
|
|
<div key={group.label}>
|
|
|
|
|
<div className="px-2 pt-1.5 pb-0.5 text-[10px] text-muted-foreground/80 font-normal select-none">
|
|
|
|
|
{group.label}
|
2026-03-21 13:20:13 +05:30
|
|
|
</div>
|
2026-05-17 00:57:35 +05:30
|
|
|
{group.tools.map((tool) => {
|
|
|
|
|
const isDisabled = disabledToolsSet.has(tool.name);
|
|
|
|
|
const ToolIcon = getToolIcon(tool.name);
|
|
|
|
|
return (
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
key={tool.name}
|
|
|
|
|
onSelect={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
toggleTool(tool.name);
|
|
|
|
|
}}
|
|
|
|
|
className={cn(
|
|
|
|
|
"mb-1 last:mb-0 transition-all",
|
|
|
|
|
"hover:bg-accent hover:text-accent-foreground",
|
|
|
|
|
!isDisabled && "text-primary"
|
2026-04-03 17:28:12 +05:30
|
|
|
)}
|
2026-05-17 00:57:35 +05:30
|
|
|
>
|
|
|
|
|
<ToolIcon className="h-4 w-4" />
|
|
|
|
|
<span className="flex-1 min-w-0 truncate">
|
|
|
|
|
{formatToolName(tool.name)}
|
|
|
|
|
</span>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={!isDisabled}
|
|
|
|
|
tabIndex={-1}
|
|
|
|
|
className="pointer-events-none shrink-0 scale-[0.6]"
|
|
|
|
|
/>
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
{groupedTools.some((g) => g.connectorIcon) && (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="px-2 pt-1.5 pb-0.5 text-[10px] text-muted-foreground/80 font-normal select-none">
|
|
|
|
|
Connector Actions
|
|
|
|
|
</div>
|
|
|
|
|
{groupedTools
|
|
|
|
|
.filter((g) => g.connectorIcon)
|
|
|
|
|
.map((group) => {
|
|
|
|
|
const iconKey = group.connectorIcon ?? "";
|
|
|
|
|
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
|
|
|
|
|
const toolNames = group.tools.map((t) => t.name);
|
|
|
|
|
const allDisabled = toolNames.every((n) => disabledToolsSet.has(n));
|
|
|
|
|
return (
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
key={group.label}
|
|
|
|
|
onSelect={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
toggleToolGroup(toolNames);
|
|
|
|
|
}}
|
|
|
|
|
className={cn(
|
|
|
|
|
"mb-1 last:mb-0 transition-all",
|
|
|
|
|
"hover:bg-accent hover:text-accent-foreground",
|
|
|
|
|
!allDisabled && "text-primary"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{iconInfo ? (
|
|
|
|
|
<Image
|
|
|
|
|
src={iconInfo.src}
|
|
|
|
|
alt={iconInfo.alt}
|
|
|
|
|
width={16}
|
|
|
|
|
height={16}
|
|
|
|
|
className="h-4 w-4 shrink-0 select-none pointer-events-none"
|
|
|
|
|
draggable={false}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<Wrench className="h-4 w-4" />
|
|
|
|
|
)}
|
|
|
|
|
<span className="flex-1 min-w-0 truncate">{group.label}</span>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={!allDisabled}
|
|
|
|
|
tabIndex={-1}
|
|
|
|
|
className="pointer-events-none shrink-0 scale-[0.6]"
|
|
|
|
|
/>
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-04-09 00:31:36 +05:30
|
|
|
</div>
|
2026-05-17 00:57:35 +05:30
|
|
|
)}
|
|
|
|
|
{!filteredTools?.length && (
|
|
|
|
|
<div className="px-2 pt-1.5 pb-1">
|
|
|
|
|
<Skeleton className="h-2 w-12 mb-1.5" />
|
|
|
|
|
{["dt1", "dt2", "dt3", "dt4"].map((k) => (
|
|
|
|
|
<div key={k} className="flex items-center gap-2 py-1">
|
|
|
|
|
<Skeleton className="h-4 w-4 rounded shrink-0" />
|
|
|
|
|
<Skeleton className="h-3 flex-1" />
|
|
|
|
|
<Skeleton className="h-4 w-8 rounded-full shrink-0" />
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2026-04-09 00:31:36 +05:30
|
|
|
</div>
|
2026-05-17 00:57:35 +05:30
|
|
|
)}
|
|
|
|
|
</DropdownMenuSubContent>
|
|
|
|
|
</DropdownMenuPortal>
|
|
|
|
|
</DropdownMenuSub>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
2026-03-11 12:04:22 +05:30
|
|
|
)}
|
2025-12-22 23:57:16 +05:30
|
|
|
</div>
|
2026-03-06 14:40:10 +05:30
|
|
|
{!hasModelConfigured && (
|
2025-12-23 01:16:25 -08:00
|
|
|
<div className="flex items-center gap-1.5 text-amber-600 dark:text-amber-400 text-xs">
|
|
|
|
|
<AlertCircle className="size-3" />
|
|
|
|
|
<span>Select a model</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-03-06 15:59:45 +05:30
|
|
|
<div className="flex items-center gap-2">
|
2026-03-24 02:22:51 +05:30
|
|
|
<AuiIf condition={({ thread }) => !thread.isRunning}>
|
2026-03-07 04:46:48 +05:30
|
|
|
<ComposerPrimitive.Send asChild disabled={isSendDisabled}>
|
|
|
|
|
<TooltipIconButton
|
|
|
|
|
tooltip={
|
|
|
|
|
isBlockedByOtherUser
|
|
|
|
|
? "Wait for AI to finish responding"
|
|
|
|
|
: !hasModelConfigured
|
|
|
|
|
? "Please select a model from the header to start chatting"
|
|
|
|
|
: isComposerEmpty
|
2026-04-24 19:17:43 +02:00
|
|
|
? "Enter a message or add a screenshot to send"
|
2026-03-07 04:46:48 +05:30
|
|
|
: "Send message"
|
|
|
|
|
}
|
|
|
|
|
side="bottom"
|
|
|
|
|
type="submit"
|
|
|
|
|
variant="default"
|
|
|
|
|
size="icon"
|
|
|
|
|
className={cn(
|
2026-05-17 02:57:00 +05:30
|
|
|
"aui-composer-send size-9 rounded-full",
|
2026-03-07 04:46:48 +05:30
|
|
|
isSendDisabled && "cursor-not-allowed opacity-50"
|
|
|
|
|
)}
|
|
|
|
|
aria-label="Send message"
|
|
|
|
|
disabled={isSendDisabled}
|
|
|
|
|
>
|
2026-05-17 02:57:00 +05:30
|
|
|
<ArrowUpIcon className="aui-composer-send-icon size-5" />
|
2026-03-07 04:46:48 +05:30
|
|
|
</TooltipIconButton>
|
|
|
|
|
</ComposerPrimitive.Send>
|
2026-03-24 02:22:51 +05:30
|
|
|
</AuiIf>
|
2026-03-07 04:46:48 +05:30
|
|
|
|
2026-03-24 02:22:51 +05:30
|
|
|
<AuiIf condition={({ thread }) => thread.isRunning}>
|
2026-03-07 04:46:48 +05:30
|
|
|
<ComposerPrimitive.Cancel asChild>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="default"
|
|
|
|
|
size="icon"
|
2026-05-17 02:57:00 +05:30
|
|
|
className="aui-composer-cancel size-9 rounded-full"
|
2026-03-07 04:46:48 +05:30
|
|
|
aria-label="Stop generating"
|
|
|
|
|
>
|
2026-05-17 02:57:00 +05:30
|
|
|
<SquareIcon className="aui-composer-cancel-icon size-3.5 fill-current" />
|
2026-03-07 04:46:48 +05:30
|
|
|
</Button>
|
|
|
|
|
</ComposerPrimitive.Cancel>
|
2026-03-24 02:22:51 +05:30
|
|
|
</AuiIf>
|
2026-03-06 15:59:45 +05:30
|
|
|
</div>
|
2025-12-19 16:42:58 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-29 07:40:11 -07:00
|
|
|
/**
|
|
|
|
|
* Friendly tool name for display in the chat UI. Delegates to the
|
|
|
|
|
* shared map in ``contracts/enums/toolIcons`` so unix-style identifiers
|
|
|
|
|
* (``rm``, ``ls``, ``grep`` …) and snake_cased function names render as
|
|
|
|
|
* plain English (e.g. "Delete file", "List files", "Search in files").
|
|
|
|
|
*/
|
2026-03-10 17:36:26 -07:00
|
|
|
function formatToolName(name: string): string {
|
2026-04-29 07:40:11 -07:00
|
|
|
return getToolDisplayName(name);
|
2026-03-10 17:36:26 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-21 11:38:42 +05:30
|
|
|
interface ToolGroup {
|
|
|
|
|
label: string;
|
|
|
|
|
tools: string[];
|
|
|
|
|
connectorIcon?: string;
|
|
|
|
|
tooltip?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const TOOL_GROUPS: ToolGroup[] = [
|
2026-03-17 15:18:58 +05:30
|
|
|
{
|
|
|
|
|
label: "Research",
|
2026-03-28 16:39:46 -07:00
|
|
|
tools: ["search_surfsense_docs", "scrape_webpage"],
|
2026-03-17 15:18:58 +05:30
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Generate",
|
2026-03-24 16:28:11 +05:30
|
|
|
tools: ["generate_podcast", "generate_video_presentation", "generate_report", "generate_image"],
|
2026-03-17 15:18:58 +05:30
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Memory",
|
2026-04-09 00:02:14 +05:30
|
|
|
tools: ["update_memory"],
|
2026-03-17 15:18:58 +05:30
|
|
|
},
|
2026-03-21 11:38:42 +05:30
|
|
|
{
|
|
|
|
|
label: "Gmail",
|
|
|
|
|
tools: ["create_gmail_draft", "update_gmail_draft", "send_gmail_email", "trash_gmail_email"],
|
|
|
|
|
connectorIcon: "gmail",
|
2026-03-31 17:31:54 +05:30
|
|
|
tooltip: "Create drafts, update drafts, send emails, and trash emails in Gmail",
|
2026-03-21 11:38:42 +05:30
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Google Calendar",
|
|
|
|
|
tools: ["create_calendar_event", "update_calendar_event", "delete_calendar_event"],
|
|
|
|
|
connectorIcon: "google_calendar",
|
2026-03-31 17:31:54 +05:30
|
|
|
tooltip: "Create, update, and delete events in Google Calendar",
|
2026-03-21 11:38:42 +05:30
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Google Drive",
|
|
|
|
|
tools: ["create_google_drive_file", "delete_google_drive_file"],
|
|
|
|
|
connectorIcon: "google_drive",
|
2026-03-31 17:31:54 +05:30
|
|
|
tooltip: "Create and delete files in Google Drive",
|
2026-03-21 11:38:42 +05:30
|
|
|
},
|
2026-03-28 17:00:52 +05:30
|
|
|
{
|
|
|
|
|
label: "OneDrive",
|
|
|
|
|
tools: ["create_onedrive_file", "delete_onedrive_file"],
|
|
|
|
|
connectorIcon: "onedrive",
|
2026-03-31 17:31:54 +05:30
|
|
|
tooltip: "Create and delete files in OneDrive",
|
2026-03-28 17:00:52 +05:30
|
|
|
},
|
2026-03-30 22:37:19 +05:30
|
|
|
{
|
|
|
|
|
label: "Dropbox",
|
|
|
|
|
tools: ["create_dropbox_file", "delete_dropbox_file"],
|
|
|
|
|
connectorIcon: "dropbox",
|
2026-03-31 17:31:54 +05:30
|
|
|
tooltip: "Create and delete files in Dropbox",
|
2026-03-30 22:37:19 +05:30
|
|
|
},
|
2026-03-21 11:38:42 +05:30
|
|
|
{
|
|
|
|
|
label: "Notion",
|
|
|
|
|
tools: ["create_notion_page", "update_notion_page", "delete_notion_page"],
|
|
|
|
|
connectorIcon: "notion",
|
2026-03-31 17:31:54 +05:30
|
|
|
tooltip: "Create, update, and delete pages in Notion",
|
2026-03-21 11:38:42 +05:30
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Linear",
|
|
|
|
|
tools: ["create_linear_issue", "update_linear_issue", "delete_linear_issue"],
|
|
|
|
|
connectorIcon: "linear",
|
2026-03-31 17:31:54 +05:30
|
|
|
tooltip: "Create, update, and delete issues in Linear",
|
2026-03-21 11:38:42 +05:30
|
|
|
},
|
2026-03-21 12:41:06 +05:30
|
|
|
{
|
|
|
|
|
label: "Jira",
|
|
|
|
|
tools: ["create_jira_issue", "update_jira_issue", "delete_jira_issue"],
|
|
|
|
|
connectorIcon: "jira",
|
2026-03-31 17:31:54 +05:30
|
|
|
tooltip: "Create, update, and delete issues in Jira",
|
2026-03-21 12:41:06 +05:30
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Confluence",
|
|
|
|
|
tools: ["create_confluence_page", "update_confluence_page", "delete_confluence_page"],
|
|
|
|
|
connectorIcon: "confluence",
|
2026-03-31 17:31:54 +05:30
|
|
|
tooltip: "Create, update, and delete pages in Confluence",
|
2026-03-21 12:41:06 +05:30
|
|
|
},
|
2026-03-17 15:18:58 +05:30
|
|
|
];
|
|
|
|
|
|
2025-12-19 16:42:58 +02:00
|
|
|
const EditComposer: FC = () => {
|
|
|
|
|
return (
|
|
|
|
|
<MessagePrimitive.Root className="aui-edit-composer-wrapper mx-auto flex w-full max-w-(--thread-max-width) flex-col px-2 py-3">
|
|
|
|
|
<ComposerPrimitive.Root className="aui-edit-composer-root ml-auto flex w-full max-w-[85%] flex-col rounded-2xl bg-muted">
|
|
|
|
|
<ComposerPrimitive.Input
|
|
|
|
|
className="aui-edit-composer-input min-h-14 w-full resize-none bg-transparent p-4 text-foreground text-sm outline-none"
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
<div className="aui-edit-composer-footer mx-3 mb-3 flex items-center gap-2 self-end">
|
|
|
|
|
<ComposerPrimitive.Cancel asChild>
|
|
|
|
|
<Button variant="ghost" size="sm">
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
</ComposerPrimitive.Cancel>
|
|
|
|
|
<ComposerPrimitive.Send asChild>
|
|
|
|
|
<Button size="sm">Update</Button>
|
|
|
|
|
</ComposerPrimitive.Send>
|
|
|
|
|
</div>
|
|
|
|
|
</ComposerPrimitive.Root>
|
|
|
|
|
</MessagePrimitive.Root>
|
|
|
|
|
);
|
|
|
|
|
};
|