diff --git a/agentex-ui/app/agentex-ui-root.tsx b/agentex-ui/app/agentex-ui-root.tsx new file mode 100644 index 00000000..18b45799 --- /dev/null +++ b/agentex-ui/app/agentex-ui-root.tsx @@ -0,0 +1,89 @@ +'use client'; + +import { Suspense, useCallback, useEffect, useState } from 'react'; + +import { ToastContainer } from 'react-toastify'; + +import { TaskSidebar } from '@/components/agentex/task-sidebar'; +import { TracesSidebar } from '@/components/agentex/traces-sidebar'; +import { AgentexProvider } from '@/components/providers'; +import { QueryProvider } from '@/components/providers/query-provider'; +import { useLocalStorageState } from '@/hooks/use-local-storage-state'; +import { + SearchParamKey, + useSafeSearchParams, +} from '@/hooks/use-safe-search-params'; + +import { PrimaryContent } from './primary-content'; + +type AgentexUIRootProps = { + sgpAppURL: string; + agentexAPIBaseURL: string; +}; + +export function AgentexUIRoot({ + sgpAppURL, + agentexAPIBaseURL, +}: AgentexUIRootProps) { + const { agentName, updateParams } = useSafeSearchParams(); + const [isTracesSidebarOpen, setIsTracesSidebarOpen] = useState(false); + const [localAgentName] = useLocalStorageState( + 'lastSelectedAgent', + undefined + ); + + useEffect(() => { + if (!agentName && localAgentName) { + updateParams({ [SearchParamKey.AGENT_NAME]: localAgentName }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleSelectTask = useCallback( + (taskId: string | null) => { + updateParams({ + [SearchParamKey.TASK_ID]: taskId, + }); + }, + [updateParams] + ); + + // Global keyboard shortcut: cmd + k for new chat + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key === 'k') { + event.preventDefault(); + handleSelectTask(null); + } + }; + + window.addEventListener('keydown', handleKeyDown); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [handleSelectTask]); + + return ( + + + Loading...}> +
+ + + setIsTracesSidebarOpen(!isTracesSidebarOpen) + } + /> + +
+
+
+ +
+ ); +} diff --git a/agentex-ui/app/chat-view.tsx b/agentex-ui/app/chat-view.tsx new file mode 100644 index 00000000..8aa928cd --- /dev/null +++ b/agentex-ui/app/chat-view.tsx @@ -0,0 +1,30 @@ +import { useRef } from 'react'; + +import { motion } from 'framer-motion'; + +import { MemoizedTaskMessagesComponent } from '@/components/agentex/task-messages'; +import { TaskProvider } from '@/components/providers'; + +export function ChatView({ taskID }: { taskID: string }) { + const scrollContainerRef = useRef(null); + + return ( + +
+
+ + + +
+
+
+ ); +} diff --git a/agentex-ui/app/home-view.tsx b/agentex-ui/app/home-view.tsx new file mode 100644 index 00000000..ad9d92e9 --- /dev/null +++ b/agentex-ui/app/home-view.tsx @@ -0,0 +1,46 @@ +import { motion } from 'framer-motion'; + +import { AgentsList } from '@/components/agentex/agents-list'; +import { useAgentexClient } from '@/components/providers/agentex-provider'; +import { useAgents } from '@/hooks/use-agents'; +import { useLocalStorageState } from '@/hooks/use-local-storage-state'; +import { + SearchParamKey, + useSafeSearchParams, +} from '@/hooks/use-safe-search-params'; + +export function HomeView() { + const { agentName, updateParams } = useSafeSearchParams(); + const { agentexClient } = useAgentexClient(); + const { data: agents = [], isLoading } = useAgents(agentexClient); + const [, setLocalAgentName] = useLocalStorageState( + 'lastSelectedAgent', + undefined + ); + + return ( + +
+
Agentex
+ + +
+
+ ); +} diff --git a/agentex-ui/app/main-content.tsx b/agentex-ui/app/main-content.tsx deleted file mode 100644 index 38abd435..00000000 --- a/agentex-ui/app/main-content.tsx +++ /dev/null @@ -1,318 +0,0 @@ -'use client'; - -import { Suspense, useCallback, useEffect, useRef, useState } from 'react'; - -import { AnimatePresence, motion } from 'framer-motion'; -import { ArrowDown } from 'lucide-react'; -import { ToastContainer } from 'react-toastify'; - -import { AgentsList } from '@/components/agentex/agents-list'; -import { IconButton } from '@/components/agentex/icon-button'; -import { PromptInput } from '@/components/agentex/prompt-input'; -import { MemoizedTaskMessagesComponent } from '@/components/agentex/task-messages'; -import { TaskSidebar } from '@/components/agentex/task-sidebar'; -import { TaskTopBar } from '@/components/agentex/task-top-bar'; -import { TracesSidebar } from '@/components/agentex/traces-sidebar'; -import { - AgentexProvider, - TaskProvider, - useAgentexClient, -} from '@/components/providers'; -import { QueryProvider } from '@/components/providers/query-provider'; -import { useAgents } from '@/hooks/use-agents'; -import { useLocalStorageState } from '@/hooks/use-local-storage-state'; -import { - SearchParamKey, - useSafeSearchParams, -} from '@/hooks/use-safe-search-params'; - -function NoAgentImpl() { - const { taskID, agentName, updateParams } = useSafeSearchParams(); - const [isTracesSidebarOpen, setIsTracesSidebarOpen] = useState(false); - const [localAgentName] = useLocalStorageState( - 'lastSelectedAgent', - undefined - ); - - const selectedAgentName = agentName ?? localAgentName; - - const handleSelectTask = useCallback( - (taskId: string | null) => { - updateParams({ - [SearchParamKey.TASK_ID]: taskId, - }); - }, - [updateParams] - ); - - // Global keyboard shortcut: cmd + k for new chat - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ((event.metaKey || event.ctrlKey) && event.key === 'k') { - event.preventDefault(); - handleSelectTask(null); - } - }; - - window.addEventListener('keydown', handleKeyDown); - - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - }, [handleSelectTask]); - - return ( -
- - { - - - - } - - setIsTracesSidebarOpen(!isTracesSidebarOpen)} - /> - - {taskID && ( - - - - )} - -
- ); -} - -interface ContentAreaProps { - taskID: string | null; - isTracesSidebarOpen: boolean; - toggleTracesSidebar: () => void; -} - -function ContentArea({ - taskID, - isTracesSidebarOpen, - toggleTracesSidebar, -}: ContentAreaProps) { - const { agentexClient } = useAgentexClient(); - const { data: agents = [], isLoading } = useAgents(agentexClient); - const { agentName, updateParams } = useSafeSearchParams(); - const [prompt, setPrompt] = useState(''); - const scrollContainerRef = useRef(null); - const [showScrollButton, setShowScrollButton] = useState(false); - const [localAgentName, setLocalAgentName] = useLocalStorageState< - string | undefined - >('lastSelectedAgent', undefined); - - useEffect(() => { - if (!isLoading) { - if (!agentName && localAgentName) { - updateParams({ [SearchParamKey.AGENT_NAME]: localAgentName }); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoading]); - - const handleSelectAgent = useCallback( - (agentName: string | undefined) => { - updateParams({ - [SearchParamKey.AGENT_NAME]: agentName ?? null, - [SearchParamKey.TASK_ID]: null, - }); - setPrompt(''); - }, - [updateParams, setPrompt] - ); - - // Scroll detection - track if user is near bottom - useEffect(() => { - const container = scrollContainerRef.current; - if (!container) { - setShowScrollButton(false); - return; - } - - const handleScroll = () => { - const { scrollTop, scrollHeight, clientHeight } = container; - const distanceFromBottom = scrollHeight - scrollTop - clientHeight; - - const scrollThreshold = 100; // pixels from bottom - const isNearBottom = distanceFromBottom < scrollThreshold; - - setShowScrollButton(!isNearBottom); - }; - - container.addEventListener('scroll', handleScroll); - return () => container.removeEventListener('scroll', handleScroll); - }, [taskID]); - - const scrollToBottom = useCallback(() => { - if (!scrollContainerRef.current) return; - scrollContainerRef.current.scrollTo({ - top: scrollContainerRef.current.scrollHeight, - behavior: 'smooth', - }); - }, [scrollContainerRef]); - - useEffect(() => { - if (scrollContainerRef.current && taskID) { - setTimeout(() => { - scrollToBottom(); - }, 150); - } - }, [scrollToBottom, taskID]); - - return ( - - - {taskID && agentName && ( - - - - )} - - {taskID ? ( - -
-
- - - -
-
-
- ) : ( - -
-
Agentex
- - -
-
- )} - - - {taskID && ( - - {showScrollButton && ( - - - - )} - - )} - -
- -
-
-
-
- ); -} - -type Props = { - sgpAppURL: string; - agentexAPIBaseURL: string; -}; - -export function MainContent({ sgpAppURL, agentexAPIBaseURL }: Props) { - return ( - - - Loading...}> - - - - - - ); -} diff --git a/agentex-ui/app/page.tsx b/agentex-ui/app/page.tsx index f2b4f7e4..7b0363aa 100644 --- a/agentex-ui/app/page.tsx +++ b/agentex-ui/app/page.tsx @@ -1,6 +1,6 @@ import { connection } from 'next/server'; -import { MainContent } from './main-content'; +import { AgentexUIRoot } from './agentex-ui-root'; export default async function RootPage() { await connection(); @@ -19,6 +19,9 @@ export default async function RootPage() { } return ( - + ); } diff --git a/agentex-ui/app/primary-content.tsx b/agentex-ui/app/primary-content.tsx new file mode 100644 index 00000000..32f2d2ee --- /dev/null +++ b/agentex-ui/app/primary-content.tsx @@ -0,0 +1,154 @@ +import { useState, useRef, useEffect, useCallback } from 'react'; + +import { AnimatePresence, motion } from 'framer-motion'; +import { ArrowDown } from 'lucide-react'; + +import { IconButton } from '@/components/agentex/icon-button'; +import { PromptInput } from '@/components/agentex/prompt-input'; +import { TaskTopBar } from '@/components/agentex/task-top-bar'; +import { useAgentexClient } from '@/components/providers'; +import { useAgents } from '@/hooks/use-agents'; +import { + useSafeSearchParams, + SearchParamKey, +} from '@/hooks/use-safe-search-params'; + +import { ChatView } from './chat-view'; +import { HomeView } from './home-view'; + +interface ContentAreaProps { + isTracesSidebarOpen: boolean; + toggleTracesSidebar: () => void; +} + +export function PrimaryContent({ + isTracesSidebarOpen, + toggleTracesSidebar, +}: ContentAreaProps) { + const { taskID } = useSafeSearchParams(); + const { agentexClient } = useAgentexClient(); + const { data: agents = [] } = useAgents(agentexClient); + const { agentName, updateParams } = useSafeSearchParams(); + const [prompt, setPrompt] = useState(''); + const scrollContainerRef = useRef(null); + const [showScrollButton, setShowScrollButton] = useState(false); + + const handleSelectAgent = useCallback( + (agentName: string | undefined) => { + updateParams({ + [SearchParamKey.AGENT_NAME]: agentName ?? null, + [SearchParamKey.TASK_ID]: null, + }); + setPrompt(''); + }, + [updateParams, setPrompt] + ); + + // Scroll detection - track if user is near bottom + useEffect(() => { + const container = scrollContainerRef.current; + if (!container) { + setShowScrollButton(false); + return; + } + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = container; + const distanceFromBottom = scrollHeight - scrollTop - clientHeight; + + const scrollThreshold = 100; // pixels from bottom + const isNearBottom = distanceFromBottom < scrollThreshold; + + setShowScrollButton(!isNearBottom); + }; + + container.addEventListener('scroll', handleScroll); + return () => container.removeEventListener('scroll', handleScroll); + }, [taskID]); + + const scrollToBottom = useCallback(() => { + if (!scrollContainerRef.current) return; + scrollContainerRef.current.scrollTo({ + top: scrollContainerRef.current.scrollHeight, + behavior: 'smooth', + }); + }, [scrollContainerRef]); + + useEffect(() => { + if (scrollContainerRef.current && taskID) { + setTimeout(() => { + scrollToBottom(); + }, 150); + } + }, [scrollToBottom, taskID]); + + return ( + + {taskID && agentName && ( + + + + )} + + {taskID ? : } + + + {taskID && ( + + {showScrollButton && ( + + + + )} + + )} + +
+ +
+
+
+ ); +} diff --git a/agentex-ui/components/agentex/task-sidebar.tsx b/agentex-ui/components/agentex/task-sidebar.tsx index ad273613..8a2aed4e 100644 --- a/agentex-ui/components/agentex/task-sidebar.tsx +++ b/agentex-ui/components/agentex/task-sidebar.tsx @@ -19,6 +19,10 @@ import { useAgentexClient } from '@/components/providers'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; import { Skeleton } from '@/components/ui/skeleton'; +import { + SearchParamKey, + useSafeSearchParams, +} from '@/hooks/use-safe-search-params'; import { useInfiniteTasks } from '@/hooks/use-tasks'; import { cn } from '@/lib/utils'; @@ -26,14 +30,21 @@ import type { TaskListResponse } from 'agentex/resources'; type TaskButtonProps = { task: TaskListResponse.TaskListResponseItem; - selectedTaskID: TaskListResponse.TaskListResponseItem['id'] | null; - selectTask: ( - taskID: TaskListResponse.TaskListResponseItem['id'] | null - ) => void; }; -function TaskButton({ task, selectedTaskID, selectTask }: TaskButtonProps) { +function TaskButton({ task }: TaskButtonProps) { + const { taskID, updateParams } = useSafeSearchParams(); const taskName = createTaskName(task); + + const handleTaskSelect = useCallback( + (taskID: TaskListResponse.TaskListResponseItem['id']) => { + updateParams({ + [SearchParamKey.TASK_ID]: taskID, + }); + }, + [updateParams] + ); + const createdAtString = useMemo( () => task.created_at @@ -80,14 +91,14 @@ function TaskButton({ task, selectedTaskID, selectTask }: TaskButtonProps) {