"use client"; import { appStore } from "@/app/store"; import { AllowedMCPServer, MCPServerInfo } from "app-types/mcp"; import { cn, objectFlow } from "lib/utils"; import { ArrowUpRightIcon, AtSign, ChartColumn, ChevronRight, CodeIcon, GlobeIcon, HardDriveUploadIcon, ImagesIcon, InfoIcon, Loader, MessageCircle, MousePointer2, Package, Plus, ShieldAlertIcon, Waypoints, Wrench, WrenchIcon, X, } from "lucide-react"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { Badge } from "ui/badge"; import { Button } from "ui/button"; import { Checkbox } from "ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "ui/dropdown-menu"; import { Input } from "ui/input"; import { MCPIcon } from "ui/mcp-icon"; import { useTranslations } from "next-intl"; import { Switch } from "ui/switch"; import { useShallow } from "zustand/shallow"; import { useMcpList } from "@/hooks/queries/use-mcp-list"; import { useWorkflowToolList } from "@/hooks/queries/use-workflow-tool-list"; import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar"; import { WorkflowSummary } from "app-types/workflow"; import { WorkflowGreeting } from "./workflow/workflow-greeting"; import { AppDefaultToolkit } from "lib/ai/tools"; import { ChatMention } from "app-types/chat"; import { CountAnimation } from "ui/count-animation"; import { Separator } from "ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip"; import { AgentSummary } from "app-types/agent"; import { authClient } from "auth/client"; import { Alert, AlertDescription, AlertTitle } from "ui/alert"; import { safe } from "ts-safe"; import { mutate } from "swr"; import { handleErrorWithToast } from "ui/shared-toast"; import { useAgents } from "@/hooks/queries/use-agents"; import { redriectMcpOauth } from "lib/ai/mcp/oauth-redirect"; import { GeminiIcon } from "ui/gemini-icon"; import { useChatModels } from "@/hooks/queries/use-chat-models"; import { OpenAIIcon } from "ui/openai-icon"; interface ToolSelectDropdownProps { align?: "start" | "end" | "center"; side?: "left" | "right" | "top" | "bottom"; disabled?: boolean; mentions?: ChatMention[]; onSelectWorkflow?: (workflow: WorkflowSummary) => void; onSelectAgent?: (agent: AgentSummary) => void; onGenerateImage?: (provider?: "google" | "openai") => void; className?: string; } const calculateToolCount = ( allowedMcpServers: Record, mcpList: (MCPServerInfo & { id: string })[], ) => { return mcpList.reduce((acc, server) => { const count = allowedMcpServers[server.id]?.tools?.length; return acc + count; }, 0); }; export function ToolSelectDropdown({ align, side, onSelectWorkflow, onSelectAgent, onGenerateImage, mentions, className, }: ToolSelectDropdownProps) { const [open, setOpen] = useState(false); const [toolChoice, allowedAppDefaultToolkit, allowedMcpServers, mcpList] = appStore( useShallow((state) => [ state.toolChoice, state.allowedAppDefaultToolkit, state.allowedMcpServers, state.mcpList, ]), ); const t = useTranslations("Chat.Tool"); const { isLoading } = useMcpList(); const { data: providers } = useChatModels(); const [globalModel] = appStore(useShallow((state) => [state.chatModel])); const modelInfo = useMemo(() => { const provider = providers?.find( (provider) => provider.provider === globalModel?.provider, ); const model = provider?.models.find( (model) => model.name === globalModel?.model, ); return model; }, [providers, globalModel]); useWorkflowToolList({ refreshInterval: 1000 * 60 * 5, }); const agentMention = useMemo(() => { return mentions?.find((m) => m.type === "agent"); }, [mentions]); const bindingTools = useMemo(() => { if (mentions?.length) { return mentions.map((m) => m.name); } if (toolChoice == "none") return []; const translate = t.raw("defaultToolKit"); const defaultTools = Object.values(AppDefaultToolkit) .filter((t) => allowedAppDefaultToolkit?.includes(t)) .map((t) => translate[t]); const mcpIds = mcpList.map((v) => v.id); const mcpTools = Object.values( objectFlow(allowedMcpServers ?? {}).filter((_, id) => mcpIds.includes(id), ), ) .map((v) => v.tools) .flat(); return [...defaultTools, ...mcpTools]; }, [ mentions, allowedAppDefaultToolkit, allowedMcpServers, toolChoice, mcpList, ]); const triggerButton = useMemo(() => { return ( ); }, [mentions?.length, bindingTools.length, isLoading, open]); useEffect(() => { if (bindingTools.length > 128) { toast(t("tooManyToolsSelected")); } }, [bindingTools.length > 128]); return (
{triggerButton}
{t("toolsSetup")}

{t("toolsSetupDescription")}

); } function ToolPresets() { const [ appStoreMutate, presets, allowedMcpServers, allowedAppDefaultToolkit, mcpList, ] = appStore( useShallow((state) => [ state.mutate, state.toolPresets, state.allowedMcpServers, state.allowedAppDefaultToolkit, state.mcpList, ]), ); const [open, setOpen] = useState(false); const [presetName, setPresetName] = useState(""); const t = useTranslations(); const presetWithToolCount = useMemo(() => { return presets.map((preset) => ({ ...preset, toolCount: calculateToolCount(preset.allowedMcpServers ?? {}, mcpList), })); }, [presets, mcpList]); const addPreset = useCallback( (name: string) => { if (name.trim() === "") { toast.error(t("Chat.Tool.presetNameCannotBeEmpty")); return; } if (presets.find((p) => p.name === name)) { toast.error(t("Chat.Tool.presetNameAlreadyExists")); return; } appStoreMutate((prev) => { return { toolPresets: [ ...prev.toolPresets, { name, allowedMcpServers, allowedAppDefaultToolkit }, ], }; }); setPresetName(""); setOpen(false); toast.success(t("Chat.Tool.presetSaved")); }, [allowedMcpServers, allowedAppDefaultToolkit, presets], ); const deletePreset = useCallback((index: number) => { appStoreMutate((prev) => { return { toolPresets: prev.toolPresets.filter((_, i) => i !== index), }; }); }, []); const applyPreset = useCallback((preset: (typeof presets)[number]) => { appStoreMutate({ allowedMcpServers: preset.allowedMcpServers, allowedAppDefaultToolkit: preset.allowedAppDefaultToolkit, }); }, []); return ( {t("Chat.Tool.preset")} {t("Chat.Tool.toolPresets")}
{t("Chat.Tool.saveAsPreset")} {t("Chat.Tool.saveAsPresetDescription")} { if (e.key === "Enter" && !e.nativeEvent.isComposing) { addPreset(presetName); } }} onChange={(e) => setPresetName(e.target.value)} /> {presets.length === 0 ? (

{t("Chat.Tool.noPresetsAvailableYet")}

{t("Chat.Tool.clickSaveAsPresetToGetStarted")}

) : ( presetWithToolCount.map((preset, index) => { return ( { applyPreset(preset); }} key={preset.name} className="flex items-center gap-2 cursor-pointer" > {preset.toolCount} {preset.name}
{ e.preventDefault(); deletePreset(index); }} >
); }) )} ); } function WorkflowToolSelector({ onSelectWorkflow, }: { onSelectWorkflow?: (workflow: WorkflowSummary) => void; }) { const t = useTranslations(); const workflowToolList = appStore((state) => state.workflowToolList); const { data: session } = authClient.useSession(); const currentUserId = session?.user?.id; // Separate user's workflows from shared workflows const myWorkflows = workflowToolList.filter( (w) => w.userId === currentUserId, ); const sharedWorkflows = workflowToolList.filter( (w) => w.userId !== currentUserId, ); return ( {t("Workflow.title")} {myWorkflows.length === 0 && sharedWorkflows.length === 0 ? (

{t("Workflow.noTools")}

workflow greeting
) : ( <> {/* My Workflows */} {myWorkflows.map((workflow) => ( onSelectWorkflow?.(workflow)} > {workflow.icon && workflow.icon.type === "emoji" ? (
{workflow.name.slice(0, 1)}
) : null} {workflow.name}
))} {myWorkflows.length > 0 && sharedWorkflows.length > 0 && ( )} {/* Shared Workflows */} {sharedWorkflows.map((workflow) => ( onSelectWorkflow?.(workflow)} > {workflow.icon && workflow.icon.type === "emoji" ? (
{workflow.name.slice(0, 1)}
) : null}
{workflow.name} {workflow.userName && ( {workflow.userName[0]?.toUpperCase()} {t("Common.sharedBy", { userName: workflow.userName, })} )}
))} )}
); } function McpServerSelector() { const t = useTranslations("Chat.Tool"); const [appStoreMutate, allowedMcpServers, mcpServerList] = appStore( useShallow((state) => [ state.mutate, state.allowedMcpServers, state.mcpList, ]), ); const selectedMcpServerList = useMemo(() => { if (mcpServerList.length === 0) return []; return [...mcpServerList] .sort( (a, b) => (a.status === "connected" ? -1 : 1) - (b.status === "connected" ? -1 : 1), ) .map((server) => { const allowedTools: string[] = allowedMcpServers?.[server.id]?.tools ?? []; return { id: server.id, serverName: server.name, checked: allowedTools.length > 0, tools: server.toolInfo.map((tool) => ({ name: tool.name, checked: allowedTools.includes(tool.name), description: tool.description, })), error: server.error, status: server.status, }; }); }, [mcpServerList, allowedMcpServers]); const setMcpServerTool = useCallback( (serverId: string, toolNames: string[]) => { appStoreMutate((prev) => { return { allowedMcpServers: { ...prev.allowedMcpServers, [serverId]: { ...(prev.allowedMcpServers?.[serverId] ?? {}), tools: toolNames, }, }, }; }); }, [], ); return ( {!selectedMcpServerList.length ? (
{t("noMcpServersDetected")}
) : ( selectedMcpServerList.map((server) => ( {server.status === "authorizing" ? (
) : ( <> {server.tools.filter((t) => t.checked).length > 0 ? ( {server.tools.filter((t) => t.checked).length} ) : null} )}
} onClick={(e) => { e.preventDefault(); setMcpServerTool( server.id, server.checked ? [] : server.tools.map((t) => t.name), ); }} >
{server.serverName} {Boolean(server.error) ? ( {t("error")} ) : null} { setMcpServerTool( server.id, checked ? server.tools.map((t) => t.name) : [], ); }} onToolClick={(toolName, checked) => { const currentTools = server.tools .filter((v) => v.checked) .map((v) => v.name); setMcpServerTool( server.id, checked ? currentTools.concat(toolName) : currentTools.filter((v) => v !== toolName), ); }} /> )) )} ); } interface McpServerToolSelectorProps { tools: { name: string; checked: boolean; description: string; }[]; isAuthorizing: boolean; serverId: string; onClickAllChecked: (checked: boolean) => void; checked: boolean; onToolClick: (toolName: string, checked: boolean) => void; } function McpServerToolSelector({ tools, serverId, onClickAllChecked, isAuthorizing, checked, onToolClick, }: McpServerToolSelectorProps) { const t = useTranslations("Common"); const tTool = useTranslations("Chat.Tool"); const [loading, setLoading] = useState(false); const [search, setSearch] = useState(""); const filteredTools = useMemo(() => { return tools.filter((tool) => tool.name.toLowerCase().includes(search.toLowerCase()), ); }, [tools, search]); const handleAuthorize = useCallback( () => safe(() => setLoading(true)) .map(() => redriectMcpOauth(serverId)) .ifOk(() => mutate("/api/mcp/list")) .ifFail(handleErrorWithToast) .watch(() => setLoading(false)), [serverId], ); if (isAuthorizing) { return ( { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleAuthorize(); } }} > {loading ? : } {tTool("authorizationRequired")} {tTool("clickToAuthorize")} ); } return (
{ e.preventDefault(); onClickAllChecked(!checked); }} > { e.stopPropagation(); }} onChange={(e) => setSearch(e.target.value)} onClick={(e) => { e.stopPropagation(); }} className="placeholder:text-muted-foreground flex w-full text-xs outline-hidden disabled:cursor-not-allowed disabled:opacity-50" />
{filteredTools.length === 0 ? (
{t("noResults")}
) : ( filteredTools.map((tool) => ( { e.preventDefault(); onToolClick(tool.name, !tool.checked); }} >

{tool.name}

{tool.description}

)) )}
); } function AppDefaultToolKitSelector() { const [appStoreMutate, allowedAppDefaultToolkit] = appStore( useShallow((state) => [state.mutate, state.allowedAppDefaultToolkit]), ); const t = useTranslations(); const toggleAppDefaultToolkit = useCallback((toolkit: AppDefaultToolkit) => { appStoreMutate((prev) => { const newAllowedAppDefaultToolkit = [ ...(prev.allowedAppDefaultToolkit ?? []), ]; if (newAllowedAppDefaultToolkit.includes(toolkit)) { newAllowedAppDefaultToolkit.splice( newAllowedAppDefaultToolkit.indexOf(toolkit), 1, ); } else { newAllowedAppDefaultToolkit.push(toolkit); } return { allowedAppDefaultToolkit: newAllowedAppDefaultToolkit }; }); }, []); const defaultToolInfo = useMemo(() => { const raw = t.raw("Chat.Tool.defaultToolKit"); return Object.values(AppDefaultToolkit).map((toolkit) => { const label = raw[toolkit] || toolkit; const id = toolkit; let icon = Wrench; switch (toolkit) { case AppDefaultToolkit.Visualization: icon = ChartColumn; break; case AppDefaultToolkit.WebSearch: icon = GlobeIcon; break; case AppDefaultToolkit.Http: icon = HardDriveUploadIcon; break; case AppDefaultToolkit.Code: icon = CodeIcon; break; } return { label, id, icon, }; }); }, []); return ( {defaultToolInfo.map((tool) => { return ( { e.preventDefault(); toggleAppDefaultToolkit(tool.id); }} > {tool.label} ); })} ); } function AgentSelector({ onSelectAgent, }: { onSelectAgent?: (agent: AgentSummary) => void; }) { const t = useTranslations(); const { myAgents, bookmarkedAgents } = useAgents({ filters: ["mine", "bookmarked"], }); const emptyAgent = useMemo(() => { if (myAgents.length + bookmarkedAgents.length > 0) return null; return (

{t("Layout.createAgent")}

{bookmarkedAgents.length > 0 ? t("Layout.createYourOwnAgentOrSelectShared") : t("Layout.createYourOwnAgent")}

); }, [myAgents.length, bookmarkedAgents.length, t]); return ( {t("Agent.title")} {emptyAgent} {/* My Agents */} {myAgents.map((agent) => ( onSelectAgent?.(agent)} > {agent.icon && agent.icon.type === "emoji" ? (
{agent.name.slice(0, 1)}
) : null} {agent.name}
))} {myAgents.length > 0 && bookmarkedAgents.length > 0 && ( )} {bookmarkedAgents.map((agent) => ( onSelectAgent?.(agent)} > {agent.icon && agent.icon.type === "emoji" ? (
{agent.name.slice(0, 1)}
) : null}
{agent.name} {agent.userName && ( {agent.userName[0]?.toUpperCase()} {t("Common.sharedBy", { userName: agent.userName })} )}
))}
); } function ImageGeneratorSelector({ onGenerateImage, modelInfo, }: { onGenerateImage?: (provider?: "google" | "openai") => void; modelInfo?: { isToolCallUnsupported?: boolean }; }) { const t = useTranslations("Chat"); return ( {t("generateImage")} onGenerateImage?.("google")} className="cursor-pointer" > Gemini (Nano Banana) onGenerateImage?.("openai")} className="cursor-pointer" > OpenAI ); } function EternitySelector() { const [activeProject, setActiveProject] = useState<{ project_name: string; is_active: boolean; priority: string } | null>(null); const [loading, setLoading] = useState(false); const fetchStatus = async () => { try { const res = await fetch("/api/eternity"); if (res.ok) { const data = await res.json(); const projects = data.projects || []; // Find the active project, or default to the most recent project const active = projects.find((p: any) => p.is_active) || projects[0]; if (active) { setActiveProject({ project_name: active.project_name, is_active: active.is_active, priority: active.priority, }); } else { setActiveProject(null); } } } catch (err) { console.error(err); } }; useEffect(() => { fetchStatus(); }, []); const handleToggle = async (checked: boolean) => { if (!activeProject) return; setLoading(true); try { const res = await fetch("/api/eternity", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "toggle", project_name: activeProject.project_name, is_active: checked, }), }); if (res.ok) { setActiveProject(prev => prev ? { ...prev, is_active: checked } : null); toast.success(`Eternity Mode ${checked ? "enabled" : "paused"}`); } else { throw new Error("Failed to update"); } } catch (err: any) { toast.error(err.message); } finally { setLoading(false); } }; const handlePriorityChange = async (priority: string) => { if (!activeProject) return; setLoading(true); try { const res = await fetch("/api/eternity", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "set-priority", project_name: activeProject.project_name, priority: priority, }), }); if (res.ok) { setActiveProject(prev => prev ? { ...prev, priority: priority } : null); toast.success(`Priority updated to ${priority}`); } else { throw new Error("Failed to update"); } } catch (err: any) { toast.error(err.message); } finally { setLoading(false); } }; if (!activeProject) { return ( Eternity Mode (No project) ); } return ( { e.preventDefault(); if (!loading) handleToggle(!activeProject.is_active); }} > Eternity Loop: {activeProject.project_name} Priority: {activeProject.priority.toUpperCase()} handlePriorityChange("supreme")} disabled={loading} > 🚀 SUPREME handlePriorityChange("low")} disabled={loading} > 💤 LOW ); }