From 5fff4a7d1966799c6d565268338c9070586def98 Mon Sep 17 00:00:00 2001 From: Ryan Hopper-Lowe Date: Fri, 13 Dec 2024 17:10:14 -0600 Subject: [PATCH 01/10] feat: enhance chat functionality with new ChatHelpers component and optional tool deletion - Added ChatHelpers component to provide tools and knowledge information in the chat interface. - Updated ToolEntry component to make the onDelete prop optional, allowing for conditional rendering of the delete button. - Adjusted Chat component layout to include padding and integrate ChatHelpers. - Minor style adjustments in Chatbar component to remove unnecessary padding. This update improves the user experience by providing additional context and functionality within the chat interface. --- ui/admin/app/components/agent/ToolEntry.tsx | 22 +-- ui/admin/app/components/chat/Chat.tsx | 5 +- ui/admin/app/components/chat/ChatHelpers.tsx | 162 +++++++++++++++++++ ui/admin/app/components/chat/Chatbar.tsx | 2 +- ui/admin/app/lib/model/agents.ts | 2 +- 5 files changed, 180 insertions(+), 13 deletions(-) create mode 100644 ui/admin/app/components/chat/ChatHelpers.tsx diff --git a/ui/admin/app/components/agent/ToolEntry.tsx b/ui/admin/app/components/agent/ToolEntry.tsx index 7e510dfa0..0e76b6184 100644 --- a/ui/admin/app/components/agent/ToolEntry.tsx +++ b/ui/admin/app/components/agent/ToolEntry.tsx @@ -14,7 +14,7 @@ export function ToolEntry({ actions, }: { tool: string; - onDelete: () => void; + onDelete?: () => void; actions?: React.ReactNode; }) { const { data: toolReference, isLoading } = useSWR( @@ -26,7 +26,7 @@ export function ToolEntry({ return (
-
+
{isLoading ? ( @@ -44,14 +44,16 @@ export function ToolEntry({
{actions} - + {onDelete && ( + + )}
diff --git a/ui/admin/app/components/chat/Chat.tsx b/ui/admin/app/components/chat/Chat.tsx index f9d057a4f..27a2810e6 100644 --- a/ui/admin/app/components/chat/Chat.tsx +++ b/ui/admin/app/components/chat/Chat.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { cn } from "~/lib/utils"; import { useChat } from "~/components/chat/ChatContext"; +import { ChatHelpers } from "~/components/chat/ChatHelpers"; import { Chatbar } from "~/components/chat/Chatbar"; import { MessagePane } from "~/components/chat/MessagePane"; import { RunWorkflow } from "~/components/chat/RunWorkflow"; @@ -31,7 +32,7 @@ export function Chat({ className }: ChatProps) { const showStartButtonPane = mode === "workflow" && !readOnly; return ( -
+
{showMessagePane && (
)} + +
); } diff --git a/ui/admin/app/components/chat/ChatHelpers.tsx b/ui/admin/app/components/chat/ChatHelpers.tsx new file mode 100644 index 000000000..dc372e805 --- /dev/null +++ b/ui/admin/app/components/chat/ChatHelpers.tsx @@ -0,0 +1,162 @@ +import { LibraryIcon, WrenchIcon } from "lucide-react"; +import { useMemo } from "react"; +import useSWR from "swr"; + +import { Agent } from "~/lib/model/agents"; +import { KnowledgeFile } from "~/lib/model/knowledge"; +import { AgentService } from "~/lib/service/api/agentService"; +import { ThreadsService } from "~/lib/service/api/threadsService"; +import { cn } from "~/lib/utils"; + +import { TypographyMuted, TypographySmall } from "~/components/Typography"; +import { ToolEntry } from "~/components/agent/ToolEntry"; +import { useChat } from "~/components/chat/ChatContext"; +import { Button } from "~/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "~/components/ui/popover"; +import { Switch } from "~/components/ui/switch"; + +export function ChatHelpers() { + const { threadId } = useChat(); + + const { data: thread } = useSWR( + ThreadsService.getThreadById.key(threadId), + ({ threadId }) => ThreadsService.getThreadById(threadId) + ); + + const { data: knowledge } = useSWR( + ThreadsService.getKnowledge.key(threadId), + ({ threadId }) => ThreadsService.getKnowledge(threadId) + ); + + const { data: agent } = useSWR( + AgentService.getAgentById.key(thread?.agentID), + ({ agentId }) => AgentService.getAgentById(agentId) + ); + + const tools = thread?.tools; + + return ( +
+
+ {!!tools?.length && } + + {!!knowledge?.length && } +
+
+ ); +} + +function ToolsInfo({ + tools, + className, + agent, +}: { + tools: string[]; + className?: string; + agent: Nullish; +}) { + const toolItems = useMemo(() => { + if (!agent) + return tools.map((tool) => ({ + tool, + isToggleable: false, + isEnabled: true, + })); + + const agentTools = (agent.tools ?? []).map((tool) => ({ + tool, + isToggleable: false, + isEnabled: true, + })); + + const { defaultThreadTools, availableThreadTools } = agent ?? {}; + + const toggleableTools = [ + ...(defaultThreadTools ?? []), + ...(availableThreadTools ?? []), + ].map((tool) => ({ + tool, + isToggleable: true, + isEnabled: tools.includes(tool), + })); + + return [...agentTools, ...toggleableTools]; + }, [tools, agent]); + + return ( + + + + + + +
+ + Available Tools + +
+ {toolItems.map(({ tool, isToggleable, isEnabled }) => ( + {}} + /> + ) : ( + On + ) + } + /> + ))} +
+
+
+
+ ); +} + +function KnowledgeInfo({ + knowledge, + className, +}: { + knowledge: KnowledgeFile[]; + className?: string; +}) { + return ( + + + + + + +
+ {knowledge.map((file) => ( + + {file.fileName} + + ))} +
+
+
+ ); +} diff --git a/ui/admin/app/components/chat/Chatbar.tsx b/ui/admin/app/components/chat/Chatbar.tsx index 46629d5b9..1bf06adb5 100644 --- a/ui/admin/app/components/chat/Chatbar.tsx +++ b/ui/admin/app/components/chat/Chatbar.tsx @@ -30,7 +30,7 @@ export function Chatbar({ className }: ChatbarProps) { return (
; params?: Record; knowledgeDescription?: string; model?: string; From 59a040d83b3e9ba4dd6a1d7d41408beb58c04b62 Mon Sep 17 00:00:00 2001 From: Ryan Hopper-Lowe Date: Mon, 16 Dec 2024 11:37:29 -0600 Subject: [PATCH 02/10] chore: prevent flash of unresized textarea --- ui/admin/app/components/ui/textarea.tsx | 98 +++++++++++++++---------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/ui/admin/app/components/ui/textarea.tsx b/ui/admin/app/components/ui/textarea.tsx index 0e4b4c74e..e6f7eb488 100644 --- a/ui/admin/app/components/ui/textarea.tsx +++ b/ui/admin/app/components/ui/textarea.tsx @@ -27,39 +27,61 @@ interface UseAutosizeTextAreaProps { textAreaRef: HTMLTextAreaElement | null; minHeight?: number; maxHeight?: number; - triggerAutoSize: string; } const useAutosizeTextArea = ({ textAreaRef, - triggerAutoSize, maxHeight = Number.MAX_SAFE_INTEGER, minHeight = 0, }: UseAutosizeTextAreaProps) => { const [init, setInit] = React.useState(true); - React.useEffect(() => { - // We need to reset the height momentarily to get the correct scrollHeight for the textarea - const offsetBorder = 2; - if (textAreaRef) { + + const resize = React.useCallback( + (node: HTMLTextAreaElement) => { + // Reset the height to auto to get the correct scrollHeight + node.style.height = "auto"; + + const offsetBorder = 2; + if (init) { - textAreaRef.style.minHeight = `${minHeight + offsetBorder}px`; + node.style.minHeight = `${minHeight + offsetBorder}px`; if (maxHeight > minHeight) { - textAreaRef.style.maxHeight = `${maxHeight}px`; + node.style.maxHeight = `${maxHeight}px`; } + node.style.height = `${minHeight + offsetBorder}px`; setInit(false); } - textAreaRef.style.height = `${minHeight + offsetBorder}px`; - const scrollHeight = textAreaRef.scrollHeight; - // We then set the height directly, outside of the render loop - // Trying to set this with state or a ref will product an incorrect value. - if (scrollHeight > maxHeight) { - textAreaRef.style.height = `${maxHeight}px`; - } else { - textAreaRef.style.height = `${scrollHeight + offsetBorder}px`; - } - } + + node.style.height = `${ + Math.min(Math.max(node.scrollHeight, minHeight), maxHeight) + + offsetBorder + }px`; + }, + // disable exhaustive deps because we don't want to rerun this after init is set to false // eslint-disable-next-line react-hooks/exhaustive-deps - }, [textAreaRef, triggerAutoSize]); + [maxHeight, minHeight] + ); + + const initResizer = React.useCallback( + (node: HTMLTextAreaElement) => { + node.onkeyup = () => resize(node); + node.onfocus = () => resize(node); + node.oninput = () => resize(node); + node.onresize = () => resize(node); + node.onchange = () => resize(node); + resize(node); + }, + [resize] + ); + + React.useEffect(() => { + if (textAreaRef) { + initResizer(textAreaRef); + resize(textAreaRef); + } + }, [resize, initResizer, textAreaRef]); + + return { initResizer }; }; export type AutosizeTextAreaRef = { @@ -89,36 +111,38 @@ const AutosizeTextarea = React.forwardRef< ref: React.Ref ) => { const textAreaRef = React.useRef(null); - const [triggerAutoSize, setTriggerAutoSize] = React.useState(""); - - useAutosizeTextArea({ - textAreaRef: textAreaRef.current, - triggerAutoSize: triggerAutoSize, - maxHeight, - minHeight, - }); useImperativeHandle(ref, () => ({ textArea: textAreaRef.current as HTMLTextAreaElement, - focus: () => textAreaRef?.current?.focus(), + focus: textAreaRef?.current?.focus, maxHeight, minHeight, })); - React.useEffect(() => { - setTriggerAutoSize(value as string); - }, [props?.defaultValue, value]); + const { initResizer } = useAutosizeTextArea({ + textAreaRef: textAreaRef.current, + maxHeight, + minHeight, + }); + + const initRef = React.useCallback( + (node: HTMLTextAreaElement | null) => { + textAreaRef.current = node; + + if (!node) return; + + initResizer(node); + }, + [initResizer] + ); return (