Programmer-RD-AI commited on
Commit
bbe4eea
Β·
1 Parent(s): ae11e81

Update Agent UI Verion

Browse files
Files changed (48) hide show
  1. Dockerfile +0 -66
  2. README.md +40 -17
  3. next.config.js +0 -3
  4. next.config.ts +1 -2
  5. package-lock.json +0 -0
  6. package.json +2 -2
  7. pnpm-lock.yaml +0 -0
  8. public/favicon.ico +0 -0
  9. src/api/os.ts +133 -0
  10. src/api/playground.ts +0 -89
  11. src/api/routes.ts +14 -20
  12. src/app/page.tsx +2 -2
  13. src/components/{playground β†’ chat}/ChatArea/ChatArea.tsx +0 -0
  14. src/components/{playground β†’ chat}/ChatArea/ChatInput/ChatInput.tsx +8 -5
  15. src/components/{playground β†’ chat}/ChatArea/ChatInput/index.ts +0 -0
  16. src/components/{playground β†’ chat}/ChatArea/MessageArea.tsx +3 -3
  17. src/components/{playground β†’ chat}/ChatArea/Messages/AgentThinkingLoader.tsx +0 -0
  18. src/components/{playground β†’ chat}/ChatArea/Messages/ChatBlankState.tsx +3 -6
  19. src/components/{playground β†’ chat}/ChatArea/Messages/MessageItem.tsx +10 -12
  20. src/components/{playground β†’ chat}/ChatArea/Messages/Messages.tsx +7 -6
  21. src/components/{playground β†’ chat}/ChatArea/Messages/Multimedia/Audios/Audios.tsx +1 -1
  22. src/components/{playground β†’ chat}/ChatArea/Messages/Multimedia/Audios/index.ts +0 -0
  23. src/components/{playground β†’ chat}/ChatArea/Messages/Multimedia/Images/Images.tsx +1 -1
  24. src/components/{playground β†’ chat}/ChatArea/Messages/Multimedia/Images/index.ts +0 -0
  25. src/components/{playground β†’ chat}/ChatArea/Messages/Multimedia/Videos/Videos.tsx +1 -1
  26. src/components/{playground β†’ chat}/ChatArea/Messages/Multimedia/Videos/index.ts +0 -0
  27. src/components/{playground β†’ chat}/ChatArea/Messages/index.ts +0 -0
  28. src/components/{playground β†’ chat}/ChatArea/ScrollToBottom.tsx +0 -0
  29. src/components/{playground β†’ chat}/ChatArea/index.ts +0 -0
  30. src/components/{playground/Sidebar/AgentSelector.tsx β†’ chat/Sidebar/EntitySelector.tsx} +53 -35
  31. src/components/chat/Sidebar/ModeSelector.tsx +57 -0
  32. src/components/{playground β†’ chat}/Sidebar/NewChatButton.tsx +2 -2
  33. src/components/{playground β†’ chat}/Sidebar/Sessions/DeleteSessionModal.tsx +0 -0
  34. src/components/{playground β†’ chat}/Sidebar/Sessions/SessionBlankState.tsx +2 -18
  35. src/components/{playground β†’ chat}/Sidebar/Sessions/SessionItem.tsx +54 -33
  36. src/components/{playground β†’ chat}/Sidebar/Sessions/Sessions.tsx +62 -68
  37. src/components/{playground β†’ chat}/Sidebar/Sessions/index.ts +0 -0
  38. src/components/{playground β†’ chat}/Sidebar/Sidebar.tsx +27 -18
  39. src/components/{playground β†’ chat}/Sidebar/index.ts +0 -0
  40. src/components/ui/typography/MarkdownRenderer/inlineStyles.tsx +3 -3
  41. src/components/ui/typography/MarkdownRenderer/styles.tsx +7 -4
  42. src/hooks/useAIResponseStream.tsx +118 -38
  43. src/hooks/useAIStreamHandler.tsx +184 -38
  44. src/hooks/useChatActions.ts +109 -32
  45. src/hooks/useSessionLoader.tsx +65 -54
  46. src/store.ts +22 -28
  47. src/types/{playground.ts β†’ os.ts} +93 -15
  48. workflows/validate.yml +0 -21
Dockerfile DELETED
@@ -1,66 +0,0 @@
1
- # Use Node.js 18 Alpine for smaller image size
2
- FROM node:18-alpine AS base
3
-
4
- # Install dependencies only when needed
5
- FROM base AS deps
6
- # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
7
- RUN apk add --no-cache libc6-compat
8
- WORKDIR /app
9
-
10
- # Install dependencies based on the preferred package manager
11
- COPY package*.json pnpm-lock.yaml* ./
12
- RUN \
13
- if [ -f pnpm-lock.yaml ]; then \
14
- corepack enable pnpm && pnpm i --frozen-lockfile; \
15
- elif [ -f package-lock.json ]; then \
16
- npm ci; \
17
- else \
18
- echo "Lockfile not found." && exit 1; \
19
- fi
20
-
21
- # Rebuild the source code only when needed
22
- FROM base AS builder
23
- WORKDIR /app
24
- COPY --from=deps /app/node_modules ./node_modules
25
- COPY . .
26
-
27
- # Build the Next.js application
28
- RUN \
29
- if [ -f pnpm-lock.yaml ]; then \
30
- corepack enable pnpm && pnpm run build; \
31
- else \
32
- npm run build; \
33
- fi
34
-
35
- # Production image, copy all the files and run next
36
- FROM base AS runner
37
- WORKDIR /app
38
-
39
- ENV NODE_ENV production
40
- # Uncomment the following line in case you want to disable telemetry during runtime.
41
- # ENV NEXT_TELEMETRY_DISABLED 1
42
-
43
- RUN addgroup --system --gid 1001 nodejs
44
- RUN adduser --system --uid 1001 nextjs
45
-
46
- COPY --from=builder /app/public ./public
47
-
48
- # Set the correct permission for prerender cache
49
- RUN mkdir .next
50
- RUN chown nextjs:nodejs .next
51
-
52
- # Automatically leverage output traces to reduce image size
53
- # https://nextjs.org/docs/advanced-features/output-file-tracing
54
- COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
55
- COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
56
-
57
- USER nextjs
58
-
59
- # Expose port 7860 (Hugging Face Spaces default)
60
- EXPOSE 7860
61
-
62
- ENV PORT 7860
63
- ENV HOSTNAME "0.0.0.0"
64
-
65
- # Run the application
66
- CMD ["node", "server.js"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,22 +1,12 @@
1
- ---
2
- title: SIFA Classification Agentic RAG Frontend
3
- emoji: πŸ€–
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: docker
7
- app_port: 7860
8
- pinned: false
9
- license: mit
10
- ---
11
-
12
  # Agent UI
13
 
14
- A modern chat interface for AI agents built with Next.js, Tailwind CSS, and TypeScript. This template provides a ready-to-use UI for interacting with Agno agents.
15
 
16
- <img src="https://github.com/user-attachments/assets/7765fae5-a813-46cb-993b-904af9bc1672" alt="agent-ui" style="border-radius: 10px; width: 100%; max-width: 800px;" />
17
 
18
  ## Features
19
 
 
20
  - πŸ’¬ **Modern Chat Interface**: Clean design with real-time streaming support
21
  - 🧩 **Tool Calls Support**: Visualizes agent tool calls and their results
22
  - 🧠 **Reasoning Steps**: Displays agent reasoning process (when available)
@@ -25,11 +15,18 @@ A modern chat interface for AI agents built with Next.js, Tailwind CSS, and Type
25
  - 🎨 **Customizable UI**: Built with Tailwind CSS for easy styling
26
  - 🧰 **Built with Modern Stack**: Next.js, TypeScript, shadcn/ui, Framer Motion, and more
27
 
 
 
 
 
 
28
  ## Getting Started
29
 
30
  ### Prerequisites
31
 
32
- Before setting up Agent UI, you may want to have an Agno Playground running. If you haven't set up the Agno Playground yet, follow the [official guide](https://agno.link/agent-ui#connect-to-local-agents) to run the Playground locally.
 
 
33
 
34
  ### Installation
35
 
@@ -62,11 +59,37 @@ pnpm dev
62
 
63
  4. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
64
 
65
- ## Connecting to an Agent Backend
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
- By default Agent UI connects to `http://localhost:7777`. You can easily change this by hovering over the endpoint URL and clicking the edit option.
68
 
69
- The default endpoint works with the standard Agno Playground setup described in the [official documentation](https://agno.link/agent-ui#connect-to-local-agents).
70
 
71
  ## Contributing
72
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # Agent UI
2
 
3
+ A modern chat interface for AgentOS built with Next.js, Tailwind CSS, and TypeScript. This template provides a ready-to-use UI for connecting to and interacting with your AgentOS instances through the Agno platform.
4
 
5
+ <img src="https://agno-public.s3.us-east-1.amazonaws.com/assets/agent_ui_banner.svg" alt="agent-ui" style="border-radius: 10px; width: 100%; max-width: 800px;" />
6
 
7
  ## Features
8
 
9
+ - πŸ”— **AgentOS Integration**: Seamlessly connect to local and live AgentOS instances
10
  - πŸ’¬ **Modern Chat Interface**: Clean design with real-time streaming support
11
  - 🧩 **Tool Calls Support**: Visualizes agent tool calls and their results
12
  - 🧠 **Reasoning Steps**: Displays agent reasoning process (when available)
 
15
  - 🎨 **Customizable UI**: Built with Tailwind CSS for easy styling
16
  - 🧰 **Built with Modern Stack**: Next.js, TypeScript, shadcn/ui, Framer Motion, and more
17
 
18
+ ## Version Support
19
+
20
+ - **Main Branch**: Supports Agno v2.x (recommended)
21
+ - **v1 Branch**: Supports Agno v1.x for legacy compatibility
22
+
23
  ## Getting Started
24
 
25
  ### Prerequisites
26
 
27
+ Before setting up Agent UI, you need a running AgentOS instance. If you haven't created one yet, check out our [Creating Your First OS](/agent-os/creating-your-first-os) guide.
28
+
29
+ > **Note**: Agent UI connects to AgentOS instances through the Agno platform. Make sure your AgentOS is running before attempting to connect.
30
 
31
  ### Installation
32
 
 
59
 
60
  4. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
61
 
62
+ ## Connecting to Your AgentOS
63
+
64
+ Agent UI connects directly to your AgentOS instance, allowing you to interact with your agents through a modern chat interface.
65
+
66
+ > **Prerequisites**: You need a running AgentOS instance before you can connect Agent UI to it. If you haven't created one yet, check out our [Creating Your First OS](https://docs.agno.com/agent-os/creating-your-first-os) guide.
67
+
68
+ ## Step-by-Step Connection Process
69
+
70
+ ### 1. Configure the Endpoint
71
+
72
+ By default, Agent UI connects to `http://localhost:7777`. You can easily change this by:
73
+
74
+ 1. Hovering over the endpoint URL in the left sidebar
75
+ 2. Clicking the edit option to modify the connection settings
76
+
77
+ ### 2. Choose Your Environment
78
+
79
+ - **Local Development**: Use `http://localhost:7777` (default) or your custom local port
80
+ - **Production**: Enter your production AgentOS HTTPS URL
81
+
82
+ > **Warning**: Make sure your AgentOS is actually running on the specified endpoint before attempting to connect.
83
+
84
+ ### 3. Test the Connection
85
+
86
+ Once you've configured the endpoint:
87
+
88
+ 1. The Agent UI will automatically attempt to connect to your AgentOS
89
+ 2. If successful, you'll see your agents available in the chat interface
90
+ 3. If there are connection issues, check that your AgentOS is running and accessible. Check out the troubleshooting guide [here](https://docs.agno.com/faq/agentos-connection)
91
 
 
92
 
 
93
 
94
  ## Contributing
95
 
next.config.js DELETED
@@ -1,3 +0,0 @@
1
- module.exports = {
2
- output: 'standalone'
3
- }
 
 
 
 
next.config.ts CHANGED
@@ -1,8 +1,7 @@
1
  import type { NextConfig } from 'next'
2
 
3
  const nextConfig: NextConfig = {
4
- devIndicators: false,
5
- output: 'standalone'
6
  }
7
 
8
  export default nextConfig
 
1
  import type { NextConfig } from 'next'
2
 
3
  const nextConfig: NextConfig = {
4
+ devIndicators: false
 
5
  }
6
 
7
  export default nextConfig
package-lock.json DELETED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -28,8 +28,8 @@
28
  "next-themes": "^0.4.4",
29
  "nuqs": "^2.3.2",
30
  "prettier-plugin-tailwindcss": "^0.6.11",
31
- "react": "^19.0.0",
32
- "react-dom": "^19.0.0",
33
  "react-markdown": "^9.0.3",
34
  "rehype-raw": "^7.0.0",
35
  "rehype-sanitize": "^6.0.0",
 
28
  "next-themes": "^0.4.4",
29
  "nuqs": "^2.3.2",
30
  "prettier-plugin-tailwindcss": "^0.6.11",
31
+ "react": "^18.3.1",
32
+ "react-dom": "^18.3.1",
33
  "react-markdown": "^9.0.3",
34
  "rehype-raw": "^7.0.0",
35
  "rehype-sanitize": "^6.0.0",
pnpm-lock.yaml CHANGED
The diff for this file is too large to render. See raw diff
 
public/favicon.ico DELETED
Binary file (15.4 kB)
 
src/api/os.ts ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { toast } from 'sonner'
2
+
3
+ import { APIRoutes } from './routes'
4
+
5
+ import { AgentDetails, Sessions, TeamDetails } from '@/types/os'
6
+
7
+ export const getAgentsAPI = async (
8
+ endpoint: string
9
+ ): Promise<AgentDetails[]> => {
10
+ const url = APIRoutes.GetAgents(endpoint)
11
+ try {
12
+ const response = await fetch(url, { method: 'GET' })
13
+ if (!response.ok) {
14
+ toast.error(`Failed to fetch agents: ${response.statusText}`)
15
+ return []
16
+ }
17
+ const data = await response.json()
18
+ return data
19
+ } catch {
20
+ toast.error('Error fetching agents')
21
+ return []
22
+ }
23
+ }
24
+
25
+ export const getStatusAPI = async (base: string): Promise<number> => {
26
+ const response = await fetch(APIRoutes.Status(base), {
27
+ method: 'GET'
28
+ })
29
+ return response.status
30
+ }
31
+
32
+ export const getAllSessionsAPI = async (
33
+ base: string,
34
+ type: 'agent' | 'team',
35
+ componentId: string,
36
+ dbId: string
37
+ ): Promise<Sessions | { data: [] }> => {
38
+ try {
39
+ const url = new URL(APIRoutes.GetSessions(base))
40
+ url.searchParams.set('type', type)
41
+ url.searchParams.set('component_id', componentId)
42
+ url.searchParams.set('db_id', dbId)
43
+
44
+ const response = await fetch(url.toString(), {
45
+ method: 'GET'
46
+ })
47
+
48
+ if (!response.ok) {
49
+ if (response.status === 404) {
50
+ return { data: [] }
51
+ }
52
+ throw new Error(`Failed to fetch sessions: ${response.statusText}`)
53
+ }
54
+ return response.json()
55
+ } catch {
56
+ return { data: [] }
57
+ }
58
+ }
59
+
60
+ export const getSessionAPI = async (
61
+ base: string,
62
+ type: 'agent' | 'team',
63
+ sessionId: string,
64
+ dbId?: string
65
+ ) => {
66
+ // build query string
67
+ const queryParams = new URLSearchParams({ type })
68
+ if (dbId) queryParams.append('db_id', dbId)
69
+
70
+ const response = await fetch(
71
+ `${APIRoutes.GetSession(base, sessionId)}?${queryParams.toString()}`,
72
+ {
73
+ method: 'GET'
74
+ }
75
+ )
76
+
77
+ if (!response.ok) {
78
+ throw new Error(`Failed to fetch session: ${response.statusText}`)
79
+ }
80
+
81
+ return response.json()
82
+ }
83
+
84
+ export const deleteSessionAPI = async (
85
+ base: string,
86
+ dbId: string,
87
+ sessionId: string
88
+ ) => {
89
+ const queryParams = new URLSearchParams()
90
+ if (dbId) queryParams.append('db_id', dbId)
91
+ const response = await fetch(
92
+ `${APIRoutes.DeleteSession(base, sessionId)}?${queryParams.toString()}`,
93
+ {
94
+ method: 'DELETE'
95
+ }
96
+ )
97
+ return response
98
+ }
99
+
100
+ export const getTeamsAPI = async (endpoint: string): Promise<TeamDetails[]> => {
101
+ const url = APIRoutes.GetTeams(endpoint)
102
+ try {
103
+ const response = await fetch(url, { method: 'GET' })
104
+ if (!response.ok) {
105
+ toast.error(`Failed to fetch teams: ${response.statusText}`)
106
+ return []
107
+ }
108
+ const data = await response.json()
109
+
110
+ return data
111
+ } catch {
112
+ toast.error('Error fetching teams')
113
+ return []
114
+ }
115
+ }
116
+
117
+ export const deleteTeamSessionAPI = async (
118
+ base: string,
119
+ teamId: string,
120
+ sessionId: string
121
+ ) => {
122
+ const response = await fetch(
123
+ APIRoutes.DeleteTeamSession(base, teamId, sessionId),
124
+ {
125
+ method: 'DELETE'
126
+ }
127
+ )
128
+
129
+ if (!response.ok) {
130
+ throw new Error(`Failed to delete team session: ${response.statusText}`)
131
+ }
132
+ return response
133
+ }
src/api/playground.ts DELETED
@@ -1,89 +0,0 @@
1
- import { toast } from 'sonner'
2
-
3
- import { APIRoutes } from './routes'
4
-
5
- import { Agent, ComboboxAgent, SessionEntry } from '@/types/playground'
6
-
7
- export const getPlaygroundAgentsAPI = async (
8
- endpoint: string
9
- ): Promise<ComboboxAgent[]> => {
10
- const url = APIRoutes.GetPlaygroundAgents(endpoint)
11
- try {
12
- const response = await fetch(url, { method: 'GET' })
13
- if (!response.ok) {
14
- toast.error(`Failed to fetch playground agents: ${response.statusText}`)
15
- return []
16
- }
17
- const data = await response.json()
18
- // Transform the API response into the expected shape.
19
- const agents: ComboboxAgent[] = data.map((item: Agent) => ({
20
- value: item.agent_id || '',
21
- label: item.name || '',
22
- model: item.model || '',
23
- storage: item.storage || false
24
- }))
25
- return agents
26
- } catch {
27
- toast.error('Error fetching playground agents')
28
- return []
29
- }
30
- }
31
-
32
- export const getPlaygroundStatusAPI = async (base: string): Promise<number> => {
33
- const response = await fetch(APIRoutes.PlaygroundStatus(base), {
34
- method: 'GET'
35
- })
36
- return response.status
37
- }
38
-
39
- export const getAllPlaygroundSessionsAPI = async (
40
- base: string,
41
- agentId: string
42
- ): Promise<SessionEntry[]> => {
43
- try {
44
- const response = await fetch(
45
- APIRoutes.GetPlaygroundSessions(base, agentId),
46
- {
47
- method: 'GET'
48
- }
49
- )
50
- if (!response.ok) {
51
- if (response.status === 404) {
52
- // Return empty array when storage is not enabled
53
- return []
54
- }
55
- throw new Error(`Failed to fetch sessions: ${response.statusText}`)
56
- }
57
- return response.json()
58
- } catch {
59
- return []
60
- }
61
- }
62
-
63
- export const getPlaygroundSessionAPI = async (
64
- base: string,
65
- agentId: string,
66
- sessionId: string
67
- ) => {
68
- const response = await fetch(
69
- APIRoutes.GetPlaygroundSession(base, agentId, sessionId),
70
- {
71
- method: 'GET'
72
- }
73
- )
74
- return response.json()
75
- }
76
-
77
- export const deletePlaygroundSessionAPI = async (
78
- base: string,
79
- agentId: string,
80
- sessionId: string
81
- ) => {
82
- const response = await fetch(
83
- APIRoutes.DeletePlaygroundSession(base, agentId, sessionId),
84
- {
85
- method: 'DELETE'
86
- }
87
- )
88
- return response
89
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/api/routes.ts CHANGED
@@ -1,23 +1,17 @@
1
  export const APIRoutes = {
2
- GetPlaygroundAgents: (PlaygroundApiUrl: string) =>
3
- `${PlaygroundApiUrl}/v1/playground/agents`,
4
- AgentRun: (PlaygroundApiUrl: string) =>
5
- `${PlaygroundApiUrl}/v1/playground/agents/{agent_id}/runs`,
6
- PlaygroundStatus: (PlaygroundApiUrl: string) =>
7
- `${PlaygroundApiUrl}/v1/playground/status`,
8
- GetPlaygroundSessions: (PlaygroundApiUrl: string, agentId: string) =>
9
- `${PlaygroundApiUrl}/v1/playground/agents/${agentId}/sessions`,
10
- GetPlaygroundSession: (
11
- PlaygroundApiUrl: string,
12
- agentId: string,
13
- sessionId: string
14
- ) =>
15
- `${PlaygroundApiUrl}/v1/playground/agents/${agentId}/sessions/${sessionId}`,
16
 
17
- DeletePlaygroundSession: (
18
- PlaygroundApiUrl: string,
19
- agentId: string,
20
- sessionId: string
21
- ) =>
22
- `${PlaygroundApiUrl}/v1/playground/agents/${agentId}/sessions/${sessionId}`
 
 
23
  }
 
1
  export const APIRoutes = {
2
+ GetAgents: (agentOSUrl: string) => `${agentOSUrl}/agents`,
3
+ AgentRun: (agentOSUrl: string) => `${agentOSUrl}/agents/{agent_id}/runs`,
4
+ Status: (agentOSUrl: string) => `${agentOSUrl}/health`,
5
+ GetSessions: (agentOSUrl: string) => `${agentOSUrl}/sessions`,
6
+ GetSession: (agentOSUrl: string, sessionId: string) =>
7
+ `${agentOSUrl}/sessions/${sessionId}/runs`,
 
 
 
 
 
 
 
 
8
 
9
+ DeleteSession: (agentOSUrl: string, sessionId: string) =>
10
+ `${agentOSUrl}/sessions/${sessionId}`,
11
+
12
+ GetTeams: (agentOSUrl: string) => `${agentOSUrl}/teams`,
13
+ TeamRun: (agentOSUrl: string, teamId: string) =>
14
+ `${agentOSUrl}/teams/${teamId}/runs`,
15
+ DeleteTeamSession: (agentOSUrl: string, teamId: string, sessionId: string) =>
16
+ `${agentOSUrl}/v1//teams/${teamId}/sessions/${sessionId}`
17
  }
src/app/page.tsx CHANGED
@@ -1,6 +1,6 @@
1
  'use client'
2
- import Sidebar from '@/components/playground/Sidebar/Sidebar'
3
- import { ChatArea } from '@/components/playground/ChatArea'
4
  import { Suspense } from 'react'
5
 
6
  export default function Home() {
 
1
  'use client'
2
+ import Sidebar from '@/components/chat/Sidebar/Sidebar'
3
+ import { ChatArea } from '@/components/chat/ChatArea'
4
  import { Suspense } from 'react'
5
 
6
  export default function Home() {
src/components/{playground β†’ chat}/ChatArea/ChatArea.tsx RENAMED
File without changes
src/components/{playground β†’ chat}/ChatArea/ChatInput/ChatInput.tsx RENAMED
@@ -3,18 +3,19 @@ import { useState } from 'react'
3
  import { toast } from 'sonner'
4
  import { TextArea } from '@/components/ui/textarea'
5
  import { Button } from '@/components/ui/button'
6
- import { usePlaygroundStore } from '@/store'
7
  import useAIChatStreamHandler from '@/hooks/useAIStreamHandler'
8
  import { useQueryState } from 'nuqs'
9
  import Icon from '@/components/ui/icon'
10
 
11
  const ChatInput = () => {
12
- const { chatInputRef } = usePlaygroundStore()
13
 
14
  const { handleStreamResponse } = useAIChatStreamHandler()
15
  const [selectedAgent] = useQueryState('agent')
 
16
  const [inputMessage, setInputMessage] = useState('')
17
- const isStreaming = usePlaygroundStore((state) => state.isStreaming)
18
  const handleSubmit = async () => {
19
  if (!inputMessage.trim()) return
20
 
@@ -50,12 +51,14 @@ const ChatInput = () => {
50
  }
51
  }}
52
  className="w-full border border-accent bg-primaryAccent px-4 text-sm text-primary focus:border-accent"
53
- disabled={!selectedAgent}
54
  ref={chatInputRef}
55
  />
56
  <Button
57
  onClick={handleSubmit}
58
- disabled={!selectedAgent || !inputMessage.trim() || isStreaming}
 
 
59
  size="icon"
60
  className="rounded-xl bg-primary p-5 text-primaryAccent"
61
  >
 
3
  import { toast } from 'sonner'
4
  import { TextArea } from '@/components/ui/textarea'
5
  import { Button } from '@/components/ui/button'
6
+ import { useStore } from '@/store'
7
  import useAIChatStreamHandler from '@/hooks/useAIStreamHandler'
8
  import { useQueryState } from 'nuqs'
9
  import Icon from '@/components/ui/icon'
10
 
11
  const ChatInput = () => {
12
+ const { chatInputRef } = useStore()
13
 
14
  const { handleStreamResponse } = useAIChatStreamHandler()
15
  const [selectedAgent] = useQueryState('agent')
16
+ const [teamId] = useQueryState('team')
17
  const [inputMessage, setInputMessage] = useState('')
18
+ const isStreaming = useStore((state) => state.isStreaming)
19
  const handleSubmit = async () => {
20
  if (!inputMessage.trim()) return
21
 
 
51
  }
52
  }}
53
  className="w-full border border-accent bg-primaryAccent px-4 text-sm text-primary focus:border-accent"
54
+ disabled={!(selectedAgent || teamId)}
55
  ref={chatInputRef}
56
  />
57
  <Button
58
  onClick={handleSubmit}
59
+ disabled={
60
+ !(selectedAgent || teamId) || !inputMessage.trim() || isStreaming
61
+ }
62
  size="icon"
63
  className="rounded-xl bg-primary p-5 text-primaryAccent"
64
  >
src/components/{playground β†’ chat}/ChatArea/ChatInput/index.ts RENAMED
File without changes
src/components/{playground β†’ chat}/ChatArea/MessageArea.tsx RENAMED
@@ -1,12 +1,12 @@
1
  'use client'
2
 
3
- import { usePlaygroundStore } from '@/store'
4
  import Messages from './Messages'
5
- import ScrollToBottom from '@/components/playground/ChatArea/ScrollToBottom'
6
  import { StickToBottom } from 'use-stick-to-bottom'
7
 
8
  const MessageArea = () => {
9
- const { messages } = usePlaygroundStore()
10
 
11
  return (
12
  <StickToBottom
 
1
  'use client'
2
 
3
+ import { useStore } from '@/store'
4
  import Messages from './Messages'
5
+ import ScrollToBottom from '@/components/chat/ChatArea/ScrollToBottom'
6
  import { StickToBottom } from 'use-stick-to-bottom'
7
 
8
  const MessageArea = () => {
9
+ const { messages } = useStore()
10
 
11
  return (
12
  <StickToBottom
src/components/{playground β†’ chat}/ChatArea/Messages/AgentThinkingLoader.tsx RENAMED
File without changes
src/components/{playground β†’ chat}/ChatArea/Messages/ChatBlankState.tsx RENAMED
@@ -8,7 +8,7 @@ import React, { useState } from 'react'
8
 
9
  const EXTERNAL_LINKS = {
10
  documentation: 'https://agno.link/agent-ui',
11
- playground: 'https://app.agno.com/playground/agents',
12
  agno: 'https://agno.com'
13
  }
14
 
@@ -172,7 +172,7 @@ const ChatBlankState = () => {
172
  </div>
173
  </span>
174
  </div>
175
- <p>For the full experience, visit the Agent Playground.</p>
176
  </motion.h1>
177
  <motion.div
178
  initial={{ opacity: 0, y: 10 }}
@@ -185,10 +185,7 @@ const ChatBlankState = () => {
185
  variant="primary"
186
  text="GO TO DOCS"
187
  />
188
- <ActionButton
189
- href={EXTERNAL_LINKS.playground}
190
- text="VISIT AGENT PLAYGROUND"
191
- />
192
  </motion.div>
193
  </div>
194
  </section>
 
8
 
9
  const EXTERNAL_LINKS = {
10
  documentation: 'https://agno.link/agent-ui',
11
+ agenOS: 'https://os.agno.com',
12
  agno: 'https://agno.com'
13
  }
14
 
 
172
  </div>
173
  </span>
174
  </div>
175
+ <p>For the full experience, visit the AgentOS</p>
176
  </motion.h1>
177
  <motion.div
178
  initial={{ opacity: 0, y: 10 }}
 
185
  variant="primary"
186
  text="GO TO DOCS"
187
  />
188
+ <ActionButton href={EXTERNAL_LINKS.agenOS} text="VISIT AGENTOS" />
 
 
 
189
  </motion.div>
190
  </div>
191
  </section>
src/components/{playground β†’ chat}/ChatArea/Messages/MessageItem.tsx RENAMED
@@ -1,7 +1,7 @@
1
  import Icon from '@/components/ui/icon'
2
  import MarkdownRenderer from '@/components/ui/typography/MarkdownRenderer'
3
- import { usePlaygroundStore } from '@/store'
4
- import type { PlaygroundChatMessage } from '@/types/playground'
5
  import Videos from './Multimedia/Videos'
6
  import Images from './Multimedia/Images'
7
  import Audios from './Multimedia/Audios'
@@ -9,11 +9,11 @@ import { memo } from 'react'
9
  import AgentThinkingLoader from './AgentThinkingLoader'
10
 
11
  interface MessageProps {
12
- message: PlaygroundChatMessage
13
  }
14
 
15
  const AgentMessage = ({ message }: MessageProps) => {
16
- const { streamingErrorMessage } = usePlaygroundStore()
17
  let messageContent
18
  if (message.streamingError) {
19
  messageContent = (
@@ -80,14 +80,12 @@ const AgentMessage = ({ message }: MessageProps) => {
80
 
81
  const UserMessage = memo(({ message }: MessageProps) => {
82
  return (
83
- <div className="flex items-start pt-4 text-start max-md:break-words">
84
- <div className="flex flex-row gap-x-3">
85
- <p className="flex items-center gap-x-2 text-sm font-medium text-muted">
86
- <Icon type="user" size="sm" />
87
- </p>
88
- <div className="text-md rounded-lg py-1 font-geist text-secondary">
89
- {message.content}
90
- </div>
91
  </div>
92
  </div>
93
  )
 
1
  import Icon from '@/components/ui/icon'
2
  import MarkdownRenderer from '@/components/ui/typography/MarkdownRenderer'
3
+ import { useStore } from '@/store'
4
+ import type { ChatMessage } from '@/types/os'
5
  import Videos from './Multimedia/Videos'
6
  import Images from './Multimedia/Images'
7
  import Audios from './Multimedia/Audios'
 
9
  import AgentThinkingLoader from './AgentThinkingLoader'
10
 
11
  interface MessageProps {
12
+ message: ChatMessage
13
  }
14
 
15
  const AgentMessage = ({ message }: MessageProps) => {
16
+ const { streamingErrorMessage } = useStore()
17
  let messageContent
18
  if (message.streamingError) {
19
  messageContent = (
 
80
 
81
  const UserMessage = memo(({ message }: MessageProps) => {
82
  return (
83
+ <div className="flex items-start gap-4 pt-4 text-start max-md:break-words">
84
+ <div className="flex-shrink-0">
85
+ <Icon type="user" size="sm" />
86
+ </div>
87
+ <div className="text-md rounded-lg font-geist text-secondary">
88
+ {message.content}
 
 
89
  </div>
90
  </div>
91
  )
src/components/{playground β†’ chat}/ChatArea/Messages/Messages.tsx RENAMED
@@ -1,4 +1,4 @@
1
- import type { PlaygroundChatMessage } from '@/types/playground'
2
 
3
  import { AgentMessage, UserMessage } from './MessageItem'
4
  import Tooltip from '@/components/ui/tooltip'
@@ -9,17 +9,18 @@ import {
9
  ReasoningProps,
10
  ReferenceData,
11
  Reference
12
- } from '@/types/playground'
13
  import React, { type FC } from 'react'
14
- import ChatBlankState from './ChatBlankState'
15
  import Icon from '@/components/ui/icon'
 
16
 
17
  interface MessageListProps {
18
- messages: PlaygroundChatMessage[]
19
  }
20
 
21
  interface MessageWrapperProps {
22
- message: PlaygroundChatMessage
23
  isLastMessage: boolean
24
  }
25
 
@@ -93,7 +94,7 @@ const AgentMessageWrapper = ({ message }: MessageWrapperProps) => {
93
  </div>
94
  )}
95
  {message.tool_calls && message.tool_calls.length > 0 && (
96
- <div className="flex items-center gap-3">
97
  <Tooltip
98
  delayDuration={0}
99
  content={<p className="text-accent">Tool Calls</p>}
 
1
+ import type { ChatMessage } from '@/types/os'
2
 
3
  import { AgentMessage, UserMessage } from './MessageItem'
4
  import Tooltip from '@/components/ui/tooltip'
 
9
  ReasoningProps,
10
  ReferenceData,
11
  Reference
12
+ } from '@/types/os'
13
  import React, { type FC } from 'react'
14
+
15
  import Icon from '@/components/ui/icon'
16
+ import ChatBlankState from './ChatBlankState'
17
 
18
  interface MessageListProps {
19
+ messages: ChatMessage[]
20
  }
21
 
22
  interface MessageWrapperProps {
23
+ message: ChatMessage
24
  isLastMessage: boolean
25
  }
26
 
 
94
  </div>
95
  )}
96
  {message.tool_calls && message.tool_calls.length > 0 && (
97
+ <div className="flex items-start gap-3">
98
  <Tooltip
99
  delayDuration={0}
100
  content={<p className="text-accent">Tool Calls</p>}
src/components/{playground β†’ chat}/ChatArea/Messages/Multimedia/Audios/Audios.tsx RENAMED
@@ -2,7 +2,7 @@
2
 
3
  import { memo, useMemo } from 'react'
4
 
5
- import { type AudioData } from '@/types/playground'
6
  import { decodeBase64Audio } from '@/lib/audio'
7
 
8
  /**
 
2
 
3
  import { memo, useMemo } from 'react'
4
 
5
+ import { type AudioData } from '@/types/os'
6
  import { decodeBase64Audio } from '@/lib/audio'
7
 
8
  /**
src/components/{playground β†’ chat}/ChatArea/Messages/Multimedia/Audios/index.ts RENAMED
File without changes
src/components/{playground β†’ chat}/ChatArea/Messages/Multimedia/Images/Images.tsx RENAMED
@@ -1,6 +1,6 @@
1
  import { memo } from 'react'
2
 
3
- import { type ImageData } from '@/types/playground'
4
  import { cn } from '@/lib/utils'
5
 
6
  const Images = ({ images }: { images: ImageData[] }) => (
 
1
  import { memo } from 'react'
2
 
3
+ import { type ImageData } from '@/types/os'
4
  import { cn } from '@/lib/utils'
5
 
6
  const Images = ({ images }: { images: ImageData[] }) => (
src/components/{playground β†’ chat}/ChatArea/Messages/Multimedia/Images/index.ts RENAMED
File without changes
src/components/{playground β†’ chat}/ChatArea/Messages/Multimedia/Videos/Videos.tsx RENAMED
@@ -4,7 +4,7 @@ import { memo } from 'react'
4
 
5
  import { toast } from 'sonner'
6
 
7
- import { type VideoData } from '@/types/playground'
8
  import Icon from '@/components/ui/icon'
9
 
10
  const VideoItem = memo(({ video }: { video: VideoData }) => {
 
4
 
5
  import { toast } from 'sonner'
6
 
7
+ import { type VideoData } from '@/types/os'
8
  import Icon from '@/components/ui/icon'
9
 
10
  const VideoItem = memo(({ video }: { video: VideoData }) => {
src/components/{playground β†’ chat}/ChatArea/Messages/Multimedia/Videos/index.ts RENAMED
File without changes
src/components/{playground β†’ chat}/ChatArea/Messages/index.ts RENAMED
File without changes
src/components/{playground β†’ chat}/ChatArea/ScrollToBottom.tsx RENAMED
File without changes
src/components/{playground β†’ chat}/ChatArea/index.ts RENAMED
File without changes
src/components/{playground/Sidebar/AgentSelector.tsx β†’ chat/Sidebar/EntitySelector.tsx} RENAMED
@@ -8,81 +8,99 @@ import {
8
  SelectContent,
9
  SelectItem
10
  } from '@/components/ui/select'
11
- import { usePlaygroundStore } from '@/store'
12
  import { useQueryState } from 'nuqs'
13
  import Icon from '@/components/ui/icon'
14
  import { useEffect } from 'react'
15
  import useChatActions from '@/hooks/useChatActions'
16
 
17
- export function AgentSelector() {
18
- const { agents, setMessages, setSelectedModel, setHasStorage } =
19
- usePlaygroundStore()
20
  const { focusChatInput } = useChatActions()
21
  const [agentId, setAgentId] = useQueryState('agent', {
22
  parse: (value) => value || undefined,
23
  history: 'push'
24
  })
 
 
 
 
25
  const [, setSessionId] = useQueryState('session')
26
 
27
- // Set the model when the component mounts if an agent is already selected
 
 
 
28
  useEffect(() => {
29
- if (agentId && agents.length > 0) {
30
- const agent = agents.find((agent) => agent.value === agentId)
31
- if (agent) {
32
- setSelectedModel(agent.model.provider || '')
33
- setHasStorage(!!agent.storage)
34
- if (agent.model.provider) {
 
 
35
  focusChatInput()
36
  }
37
- } else {
38
- setAgentId(agents[0].value)
39
  }
40
  }
41
  // eslint-disable-next-line react-hooks/exhaustive-deps
42
- }, [agentId, agents, setSelectedModel])
43
 
44
  const handleOnValueChange = (value: string) => {
45
- const newAgent = value === agentId ? '' : value
46
- const selectedAgent = agents.find((agent) => agent.value === newAgent)
47
- setSelectedModel(selectedAgent?.model.provider || '')
48
- setHasStorage(!!selectedAgent?.storage)
49
- setAgentId(newAgent)
 
 
 
 
 
 
 
 
50
  setMessages([])
51
  setSessionId(null)
52
- if (selectedAgent?.model.provider) {
 
53
  focusChatInput()
54
  }
55
  }
56
 
 
 
 
 
 
 
 
 
 
 
57
  return (
58
  <Select
59
- value={agentId || ''}
60
  onValueChange={(value) => handleOnValueChange(value)}
61
  >
62
  <SelectTrigger className="h-9 w-full rounded-xl border border-primary/15 bg-primaryAccent text-xs font-medium uppercase">
63
- <SelectValue placeholder="Select Agent" />
64
  </SelectTrigger>
65
  <SelectContent className="border-none bg-primaryAccent font-dmmono shadow-lg">
66
- {agents.map((agent, index) => (
67
  <SelectItem
68
  className="cursor-pointer"
69
- key={`${agent.value}-${index}`}
70
- value={agent.value}
71
  >
72
  <div className="flex items-center gap-3 text-xs font-medium uppercase">
73
- <Icon type={'agent'} size="xs" />
74
- {agent.label}
75
  </div>
76
  </SelectItem>
77
  ))}
78
- {agents.length === 0 && (
79
- <SelectItem
80
- value="no-agents"
81
- className="cursor-not-allowed select-none text-center"
82
- >
83
- No agents found
84
- </SelectItem>
85
- )}
86
  </SelectContent>
87
  </Select>
88
  )
 
8
  SelectContent,
9
  SelectItem
10
  } from '@/components/ui/select'
11
+ import { useStore } from '@/store'
12
  import { useQueryState } from 'nuqs'
13
  import Icon from '@/components/ui/icon'
14
  import { useEffect } from 'react'
15
  import useChatActions from '@/hooks/useChatActions'
16
 
17
+ export function EntitySelector() {
18
+ const { mode, agents, teams, setMessages, setSelectedModel } = useStore()
19
+
20
  const { focusChatInput } = useChatActions()
21
  const [agentId, setAgentId] = useQueryState('agent', {
22
  parse: (value) => value || undefined,
23
  history: 'push'
24
  })
25
+ const [teamId, setTeamId] = useQueryState('team', {
26
+ parse: (value) => value || undefined,
27
+ history: 'push'
28
+ })
29
  const [, setSessionId] = useQueryState('session')
30
 
31
+ const currentEntities = mode === 'team' ? teams : agents
32
+ const currentValue = mode === 'team' ? teamId : agentId
33
+ const placeholder = mode === 'team' ? 'Select Team' : 'Select Agent'
34
+
35
  useEffect(() => {
36
+ if (currentValue && currentEntities.length > 0) {
37
+ const entity = currentEntities.find((item) => item.id === currentValue)
38
+ if (entity) {
39
+ setSelectedModel(entity.model?.model || '')
40
+ if (mode === 'team') {
41
+ setTeamId(entity.id)
42
+ }
43
+ if (entity.model?.model) {
44
  focusChatInput()
45
  }
 
 
46
  }
47
  }
48
  // eslint-disable-next-line react-hooks/exhaustive-deps
49
+ }, [currentValue, currentEntities, setSelectedModel, mode])
50
 
51
  const handleOnValueChange = (value: string) => {
52
+ const newValue = value === currentValue ? null : value
53
+ const selectedEntity = currentEntities.find((item) => item.id === newValue)
54
+
55
+ setSelectedModel(selectedEntity?.model?.provider || '')
56
+
57
+ if (mode === 'team') {
58
+ setTeamId(newValue)
59
+ setAgentId(null)
60
+ } else {
61
+ setAgentId(newValue)
62
+ setTeamId(null)
63
+ }
64
+
65
  setMessages([])
66
  setSessionId(null)
67
+
68
+ if (selectedEntity?.model?.provider) {
69
  focusChatInput()
70
  }
71
  }
72
 
73
+ if (currentEntities.length === 0) {
74
+ return (
75
+ <Select disabled>
76
+ <SelectTrigger className="h-9 w-full rounded-xl border border-primary/15 bg-primaryAccent text-xs font-medium uppercase opacity-50">
77
+ <SelectValue placeholder={`No ${mode}s Available`} />
78
+ </SelectTrigger>
79
+ </Select>
80
+ )
81
+ }
82
+
83
  return (
84
  <Select
85
+ value={currentValue || ''}
86
  onValueChange={(value) => handleOnValueChange(value)}
87
  >
88
  <SelectTrigger className="h-9 w-full rounded-xl border border-primary/15 bg-primaryAccent text-xs font-medium uppercase">
89
+ <SelectValue placeholder={placeholder} />
90
  </SelectTrigger>
91
  <SelectContent className="border-none bg-primaryAccent font-dmmono shadow-lg">
92
+ {currentEntities.map((entity, index) => (
93
  <SelectItem
94
  className="cursor-pointer"
95
+ key={`${entity.id}-${index}`}
96
+ value={entity.id}
97
  >
98
  <div className="flex items-center gap-3 text-xs font-medium uppercase">
99
+ <Icon type={'user'} size="xs" />
100
+ {entity.name || entity.id}
101
  </div>
102
  </SelectItem>
103
  ))}
 
 
 
 
 
 
 
 
104
  </SelectContent>
105
  </Select>
106
  )
src/components/chat/Sidebar/ModeSelector.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import {
5
+ Select,
6
+ SelectTrigger,
7
+ SelectValue,
8
+ SelectContent,
9
+ SelectItem
10
+ } from '@/components/ui/select'
11
+ import { useStore } from '@/store'
12
+ import { useQueryState } from 'nuqs'
13
+ import useChatActions from '@/hooks/useChatActions'
14
+
15
+ export function ModeSelector() {
16
+ const { mode, setMode, setMessages, setSelectedModel } = useStore()
17
+ const { clearChat } = useChatActions()
18
+ const [, setAgentId] = useQueryState('agent')
19
+ const [, setTeamId] = useQueryState('team')
20
+ const [, setSessionId] = useQueryState('session')
21
+
22
+ const handleModeChange = (newMode: 'agent' | 'team') => {
23
+ if (newMode === mode) return
24
+
25
+ setMode(newMode)
26
+
27
+ setAgentId(null)
28
+ setTeamId(null)
29
+ setSelectedModel('')
30
+ setMessages([])
31
+ setSessionId(null)
32
+ clearChat()
33
+ }
34
+
35
+ return (
36
+ <>
37
+ <Select
38
+ defaultValue={mode}
39
+ value={mode}
40
+ onValueChange={(value) => handleModeChange(value as 'agent' | 'team')}
41
+ >
42
+ <SelectTrigger className="h-9 w-full rounded-xl border border-primary/15 bg-primaryAccent text-xs font-medium uppercase">
43
+ <SelectValue />
44
+ </SelectTrigger>
45
+ <SelectContent className="border-none bg-primaryAccent font-dmmono shadow-lg">
46
+ <SelectItem value="agent" className="cursor-pointer">
47
+ <div className="text-xs font-medium uppercase">Agent</div>
48
+ </SelectItem>
49
+
50
+ <SelectItem value="team" className="cursor-pointer">
51
+ <div className="text-xs font-medium uppercase">Team</div>
52
+ </SelectItem>
53
+ </SelectContent>
54
+ </Select>
55
+ </>
56
+ )
57
+ }
src/components/{playground β†’ chat}/Sidebar/NewChatButton.tsx RENAMED
@@ -3,11 +3,11 @@
3
  import { Button } from '@/components/ui/button'
4
  import Icon from '@/components/ui/icon'
5
  import useChatActions from '@/hooks/useChatActions'
6
- import { usePlaygroundStore } from '@/store'
7
 
8
  function NewChatButton() {
9
  const { clearChat } = useChatActions()
10
- const { messages } = usePlaygroundStore()
11
  return (
12
  <Button
13
  className="z-10 cursor-pointer rounded bg-brand px-4 py-2 font-bold text-primary hover:bg-brand/80 disabled:cursor-not-allowed disabled:opacity-50"
 
3
  import { Button } from '@/components/ui/button'
4
  import Icon from '@/components/ui/icon'
5
  import useChatActions from '@/hooks/useChatActions'
6
+ import { useStore } from '@/store'
7
 
8
  function NewChatButton() {
9
  const { clearChat } = useChatActions()
10
+ const { messages } = useStore()
11
  return (
12
  <Button
13
  className="z-10 cursor-pointer rounded bg-brand px-4 py-2 font-bold text-primary hover:bg-brand/80 disabled:cursor-not-allowed disabled:opacity-50"
src/components/{playground β†’ chat}/Sidebar/Sessions/DeleteSessionModal.tsx RENAMED
File without changes
src/components/{playground β†’ chat}/Sidebar/Sessions/SessionBlankState.tsx RENAMED
@@ -1,7 +1,6 @@
1
  import React from 'react'
2
- import { usePlaygroundStore } from '@/store'
3
  import { useQueryState } from 'nuqs'
4
- import Link from 'next/link'
5
 
6
  const HistoryBlankStateIcon = () => (
7
  <svg
@@ -87,8 +86,7 @@ const HistoryBlankStateIcon = () => (
87
  )
88
 
89
  const SessionBlankState = () => {
90
- const { selectedEndpoint, isEndpointActive, hasStorage } =
91
- usePlaygroundStore()
92
  const [agentId] = useQueryState('agent')
93
 
94
  const errorMessage = (() => {
@@ -99,20 +97,6 @@ const SessionBlankState = () => {
99
  return 'Select an endpoint to see the history.'
100
  case !agentId:
101
  return 'Select an agent to see the history.'
102
- case !hasStorage:
103
- return (
104
- <>
105
- Connect{' '}
106
- <Link
107
- className="underline"
108
- href={'https://docs.agno.com/storage'}
109
- target="_blank"
110
- >
111
- storage
112
- </Link>{' '}
113
- to your agent to see sessions.{' '}
114
- </>
115
- )
116
  default:
117
  return 'No session records yet. Start a conversation to create one.'
118
  }
 
1
  import React from 'react'
2
+ import { useStore } from '@/store'
3
  import { useQueryState } from 'nuqs'
 
4
 
5
  const HistoryBlankStateIcon = () => (
6
  <svg
 
86
  )
87
 
88
  const SessionBlankState = () => {
89
+ const { selectedEndpoint, isEndpointActive } = useStore()
 
90
  const [agentId] = useQueryState('agent')
91
 
92
  const errorMessage = (() => {
 
97
  return 'Select an endpoint to see the history.'
98
  case !agentId:
99
  return 'Select an agent to see the history.'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  default:
101
  return 'No session records yet. Start a conversation to create one.'
102
  }
src/components/{playground β†’ chat}/Sidebar/Sessions/SessionItem.tsx RENAMED
@@ -1,9 +1,9 @@
1
  import { useQueryState } from 'nuqs'
2
- import { SessionEntry } from '@/types/playground'
3
  import { Button } from '../../../ui/button'
4
  import useSessionLoader from '@/hooks/useSessionLoader'
5
- import { deletePlaygroundSessionAPI } from '@/api/playground'
6
- import { usePlaygroundStore } from '@/store'
7
  import { toast } from 'sonner'
8
  import Icon from '@/components/ui/icon'
9
  import { useState } from 'react'
@@ -13,62 +13,83 @@ import { truncateText, cn } from '@/lib/utils'
13
 
14
  type SessionItemProps = SessionEntry & {
15
  isSelected: boolean
 
16
  onSessionClick: () => void
17
  }
18
  const SessionItem = ({
19
- title,
20
  session_id,
21
  isSelected,
 
22
  onSessionClick
23
  }: SessionItemProps) => {
24
  const [agentId] = useQueryState('agent')
25
- const { getSession } = useSessionLoader()
 
26
  const [, setSessionId] = useQueryState('session')
27
- const { selectedEndpoint, sessionsData, setSessionsData } =
28
- usePlaygroundStore()
29
  const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
 
30
  const { clearChat } = useChatActions()
31
 
32
  const handleGetSession = async () => {
33
- if (agentId) {
34
- onSessionClick()
35
- await getSession(session_id, agentId)
36
- setSessionId(session_id)
37
- }
 
 
 
 
 
 
 
 
38
  }
39
 
40
  const handleDeleteSession = async () => {
41
- if (agentId) {
42
- try {
43
- const response = await deletePlaygroundSessionAPI(
44
- selectedEndpoint,
45
- agentId,
46
- session_id
47
- )
48
- if (response.status === 200 && sessionsData) {
49
- setSessionsData(
50
- sessionsData.filter((session) => session.session_id !== session_id)
51
- )
 
 
 
52
  clearChat()
53
- toast.success('Session deleted')
54
- } else {
55
- toast.error('Failed to delete session')
56
  }
57
- } catch {
58
- toast.error('Failed to delete session')
59
- } finally {
60
- setIsDeleteModalOpen(false)
 
 
61
  }
 
 
 
 
 
 
 
62
  }
63
  }
64
  return (
65
  <>
66
  <div
67
  className={cn(
68
- 'group flex h-11 w-full cursor-pointer items-center justify-between rounded-lg px-3 py-2 transition-colors duration-200',
69
  isSelected
70
  ? 'cursor-default bg-primary/10'
71
- : 'bg-background-secondary hover:bg-background-secondary/80'
72
  )}
73
  onClick={handleGetSession}
74
  >
@@ -95,7 +116,7 @@ const SessionItem = ({
95
  isOpen={isDeleteModalOpen}
96
  onClose={() => setIsDeleteModalOpen(false)}
97
  onDelete={handleDeleteSession}
98
- isDeleting={false}
99
  />
100
  </>
101
  )
 
1
  import { useQueryState } from 'nuqs'
2
+ import { SessionEntry } from '@/types/os'
3
  import { Button } from '../../../ui/button'
4
  import useSessionLoader from '@/hooks/useSessionLoader'
5
+ import { deleteSessionAPI } from '@/api/os'
6
+ import { useStore } from '@/store'
7
  import { toast } from 'sonner'
8
  import Icon from '@/components/ui/icon'
9
  import { useState } from 'react'
 
13
 
14
  type SessionItemProps = SessionEntry & {
15
  isSelected: boolean
16
+ currentSessionId: string | null
17
  onSessionClick: () => void
18
  }
19
  const SessionItem = ({
20
+ session_name: title,
21
  session_id,
22
  isSelected,
23
+ currentSessionId,
24
  onSessionClick
25
  }: SessionItemProps) => {
26
  const [agentId] = useQueryState('agent')
27
+ const [teamId] = useQueryState('team')
28
+ const [dbId] = useQueryState('db_id')
29
  const [, setSessionId] = useQueryState('session')
30
+ const { getSession } = useSessionLoader()
31
+ const { selectedEndpoint, sessionsData, setSessionsData, mode } = useStore()
32
  const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
33
+ const [isDeleting, setIsDeleting] = useState(false)
34
  const { clearChat } = useChatActions()
35
 
36
  const handleGetSession = async () => {
37
+ if (!(agentId || teamId || dbId)) return
38
+
39
+ onSessionClick()
40
+ await getSession(
41
+ {
42
+ entityType: mode,
43
+ agentId,
44
+ teamId,
45
+ dbId: dbId ?? ''
46
+ },
47
+ session_id
48
+ )
49
+ setSessionId(session_id)
50
  }
51
 
52
  const handleDeleteSession = async () => {
53
+ if (!(agentId || teamId || dbId)) return
54
+ setIsDeleting(true)
55
+ try {
56
+ const response = await deleteSessionAPI(
57
+ selectedEndpoint,
58
+ dbId ?? '',
59
+ session_id
60
+ )
61
+
62
+ if (response?.ok && sessionsData) {
63
+ setSessionsData(sessionsData.filter((s) => s.session_id !== session_id))
64
+ // If the deleted session was the active one, clear the chat
65
+ if (currentSessionId === session_id) {
66
+ setSessionId(null)
67
  clearChat()
 
 
 
68
  }
69
+ toast.success('Session deleted')
70
+ } else {
71
+ const errorMsg = await response?.text()
72
+ toast.error(
73
+ `Failed to delete session: ${response?.statusText || 'Unknown error'} ${errorMsg || ''}`
74
+ )
75
  }
76
+ } catch (error) {
77
+ toast.error(
78
+ `Failed to delete session: ${error instanceof Error ? error.message : String(error)}`
79
+ )
80
+ } finally {
81
+ setIsDeleteModalOpen(false)
82
+ setIsDeleting(false)
83
  }
84
  }
85
  return (
86
  <>
87
  <div
88
  className={cn(
89
+ 'group flex h-11 w-full items-center justify-between rounded-lg px-3 py-2 transition-colors duration-200',
90
  isSelected
91
  ? 'cursor-default bg-primary/10'
92
+ : 'cursor-pointer bg-background-secondary hover:bg-background-secondary/80'
93
  )}
94
  onClick={handleGetSession}
95
  >
 
116
  isOpen={isDeleteModalOpen}
117
  onClose={() => setIsDeleteModalOpen(false)}
118
  onDelete={handleDeleteSession}
119
+ isDeleting={isDeleting}
120
  />
121
  </>
122
  )
src/components/{playground β†’ chat}/Sidebar/Sessions/Sessions.tsx RENAMED
@@ -1,74 +1,65 @@
1
  'use client'
2
 
3
- import { useEffect, useMemo, useState, useRef, useCallback } from 'react'
4
- import dayjs from 'dayjs'
5
- import utc from 'dayjs/plugin/utc'
6
-
7
- import { usePlaygroundStore } from '@/store'
8
  import { useQueryState } from 'nuqs'
9
- import SessionItem from './SessionItem'
10
- import SessionBlankState from './SessionBlankState'
11
  import useSessionLoader from '@/hooks/useSessionLoader'
12
 
13
- import { cn } from '@/lib/utils'
14
- import { FC } from 'react'
15
  import { Skeleton } from '@/components/ui/skeleton'
 
16
 
17
  interface SkeletonListProps {
18
  skeletonCount: number
19
  }
20
-
21
  const SkeletonList: FC<SkeletonListProps> = ({ skeletonCount }) => {
22
- const skeletons = useMemo(
23
  () => Array.from({ length: skeletonCount }, (_, i) => i),
24
  [skeletonCount]
25
  )
26
 
27
- return skeletons.map((skeleton, index) => (
28
  <Skeleton
29
- key={skeleton}
30
  className={cn(
31
  'mb-1 h-11 rounded-lg px-3 py-2',
32
- index > 0 && 'bg-background-secondary'
33
  )}
34
  />
35
  ))
36
  }
37
 
38
- dayjs.extend(utc)
39
-
40
- const formatDate = (
41
- timestamp: number,
42
- format: 'natural' | 'full' = 'full'
43
- ): string => {
44
- const date = dayjs.unix(timestamp).utc()
45
- return format === 'natural'
46
- ? date.format('HH:mm')
47
- : date.format('YYYY-MM-DD HH:mm:ss')
48
- }
49
-
50
  const Sessions = () => {
51
  const [agentId] = useQueryState('agent', {
52
- parse: (value) => value || undefined,
53
  history: 'push'
54
  })
 
55
  const [sessionId] = useQueryState('session')
 
 
56
  const {
57
  selectedEndpoint,
 
58
  isEndpointActive,
59
  isEndpointLoading,
60
- sessionsData,
61
  hydrated,
62
- hasStorage,
63
- setSessionsData
64
- } = usePlaygroundStore()
 
 
 
 
65
  const [isScrolling, setIsScrolling] = useState(false)
66
  const [selectedSessionId, setSelectedSessionId] = useState<string | null>(
67
  null
68
  )
69
- const { getSession, getSessions } = useSessionLoader()
 
70
  const scrollTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null)
71
- const { isSessionsLoading } = usePlaygroundStore()
72
 
73
  const handleScroll = () => {
74
  setIsScrolling(true)
@@ -91,54 +82,48 @@ const Sessions = () => {
91
  }
92
  }, [])
93
 
94
- // Load a session on render if a session id exists in url
95
  useEffect(() => {
96
- if (sessionId && agentId && selectedEndpoint && hydrated) {
97
- getSession(sessionId, agentId)
 
98
  }
99
  // eslint-disable-next-line react-hooks/exhaustive-deps
100
- }, [hydrated])
101
 
102
  useEffect(() => {
103
- if (!selectedEndpoint || !agentId || !hasStorage) {
104
- setSessionsData(() => null)
 
105
  return
106
  }
107
- if (!isEndpointLoading) {
108
- setSessionsData(() => null)
109
- getSessions(agentId)
110
- }
 
 
 
 
111
  }, [
112
  selectedEndpoint,
113
  agentId,
114
- getSessions,
 
115
  isEndpointLoading,
116
- hasStorage,
117
- setSessionsData
118
  ])
119
 
120
  useEffect(() => {
121
- if (sessionId) {
122
- setSelectedSessionId(sessionId)
123
- }
124
  }, [sessionId])
125
 
126
- const formattedSessionsData = useMemo(() => {
127
- if (!sessionsData || !Array.isArray(sessionsData)) return []
128
-
129
- return sessionsData.map((entry) => ({
130
- ...entry,
131
- created_at: entry.created_at,
132
- formatted_time: formatDate(entry.created_at, 'natural')
133
- }))
134
- }, [sessionsData])
135
-
136
  const handleSessionClick = useCallback(
137
  (id: string) => () => setSelectedSessionId(id),
138
  []
139
  )
140
 
141
- if (isSessionsLoading || isEndpointLoading)
142
  return (
143
  <div className="w-full">
144
  <div className="mb-2 text-xs font-medium uppercase">Sessions</div>
@@ -147,27 +132,36 @@ const Sessions = () => {
147
  </div>
148
  </div>
149
  )
 
 
150
  return (
151
  <div className="w-full">
152
  <div className="mb-2 w-full text-xs font-medium uppercase">Sessions</div>
153
  <div
154
- className={`h-[calc(100vh-345px)] overflow-y-auto font-geist transition-all duration-300 [&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar]:transition-opacity [&::-webkit-scrollbar]:duration-300 ${isScrolling ? '[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-background [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:opacity-0' : '[&::-webkit-scrollbar]:opacity-100'}`}
 
 
 
 
155
  onScroll={handleScroll}
156
  onMouseOver={() => setIsScrolling(true)}
157
  onMouseLeave={handleScroll}
158
  >
159
  {!isEndpointActive ||
160
- !hasStorage ||
161
- (!isSessionsLoading && (!sessionsData || sessionsData.length === 0)) ? (
162
  <SessionBlankState />
163
  ) : (
164
  <div className="flex flex-col gap-y-1 pr-1">
165
- {formattedSessionsData.map((entry, index) => (
166
  <SessionItem
167
- key={`${entry.session_id}-${index}`}
168
- {...entry}
169
- isSelected={selectedSessionId === entry.session_id}
170
- onSessionClick={handleSessionClick(entry.session_id)}
 
 
 
171
  />
172
  ))}
173
  </div>
 
1
  'use client'
2
 
3
+ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
 
 
 
 
4
  import { useQueryState } from 'nuqs'
5
+
6
+ import { useStore } from '@/store'
7
  import useSessionLoader from '@/hooks/useSessionLoader'
8
 
9
+ import SessionItem from './SessionItem'
10
+ import SessionBlankState from './SessionBlankState'
11
  import { Skeleton } from '@/components/ui/skeleton'
12
+ import { cn } from '@/lib/utils'
13
 
14
  interface SkeletonListProps {
15
  skeletonCount: number
16
  }
 
17
  const SkeletonList: FC<SkeletonListProps> = ({ skeletonCount }) => {
18
+ const list = useMemo(
19
  () => Array.from({ length: skeletonCount }, (_, i) => i),
20
  [skeletonCount]
21
  )
22
 
23
+ return list.map((k, idx) => (
24
  <Skeleton
25
+ key={k}
26
  className={cn(
27
  'mb-1 h-11 rounded-lg px-3 py-2',
28
+ idx > 0 && 'bg-background-secondary'
29
  )}
30
  />
31
  ))
32
  }
33
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  const Sessions = () => {
35
  const [agentId] = useQueryState('agent', {
36
+ parse: (v) => v || undefined,
37
  history: 'push'
38
  })
39
+ const [teamId] = useQueryState('team')
40
  const [sessionId] = useQueryState('session')
41
+ const [dbId] = useQueryState('db_id')
42
+
43
  const {
44
  selectedEndpoint,
45
+ mode,
46
  isEndpointActive,
47
  isEndpointLoading,
 
48
  hydrated,
49
+ sessionsData,
50
+ setSessionsData,
51
+ isSessionsLoading
52
+ } = useStore()
53
+
54
+ console.log({ sessionsData })
55
+
56
  const [isScrolling, setIsScrolling] = useState(false)
57
  const [selectedSessionId, setSelectedSessionId] = useState<string | null>(
58
  null
59
  )
60
+
61
+ const { getSessions, getSession } = useSessionLoader()
62
  const scrollTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null)
 
63
 
64
  const handleScroll = () => {
65
  setIsScrolling(true)
 
82
  }
83
  }, [])
84
 
 
85
  useEffect(() => {
86
+ if (hydrated && sessionId && selectedEndpoint && (agentId || teamId)) {
87
+ const entityType = agentId ? 'agent' : 'team'
88
+ getSession({ entityType, agentId, teamId, dbId }, sessionId)
89
  }
90
  // eslint-disable-next-line react-hooks/exhaustive-deps
91
+ }, [hydrated, sessionId, selectedEndpoint, agentId, teamId, dbId])
92
 
93
  useEffect(() => {
94
+ if (!selectedEndpoint || isEndpointLoading) return
95
+ if (!(agentId || teamId || dbId)) {
96
+ setSessionsData([])
97
  return
98
  }
99
+ setSessionsData([])
100
+ getSessions({
101
+ entityType: mode,
102
+ agentId,
103
+ teamId,
104
+ dbId
105
+ })
106
+ // eslint-disable-next-line react-hooks/exhaustive-deps
107
  }, [
108
  selectedEndpoint,
109
  agentId,
110
+ teamId,
111
+ mode,
112
  isEndpointLoading,
113
+ getSessions,
114
+ dbId
115
  ])
116
 
117
  useEffect(() => {
118
+ if (sessionId) setSelectedSessionId(sessionId)
 
 
119
  }, [sessionId])
120
 
 
 
 
 
 
 
 
 
 
 
121
  const handleSessionClick = useCallback(
122
  (id: string) => () => setSelectedSessionId(id),
123
  []
124
  )
125
 
126
+ if (isSessionsLoading || isEndpointLoading) {
127
  return (
128
  <div className="w-full">
129
  <div className="mb-2 text-xs font-medium uppercase">Sessions</div>
 
132
  </div>
133
  </div>
134
  )
135
+ }
136
+
137
  return (
138
  <div className="w-full">
139
  <div className="mb-2 w-full text-xs font-medium uppercase">Sessions</div>
140
  <div
141
+ className={`h-[calc(100vh-345px)] overflow-y-auto font-geist transition-all duration-300 [&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar]:transition-opacity [&::-webkit-scrollbar]:duration-300 ${
142
+ isScrolling
143
+ ? '[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-background [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:opacity-0'
144
+ : '[&::-webkit-scrollbar]:opacity-100'
145
+ }`}
146
  onScroll={handleScroll}
147
  onMouseOver={() => setIsScrolling(true)}
148
  onMouseLeave={handleScroll}
149
  >
150
  {!isEndpointActive ||
151
+ (!isSessionsLoading &&
152
+ (!sessionsData || sessionsData?.length === 0)) ? (
153
  <SessionBlankState />
154
  ) : (
155
  <div className="flex flex-col gap-y-1 pr-1">
156
+ {sessionsData?.map((entry, idx) => (
157
  <SessionItem
158
+ key={`${entry?.session_id}-${idx}`}
159
+ currentSessionId={selectedSessionId}
160
+ isSelected={selectedSessionId === entry?.session_id}
161
+ onSessionClick={handleSessionClick(entry?.session_id)}
162
+ session_name={entry?.session_name ?? '-'}
163
+ session_id={entry?.session_id}
164
+ created_at={entry?.created_at}
165
  />
166
  ))}
167
  </div>
src/components/{playground β†’ chat}/Sidebar/Sessions/index.ts RENAMED
File without changes
src/components/{playground β†’ chat}/Sidebar/Sidebar.tsx RENAMED
@@ -1,8 +1,9 @@
1
  'use client'
2
  import { Button } from '@/components/ui/button'
3
- import { AgentSelector } from '@/components/playground/Sidebar/AgentSelector'
 
4
  import useChatActions from '@/hooks/useChatActions'
5
- import { usePlaygroundStore } from '@/store'
6
  import { motion, AnimatePresence } from 'framer-motion'
7
  import { useState, useEffect } from 'react'
8
  import Icon from '@/components/ui/icon'
@@ -13,6 +14,7 @@ import { toast } from 'sonner'
13
  import { useQueryState } from 'nuqs'
14
  import { truncateText } from '@/lib/utils'
15
  import { Skeleton } from '@/components/ui/skeleton'
 
16
  const ENDPOINT_PLACEHOLDER = 'NO ENDPOINT ADDED'
17
  const SidebarHeader = () => (
18
  <div className="flex items-center gap-2">
@@ -57,8 +59,8 @@ const Endpoint = () => {
57
  setAgents,
58
  setSessionsData,
59
  setMessages
60
- } = usePlaygroundStore()
61
- const { initializePlayground } = useChatActions()
62
  const [isEditing, setIsEditing] = useState(false)
63
  const [endpointValue, setEndpointValue] = useState('')
64
  const [isMounted, setIsMounted] = useState(false)
@@ -107,13 +109,13 @@ const Endpoint = () => {
107
 
108
  const handleRefresh = async () => {
109
  setIsRotating(true)
110
- await initializePlayground()
111
  setTimeout(() => setIsRotating(false), 500)
112
  }
113
 
114
  return (
115
  <div className="flex flex-col items-start gap-2">
116
- <div className="text-xs font-medium uppercase text-primary">Endpoint</div>
117
  {isEditing ? (
118
  <div className="flex w-full items-center gap-1">
119
  <input
@@ -153,7 +155,7 @@ const Endpoint = () => {
153
  transition={{ duration: 0.2 }}
154
  >
155
  <p className="flex items-center gap-2 whitespace-nowrap text-xs font-medium text-primary">
156
- <Icon type="edit" size="xxs" /> EDIT ENDPOINT
157
  </p>
158
  </motion.div>
159
  ) : (
@@ -168,8 +170,8 @@ const Endpoint = () => {
168
  <p className="text-xs font-medium text-muted">
169
  {isMounted
170
  ? truncateText(selectedEndpoint, 21) ||
171
- ENDPOINT_PLACEHOLDER
172
- : process.env.LINK}
173
  </p>
174
  <div
175
  className={`size-2 shrink-0 rounded-full ${getStatusColor(isEndpointActive)}`}
@@ -200,25 +202,31 @@ const Endpoint = () => {
200
 
201
  const Sidebar = () => {
202
  const [isCollapsed, setIsCollapsed] = useState(false)
203
- const { clearChat, focusChatInput, initializePlayground } = useChatActions()
204
  const {
205
  messages,
206
  selectedEndpoint,
207
  isEndpointActive,
208
  selectedModel,
209
  hydrated,
210
- isEndpointLoading
211
- } = usePlaygroundStore()
 
212
  const [isMounted, setIsMounted] = useState(false)
213
  const [agentId] = useQueryState('agent')
 
 
214
  useEffect(() => {
215
  setIsMounted(true)
216
- if (hydrated) initializePlayground()
217
- }, [selectedEndpoint, initializePlayground, hydrated])
 
 
218
  const handleNewChat = () => {
219
  clearChat()
220
  focusChatInput()
221
  }
 
222
  return (
223
  <motion.aside
224
  className="relative flex h-screen shrink-0 grow-0 flex-col overflow-hidden px-2 py-3 font-dmmono"
@@ -265,11 +273,11 @@ const Sidebar = () => {
265
  transition={{ duration: 0.5, ease: 'easeInOut' }}
266
  >
267
  <div className="text-xs font-medium uppercase text-primary">
268
- Agent
269
  </div>
270
  {isEndpointLoading ? (
271
  <div className="flex w-full flex-col gap-2">
272
- {Array.from({ length: 2 }).map((_, index) => (
273
  <Skeleton
274
  key={index}
275
  className="h-9 w-full rounded-xl"
@@ -278,8 +286,9 @@ const Sidebar = () => {
278
  </div>
279
  ) : (
280
  <>
281
- <AgentSelector />
282
- {selectedModel && agentId && (
 
283
  <ModelDisplay model={selectedModel} />
284
  )}
285
  </>
 
1
  'use client'
2
  import { Button } from '@/components/ui/button'
3
+ import { ModeSelector } from '@/components/chat/Sidebar/ModeSelector'
4
+ import { EntitySelector } from '@/components/chat/Sidebar/EntitySelector'
5
  import useChatActions from '@/hooks/useChatActions'
6
+ import { useStore } from '@/store'
7
  import { motion, AnimatePresence } from 'framer-motion'
8
  import { useState, useEffect } from 'react'
9
  import Icon from '@/components/ui/icon'
 
14
  import { useQueryState } from 'nuqs'
15
  import { truncateText } from '@/lib/utils'
16
  import { Skeleton } from '@/components/ui/skeleton'
17
+
18
  const ENDPOINT_PLACEHOLDER = 'NO ENDPOINT ADDED'
19
  const SidebarHeader = () => (
20
  <div className="flex items-center gap-2">
 
59
  setAgents,
60
  setSessionsData,
61
  setMessages
62
+ } = useStore()
63
+ const { initialize } = useChatActions()
64
  const [isEditing, setIsEditing] = useState(false)
65
  const [endpointValue, setEndpointValue] = useState('')
66
  const [isMounted, setIsMounted] = useState(false)
 
109
 
110
  const handleRefresh = async () => {
111
  setIsRotating(true)
112
+ await initialize()
113
  setTimeout(() => setIsRotating(false), 500)
114
  }
115
 
116
  return (
117
  <div className="flex flex-col items-start gap-2">
118
+ <div className="text-xs font-medium uppercase text-primary">AgentOS</div>
119
  {isEditing ? (
120
  <div className="flex w-full items-center gap-1">
121
  <input
 
155
  transition={{ duration: 0.2 }}
156
  >
157
  <p className="flex items-center gap-2 whitespace-nowrap text-xs font-medium text-primary">
158
+ <Icon type="edit" size="xxs" /> EDIT AGENTOS
159
  </p>
160
  </motion.div>
161
  ) : (
 
170
  <p className="text-xs font-medium text-muted">
171
  {isMounted
172
  ? truncateText(selectedEndpoint, 21) ||
173
+ ENDPOINT_PLACEHOLDER
174
+ : 'http://localhost:7777'}
175
  </p>
176
  <div
177
  className={`size-2 shrink-0 rounded-full ${getStatusColor(isEndpointActive)}`}
 
202
 
203
  const Sidebar = () => {
204
  const [isCollapsed, setIsCollapsed] = useState(false)
205
+ const { clearChat, focusChatInput, initialize } = useChatActions()
206
  const {
207
  messages,
208
  selectedEndpoint,
209
  isEndpointActive,
210
  selectedModel,
211
  hydrated,
212
+ isEndpointLoading,
213
+ mode
214
+ } = useStore()
215
  const [isMounted, setIsMounted] = useState(false)
216
  const [agentId] = useQueryState('agent')
217
+ const [teamId] = useQueryState('team')
218
+
219
  useEffect(() => {
220
  setIsMounted(true)
221
+
222
+ if (hydrated) initialize()
223
+ }, [selectedEndpoint, initialize, hydrated, mode])
224
+
225
  const handleNewChat = () => {
226
  clearChat()
227
  focusChatInput()
228
  }
229
+
230
  return (
231
  <motion.aside
232
  className="relative flex h-screen shrink-0 grow-0 flex-col overflow-hidden px-2 py-3 font-dmmono"
 
273
  transition={{ duration: 0.5, ease: 'easeInOut' }}
274
  >
275
  <div className="text-xs font-medium uppercase text-primary">
276
+ Mode
277
  </div>
278
  {isEndpointLoading ? (
279
  <div className="flex w-full flex-col gap-2">
280
+ {Array.from({ length: 3 }).map((_, index) => (
281
  <Skeleton
282
  key={index}
283
  className="h-9 w-full rounded-xl"
 
286
  </div>
287
  ) : (
288
  <>
289
+ <ModeSelector />
290
+ <EntitySelector />
291
+ {selectedModel && (agentId || teamId) && (
292
  <ModelDisplay model={selectedModel} />
293
  )}
294
  </>
src/components/{playground β†’ chat}/Sidebar/index.ts RENAMED
File without changes
src/components/ui/typography/MarkdownRenderer/inlineStyles.tsx CHANGED
@@ -159,16 +159,16 @@ const Img = ({ src, alt }: ImgProps) => {
159
  <div className="flex h-40 flex-col items-center justify-center gap-2 rounded-md bg-secondary/50 text-muted">
160
  <Paragraph className="text-primary">Image unavailable</Paragraph>
161
  <Link
162
- href={typeof src === 'string' ? src : '#'}
163
  target="_blank"
164
  className="max-w-md truncate underline"
165
  >
166
- {typeof src === 'string' ? src : 'Invalid source'}
167
  </Link>
168
  </div>
169
  ) : (
170
  <Image
171
- src={typeof src === 'string' ? src : ''}
172
  width={96}
173
  height={56}
174
  alt={alt ?? 'Rendered image'}
 
159
  <div className="flex h-40 flex-col items-center justify-center gap-2 rounded-md bg-secondary/50 text-muted">
160
  <Paragraph className="text-primary">Image unavailable</Paragraph>
161
  <Link
162
+ href={src}
163
  target="_blank"
164
  className="max-w-md truncate underline"
165
  >
166
+ {src}
167
  </Link>
168
  </div>
169
  ) : (
170
  <Image
171
+ src={src}
172
  width={96}
173
  height={56}
174
  alt={alt ?? 'Rendered image'}
src/components/ui/typography/MarkdownRenderer/styles.tsx CHANGED
@@ -66,7 +66,10 @@ const OrderedList = ({ className, ...props }: OrderedListProps) => (
66
  )
67
 
68
  const Paragraph = ({ className, ...props }: ParagraphProps) => (
69
- <p className={cn(className, PARAGRAPH_SIZES.body)} {...filterProps(props)} />
 
 
 
70
  )
71
 
72
  const EmphasizedText = ({ className, ...props }: EmphasizedTextProps) => (
@@ -183,16 +186,16 @@ const Img = ({ src, alt }: ImgProps) => {
183
  <div className="flex h-40 flex-col items-center justify-center gap-2 rounded-md bg-secondary/50 text-muted">
184
  <Paragraph className="text-primary">Image unavailable</Paragraph>
185
  <Link
186
- href={typeof src === 'string' ? src : '#'}
187
  target="_blank"
188
  className="max-w-md truncate underline"
189
  >
190
- {typeof src === 'string' ? src : 'Invalid source'}
191
  </Link>
192
  </div>
193
  ) : (
194
  <Image
195
- src={typeof src === 'string' ? src : ''}
196
  width={1280}
197
  height={720}
198
  alt={alt ?? 'Rendered image'}
 
66
  )
67
 
68
  const Paragraph = ({ className, ...props }: ParagraphProps) => (
69
+ <div
70
+ className={cn(className, PARAGRAPH_SIZES.body)}
71
+ {...filterProps(props)}
72
+ />
73
  )
74
 
75
  const EmphasizedText = ({ className, ...props }: EmphasizedTextProps) => (
 
186
  <div className="flex h-40 flex-col items-center justify-center gap-2 rounded-md bg-secondary/50 text-muted">
187
  <Paragraph className="text-primary">Image unavailable</Paragraph>
188
  <Link
189
+ href={src}
190
  target="_blank"
191
  className="max-w-md truncate underline"
192
  >
193
+ {src}
194
  </Link>
195
  </div>
196
  ) : (
197
  <Image
198
+ src={src}
199
  width={1280}
200
  height={720}
201
  alt={alt ?? 'Rendered image'}
src/hooks/useAIResponseStream.tsx CHANGED
@@ -1,18 +1,68 @@
 
1
  import { useCallback } from 'react'
2
- import { type RunResponse } from '@/types/playground'
3
 
4
  /**
5
  * Processes a single JSON chunk by passing it to the provided callback.
6
- * @param chunk - A parsed JSON object of type RunResponse.
7
  * @param onChunk - Callback to handle the chunk.
8
  */
9
  function processChunk(
10
- chunk: RunResponse,
11
- onChunk: (chunk: RunResponse) => void
12
  ) {
13
  onChunk(chunk)
14
  }
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  /**
17
  * Parses a string buffer to extract complete JSON objects.
18
  *
@@ -30,66 +80,97 @@ function processChunk(
30
  */
31
  function parseBuffer(
32
  buffer: string,
33
- onChunk: (chunk: RunResponse) => void
34
  ): string {
35
- let jsonStartIndex = buffer.indexOf('{')
36
- let jsonEndIndex = -1
37
 
38
- while (jsonStartIndex !== -1) {
 
39
  let braceCount = 0
40
  let inString = false
 
 
 
41
 
42
- // Iterate through the buffer to find the end of the JSON object
43
- for (let i = jsonStartIndex; i < buffer.length; i++) {
44
  const char = buffer[i]
45
 
46
- // Check if the character is a double quote and the previous character is not a backslash
47
- // This is to handle escaped quotes in JSON strings
48
- if (char === '"' && buffer[i - 1] !== '\\') {
49
- inString = !inString
50
- }
51
-
52
- // If the character is not inside a string, count the braces
53
- if (!inString) {
54
- if (char === '{') braceCount++
55
- if (char === '}') braceCount--
56
- }
57
-
58
- // If the brace count is 0, we have found the end of the JSON object
59
- if (braceCount === 0) {
60
- jsonEndIndex = i
61
- break
 
 
 
 
62
  }
63
  }
64
 
65
- // If we found a complete JSON object, process it
66
  if (jsonEndIndex !== -1) {
67
  const jsonString = buffer.slice(jsonStartIndex, jsonEndIndex + 1)
 
68
  try {
69
- const parsed = JSON.parse(jsonString) as RunResponse
70
- processChunk(parsed, onChunk)
 
 
 
 
 
 
 
 
71
  } catch {
72
- // Skip invalid JSON, continue accumulating
73
- break
 
74
  }
75
- buffer = buffer.slice(jsonEndIndex + 1).trim()
76
- jsonStartIndex = buffer.indexOf('{')
77
- jsonEndIndex = -1
 
 
 
 
 
78
  } else {
79
- // No complete JSON found, wait for the next chunk
80
  break
81
  }
82
  }
83
 
 
84
  return buffer
85
  }
86
 
87
  /**
88
  * Custom React hook to handle streaming API responses as JSON objects.
89
  *
90
- * This hook:
 
 
 
 
91
  * - Accumulates partial JSON data from streaming responses.
92
  * - Extracts complete JSON objects and processes them via onChunk.
 
 
 
93
  * - Handles errors via onError and signals completion with onComplete.
94
  *
95
  * @returns An object containing the streamResponse function.
@@ -100,7 +181,7 @@ export default function useAIResponseStream() {
100
  apiUrl: string
101
  headers?: Record<string, string>
102
  requestBody: FormData | Record<string, unknown>
103
- onChunk: (chunk: RunResponse) => void
104
  onError: (error: Error) => void
105
  onComplete: () => void
106
  }): Promise<void> => {
@@ -136,7 +217,6 @@ export default function useAIResponseStream() {
136
  const errorData = await response.json()
137
  throw errorData
138
  }
139
-
140
  if (!response.body) {
141
  throw new Error('No response body')
142
  }
 
1
+ import { RunResponseContent } from '@/types/os'
2
  import { useCallback } from 'react'
 
3
 
4
  /**
5
  * Processes a single JSON chunk by passing it to the provided callback.
6
+ * @param chunk - A parsed JSON object of type RunResponseContent.
7
  * @param onChunk - Callback to handle the chunk.
8
  */
9
  function processChunk(
10
+ chunk: RunResponseContent,
11
+ onChunk: (chunk: RunResponseContent) => void
12
  ) {
13
  onChunk(chunk)
14
  }
15
 
16
+ // TODO: Make new format the default and phase out legacy format
17
+
18
+ /**
19
+ * Detects if the incoming data is in the legacy format (direct RunResponseContent)
20
+ * @param data - The parsed data object
21
+ * @returns true if it's in the legacy format, false if it's in the new format
22
+ */
23
+ function isLegacyFormat(data: RunResponseContent): boolean {
24
+ return (
25
+ typeof data === 'object' &&
26
+ data !== null &&
27
+ 'event' in data &&
28
+ !('data' in data) &&
29
+ typeof data.event === 'string'
30
+ )
31
+ }
32
+
33
+ interface NewFormatData {
34
+ event: string
35
+ data: string | Record<string, unknown>
36
+ }
37
+
38
+ type LegacyEventFormat = RunResponseContent & { event: string }
39
+
40
+ function convertNewFormatToLegacy(
41
+ newFormatData: NewFormatData
42
+ ): LegacyEventFormat {
43
+ const { event, data } = newFormatData
44
+
45
+ // Parse the data field if it's a string
46
+ let parsedData: Record<string, unknown>
47
+ if (typeof data === 'string') {
48
+ try {
49
+ // First try to parse as JSON
50
+ parsedData = JSON.parse(data)
51
+ } catch {
52
+ parsedData = {}
53
+ }
54
+ } else {
55
+ parsedData = data
56
+ }
57
+
58
+ const { ...cleanData } = parsedData
59
+
60
+ // Convert to legacy format by flattening the structure
61
+ return {
62
+ event: event,
63
+ ...cleanData
64
+ } as LegacyEventFormat
65
+ }
66
  /**
67
  * Parses a string buffer to extract complete JSON objects.
68
  *
 
80
  */
81
  function parseBuffer(
82
  buffer: string,
83
+ onChunk: (chunk: RunResponseContent) => void
84
  ): string {
85
+ let currentIndex = 0
86
+ let jsonStartIndex = buffer.indexOf('{', currentIndex)
87
 
88
+ // Process as many complete JSON objects as possible.
89
+ while (jsonStartIndex !== -1 && jsonStartIndex < buffer.length) {
90
  let braceCount = 0
91
  let inString = false
92
+ let escapeNext = false
93
+ let jsonEndIndex = -1
94
+ let i = jsonStartIndex
95
 
96
+ // Walk through the string to find the matching closing brace.
97
+ for (; i < buffer.length; i++) {
98
  const char = buffer[i]
99
 
100
+ if (inString) {
101
+ if (escapeNext) {
102
+ escapeNext = false
103
+ } else if (char === '\\') {
104
+ escapeNext = true
105
+ } else if (char === '"') {
106
+ inString = false
107
+ }
108
+ } else {
109
+ if (char === '"') {
110
+ inString = true
111
+ } else if (char === '{') {
112
+ braceCount++
113
+ } else if (char === '}') {
114
+ braceCount--
115
+ if (braceCount === 0) {
116
+ jsonEndIndex = i
117
+ break
118
+ }
119
+ }
120
  }
121
  }
122
 
123
+ // If we found a complete JSON object, try to parse it.
124
  if (jsonEndIndex !== -1) {
125
  const jsonString = buffer.slice(jsonStartIndex, jsonEndIndex + 1)
126
+
127
  try {
128
+ const parsed = JSON.parse(jsonString)
129
+
130
+ // Check if it's in the legacy format - use as is
131
+ if (isLegacyFormat(parsed)) {
132
+ processChunk(parsed, onChunk)
133
+ } else {
134
+ // New format - convert to legacy format for compatibility
135
+ const legacyChunk = convertNewFormatToLegacy(parsed)
136
+ processChunk(legacyChunk, onChunk)
137
+ }
138
  } catch {
139
+ // Move past the starting brace to avoid re-parsing the same invalid JSON.
140
+ jsonStartIndex = buffer.indexOf('{', jsonStartIndex + 1)
141
+ continue
142
  }
143
+
144
+ // Move currentIndex past the parsed JSON and trim any leading whitespace.
145
+ currentIndex = jsonEndIndex + 1
146
+ buffer = buffer.slice(currentIndex).trim()
147
+
148
+ // Reset currentIndex and search for the next JSON object.
149
+ currentIndex = 0
150
+ jsonStartIndex = buffer.indexOf('{', currentIndex)
151
  } else {
152
+ // If a complete JSON object is not found, break out and wait for more data.
153
  break
154
  }
155
  }
156
 
157
+ // Return any unprocessed (partial) data.
158
  return buffer
159
  }
160
 
161
  /**
162
  * Custom React hook to handle streaming API responses as JSON objects.
163
  *
164
+ * This hook supports two streaming formats:
165
+ * 1. Legacy format: Direct JSON objects matching RunResponseContent interface
166
+ * 2. New format: Event/data structure with { event: string, data: string|object }
167
+ *
168
+ * The hook:
169
  * - Accumulates partial JSON data from streaming responses.
170
  * - Extracts complete JSON objects and processes them via onChunk.
171
+ * - Automatically detects new format and converts it to legacy format for compatibility.
172
+ * - Parses stringified data field if it's a string (supports both JSON and Python dict syntax).
173
+ * - Removes redundant event field from data object during conversion.
174
  * - Handles errors via onError and signals completion with onComplete.
175
  *
176
  * @returns An object containing the streamResponse function.
 
181
  apiUrl: string
182
  headers?: Record<string, string>
183
  requestBody: FormData | Record<string, unknown>
184
+ onChunk: (chunk: RunResponseContent) => void
185
  onError: (error: Error) => void
186
  onComplete: () => void
187
  }): Promise<void> => {
 
217
  const errorData = await response.json()
218
  throw errorData
219
  }
 
220
  if (!response.body) {
221
  throw new Error('No response body')
222
  }
src/hooks/useAIStreamHandler.tsx CHANGED
@@ -3,30 +3,27 @@ import { useCallback } from 'react'
3
  import { APIRoutes } from '@/api/routes'
4
 
5
  import useChatActions from '@/hooks/useChatActions'
6
- import { usePlaygroundStore } from '../store'
7
- import { RunEvent, type RunResponse } from '@/types/playground'
8
  import { constructEndpointUrl } from '@/lib/constructEndpointUrl'
9
  import useAIResponseStream from './useAIResponseStream'
10
- import { ToolCall } from '@/types/playground'
11
  import { useQueryState } from 'nuqs'
12
  import { getJsonMarkdown } from '@/lib/utils'
13
 
14
- /**
15
- * useAIChatStreamHandler is responsible for making API calls and handling the stream response.
16
- * For now, it only streams message content and updates the messages state.
17
- */
18
  const useAIChatStreamHandler = () => {
19
- const setMessages = usePlaygroundStore((state) => state.setMessages)
20
  const { addMessage, focusChatInput } = useChatActions()
21
  const [agentId] = useQueryState('agent')
 
22
  const [sessionId, setSessionId] = useQueryState('session')
23
- const selectedEndpoint = usePlaygroundStore((state) => state.selectedEndpoint)
24
- const setStreamingErrorMessage = usePlaygroundStore(
 
25
  (state) => state.setStreamingErrorMessage
26
  )
27
- const setIsStreaming = usePlaygroundStore((state) => state.setIsStreaming)
28
- const setSessionsData = usePlaygroundStore((state) => state.setSessionsData)
29
- const hasStorage = usePlaygroundStore((state) => state.hasStorage)
30
  const { streamResponse } = useAIResponseStream()
31
 
32
  const updateMessagesWithErrorState = useCallback(() => {
@@ -40,6 +37,67 @@ const useAIChatStreamHandler = () => {
40
  })
41
  }, [setMessages])
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  const handleStreamResponse = useCallback(
44
  async (input: string | FormData) => {
45
  setIsStreaming(true)
@@ -83,33 +141,46 @@ const useAIChatStreamHandler = () => {
83
  try {
84
  const endpointUrl = constructEndpointUrl(selectedEndpoint)
85
 
86
- if (!agentId) return
87
- const playgroundRunUrl = APIRoutes.AgentRun(endpointUrl).replace(
88
- '{agent_id}',
89
- agentId
90
- )
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  formData.append('stream', 'true')
93
  formData.append('session_id', sessionId ?? '')
94
 
95
  await streamResponse({
96
- apiUrl: playgroundRunUrl,
97
  requestBody: formData,
98
  onChunk: (chunk: RunResponse) => {
99
  if (
100
  chunk.event === RunEvent.RunStarted ||
101
- chunk.event === RunEvent.ReasoningStarted
 
 
102
  ) {
103
  newSessionId = chunk.session_id as string
104
  setSessionId(chunk.session_id as string)
105
  if (
106
- hasStorage &&
107
  (!sessionId || sessionId !== chunk.session_id) &&
108
  chunk.session_id
109
  ) {
110
  const sessionData = {
111
  session_id: chunk.session_id as string,
112
- title: formData.get('message') as string,
113
  created_at: chunk.created_at
114
  }
115
  setSessionsData((prevSessionsData) => {
@@ -122,7 +193,27 @@ const useAIChatStreamHandler = () => {
122
  return [sessionData, ...(prevSessionsData ?? [])]
123
  })
124
  }
125
- } else if (chunk.event === RunEvent.RunResponse) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  setMessages((prevMessages) => {
127
  const newMessages = [...prevMessages]
128
  const lastMessage = newMessages[newMessages.length - 1]
@@ -135,10 +226,11 @@ const useAIChatStreamHandler = () => {
135
  lastMessage.content += uniqueContent
136
  lastContent = chunk.content
137
 
138
- const toolCalls: ToolCall[] = [...(chunk.tools ?? [])]
139
- if (toolCalls.length > 0) {
140
- lastMessage.tool_calls = toolCalls
141
- }
 
142
  if (chunk.extra_data?.reasoning_steps) {
143
  lastMessage.extra_data = {
144
  ...lastMessage.extra_data,
@@ -187,11 +279,54 @@ const useAIChatStreamHandler = () => {
187
  }
188
  return newMessages
189
  })
190
- } else if (chunk.event === RunEvent.RunError) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  updateMessagesWithErrorState()
192
- const errorContent = chunk.content as string
 
 
 
 
193
  setStreamingErrorMessage(errorContent)
194
- if (hasStorage && newSessionId) {
195
  setSessionsData(
196
  (prevSessionsData) =>
197
  prevSessionsData?.filter(
@@ -199,7 +334,16 @@ const useAIChatStreamHandler = () => {
199
  ) ?? null
200
  )
201
  }
202
- } else if (chunk.event === RunEvent.RunCompleted) {
 
 
 
 
 
 
 
 
 
203
  setMessages((prevMessages) => {
204
  const newMessages = prevMessages.map((message, index) => {
205
  if (
@@ -219,10 +363,10 @@ const useAIChatStreamHandler = () => {
219
  return {
220
  ...message,
221
  content: updatedContent,
222
- tool_calls:
223
- chunk.tools && chunk.tools.length > 0
224
- ? [...chunk.tools]
225
- : message.tool_calls,
226
  images: chunk.images ?? message.images,
227
  videos: chunk.videos ?? message.videos,
228
  response_audio: chunk.response_audio,
@@ -246,7 +390,7 @@ const useAIChatStreamHandler = () => {
246
  onError: (error) => {
247
  updateMessagesWithErrorState()
248
  setStreamingErrorMessage(error.message)
249
- if (hasStorage && newSessionId) {
250
  setSessionsData(
251
  (prevSessionsData) =>
252
  prevSessionsData?.filter(
@@ -262,7 +406,7 @@ const useAIChatStreamHandler = () => {
262
  setStreamingErrorMessage(
263
  error instanceof Error ? error.message : String(error)
264
  )
265
- if (hasStorage && newSessionId) {
266
  setSessionsData(
267
  (prevSessionsData) =>
268
  prevSessionsData?.filter(
@@ -282,13 +426,15 @@ const useAIChatStreamHandler = () => {
282
  selectedEndpoint,
283
  streamResponse,
284
  agentId,
 
 
285
  setStreamingErrorMessage,
286
  setIsStreaming,
287
  focusChatInput,
288
  setSessionsData,
289
  sessionId,
290
  setSessionId,
291
- hasStorage
292
  ]
293
  )
294
 
 
3
  import { APIRoutes } from '@/api/routes'
4
 
5
  import useChatActions from '@/hooks/useChatActions'
6
+ import { useStore } from '../store'
7
+ import { RunEvent, RunResponseContent, type RunResponse } from '@/types/os'
8
  import { constructEndpointUrl } from '@/lib/constructEndpointUrl'
9
  import useAIResponseStream from './useAIResponseStream'
10
+ import { ToolCall } from '@/types/os'
11
  import { useQueryState } from 'nuqs'
12
  import { getJsonMarkdown } from '@/lib/utils'
13
 
 
 
 
 
14
  const useAIChatStreamHandler = () => {
15
+ const setMessages = useStore((state) => state.setMessages)
16
  const { addMessage, focusChatInput } = useChatActions()
17
  const [agentId] = useQueryState('agent')
18
+ const [teamId] = useQueryState('team')
19
  const [sessionId, setSessionId] = useQueryState('session')
20
+ const selectedEndpoint = useStore((state) => state.selectedEndpoint)
21
+ const mode = useStore((state) => state.mode)
22
+ const setStreamingErrorMessage = useStore(
23
  (state) => state.setStreamingErrorMessage
24
  )
25
+ const setIsStreaming = useStore((state) => state.setIsStreaming)
26
+ const setSessionsData = useStore((state) => state.setSessionsData)
 
27
  const { streamResponse } = useAIResponseStream()
28
 
29
  const updateMessagesWithErrorState = useCallback(() => {
 
37
  })
38
  }, [setMessages])
39
 
40
+ /**
41
+ * Processes a new tool call and adds it to the message
42
+ * @param toolCall - The tool call to add
43
+ * @param prevToolCalls - The previous tool calls array
44
+ * @returns Updated tool calls array
45
+ */
46
+ const processToolCall = useCallback(
47
+ (toolCall: ToolCall, prevToolCalls: ToolCall[] = []) => {
48
+ const toolCallId =
49
+ toolCall.tool_call_id || `${toolCall.tool_name}-${toolCall.created_at}`
50
+
51
+ const existingToolCallIndex = prevToolCalls.findIndex(
52
+ (tc) =>
53
+ (tc.tool_call_id && tc.tool_call_id === toolCall.tool_call_id) ||
54
+ (!tc.tool_call_id &&
55
+ toolCall.tool_name &&
56
+ toolCall.created_at &&
57
+ `${tc.tool_name}-${tc.created_at}` === toolCallId)
58
+ )
59
+ if (existingToolCallIndex >= 0) {
60
+ const updatedToolCalls = [...prevToolCalls]
61
+ updatedToolCalls[existingToolCallIndex] = {
62
+ ...updatedToolCalls[existingToolCallIndex],
63
+ ...toolCall
64
+ }
65
+ return updatedToolCalls
66
+ } else {
67
+ return [...prevToolCalls, toolCall]
68
+ }
69
+ },
70
+ []
71
+ )
72
+
73
+ /**
74
+ * Processes tool calls from a chunk, handling both single tool object and tools array formats
75
+ * @param chunk - The chunk containing tool call data
76
+ * @param existingToolCalls - The existing tool calls array
77
+ * @returns Updated tool calls array
78
+ */
79
+ const processChunkToolCalls = useCallback(
80
+ (
81
+ chunk: RunResponseContent | RunResponse,
82
+ existingToolCalls: ToolCall[] = []
83
+ ) => {
84
+ let updatedToolCalls = [...existingToolCalls]
85
+ // Handle new single tool object format
86
+ if (chunk.tool) {
87
+ updatedToolCalls = processToolCall(chunk.tool, updatedToolCalls)
88
+ }
89
+ // Handle legacy tools array format
90
+ if (chunk.tools && chunk.tools.length > 0) {
91
+ for (const toolCall of chunk.tools) {
92
+ updatedToolCalls = processToolCall(toolCall, updatedToolCalls)
93
+ }
94
+ }
95
+
96
+ return updatedToolCalls
97
+ },
98
+ [processToolCall]
99
+ )
100
+
101
  const handleStreamResponse = useCallback(
102
  async (input: string | FormData) => {
103
  setIsStreaming(true)
 
141
  try {
142
  const endpointUrl = constructEndpointUrl(selectedEndpoint)
143
 
144
+ let RunUrl: string | null = null
145
+
146
+ if (mode === 'team' && teamId) {
147
+ RunUrl = APIRoutes.TeamRun(endpointUrl, teamId)
148
+ } else if (mode === 'agent' && agentId) {
149
+ RunUrl = APIRoutes.AgentRun(endpointUrl).replace(
150
+ '{agent_id}',
151
+ agentId
152
+ )
153
+ }
154
+
155
+ if (!RunUrl) {
156
+ updateMessagesWithErrorState()
157
+ setStreamingErrorMessage('Please select an agent or team first.')
158
+ setIsStreaming(false)
159
+ return
160
+ }
161
 
162
  formData.append('stream', 'true')
163
  formData.append('session_id', sessionId ?? '')
164
 
165
  await streamResponse({
166
+ apiUrl: RunUrl,
167
  requestBody: formData,
168
  onChunk: (chunk: RunResponse) => {
169
  if (
170
  chunk.event === RunEvent.RunStarted ||
171
+ chunk.event === RunEvent.TeamRunStarted ||
172
+ chunk.event === RunEvent.ReasoningStarted ||
173
+ chunk.event === RunEvent.TeamReasoningStarted
174
  ) {
175
  newSessionId = chunk.session_id as string
176
  setSessionId(chunk.session_id as string)
177
  if (
 
178
  (!sessionId || sessionId !== chunk.session_id) &&
179
  chunk.session_id
180
  ) {
181
  const sessionData = {
182
  session_id: chunk.session_id as string,
183
+ session_name: formData.get('message') as string,
184
  created_at: chunk.created_at
185
  }
186
  setSessionsData((prevSessionsData) => {
 
193
  return [sessionData, ...(prevSessionsData ?? [])]
194
  })
195
  }
196
+ } else if (
197
+ chunk.event === RunEvent.ToolCallStarted ||
198
+ chunk.event === RunEvent.TeamToolCallStarted ||
199
+ chunk.event === RunEvent.ToolCallCompleted ||
200
+ chunk.event === RunEvent.TeamToolCallCompleted
201
+ ) {
202
+ setMessages((prevMessages) => {
203
+ const newMessages = [...prevMessages]
204
+ const lastMessage = newMessages[newMessages.length - 1]
205
+ if (lastMessage && lastMessage.role === 'agent') {
206
+ lastMessage.tool_calls = processChunkToolCalls(
207
+ chunk,
208
+ lastMessage.tool_calls
209
+ )
210
+ }
211
+ return newMessages
212
+ })
213
+ } else if (
214
+ chunk.event === RunEvent.RunContent ||
215
+ chunk.event === RunEvent.TeamRunContent
216
+ ) {
217
  setMessages((prevMessages) => {
218
  const newMessages = [...prevMessages]
219
  const lastMessage = newMessages[newMessages.length - 1]
 
226
  lastMessage.content += uniqueContent
227
  lastContent = chunk.content
228
 
229
+ // Handle tool calls streaming
230
+ lastMessage.tool_calls = processChunkToolCalls(
231
+ chunk,
232
+ lastMessage.tool_calls
233
+ )
234
  if (chunk.extra_data?.reasoning_steps) {
235
  lastMessage.extra_data = {
236
  ...lastMessage.extra_data,
 
279
  }
280
  return newMessages
281
  })
282
+ } else if (
283
+ chunk.event === RunEvent.ReasoningStep ||
284
+ chunk.event === RunEvent.TeamReasoningStep
285
+ ) {
286
+ setMessages((prevMessages) => {
287
+ const newMessages = [...prevMessages]
288
+ const lastMessage = newMessages[newMessages.length - 1]
289
+ if (lastMessage && lastMessage.role === 'agent') {
290
+ const existingSteps =
291
+ lastMessage.extra_data?.reasoning_steps ?? []
292
+ const incomingSteps = chunk.extra_data?.reasoning_steps ?? []
293
+ lastMessage.extra_data = {
294
+ ...lastMessage.extra_data,
295
+ reasoning_steps: [...existingSteps, ...incomingSteps]
296
+ }
297
+ }
298
+ return newMessages
299
+ })
300
+ } else if (
301
+ chunk.event === RunEvent.ReasoningCompleted ||
302
+ chunk.event === RunEvent.TeamReasoningCompleted
303
+ ) {
304
+ setMessages((prevMessages) => {
305
+ const newMessages = [...prevMessages]
306
+ const lastMessage = newMessages[newMessages.length - 1]
307
+ if (lastMessage && lastMessage.role === 'agent') {
308
+ if (chunk.extra_data?.reasoning_steps) {
309
+ lastMessage.extra_data = {
310
+ ...lastMessage.extra_data,
311
+ reasoning_steps: chunk.extra_data.reasoning_steps
312
+ }
313
+ }
314
+ }
315
+ return newMessages
316
+ })
317
+ } else if (
318
+ chunk.event === RunEvent.RunError ||
319
+ chunk.event === RunEvent.TeamRunError ||
320
+ chunk.event === RunEvent.TeamRunCancelled
321
+ ) {
322
  updateMessagesWithErrorState()
323
+ const errorContent =
324
+ (chunk.content as string) ||
325
+ (chunk.event === RunEvent.TeamRunCancelled
326
+ ? 'Run cancelled'
327
+ : 'Error during run')
328
  setStreamingErrorMessage(errorContent)
329
+ if (newSessionId) {
330
  setSessionsData(
331
  (prevSessionsData) =>
332
  prevSessionsData?.filter(
 
334
  ) ?? null
335
  )
336
  }
337
+ } else if (
338
+ chunk.event === RunEvent.UpdatingMemory ||
339
+ chunk.event === RunEvent.TeamMemoryUpdateStarted ||
340
+ chunk.event === RunEvent.TeamMemoryUpdateCompleted
341
+ ) {
342
+ // No-op for now; could surface a lightweight UI indicator in the future
343
+ } else if (
344
+ chunk.event === RunEvent.RunCompleted ||
345
+ chunk.event === RunEvent.TeamRunCompleted
346
+ ) {
347
  setMessages((prevMessages) => {
348
  const newMessages = prevMessages.map((message, index) => {
349
  if (
 
363
  return {
364
  ...message,
365
  content: updatedContent,
366
+ tool_calls: processChunkToolCalls(
367
+ chunk,
368
+ message.tool_calls
369
+ ),
370
  images: chunk.images ?? message.images,
371
  videos: chunk.videos ?? message.videos,
372
  response_audio: chunk.response_audio,
 
390
  onError: (error) => {
391
  updateMessagesWithErrorState()
392
  setStreamingErrorMessage(error.message)
393
+ if (newSessionId) {
394
  setSessionsData(
395
  (prevSessionsData) =>
396
  prevSessionsData?.filter(
 
406
  setStreamingErrorMessage(
407
  error instanceof Error ? error.message : String(error)
408
  )
409
+ if (newSessionId) {
410
  setSessionsData(
411
  (prevSessionsData) =>
412
  prevSessionsData?.filter(
 
426
  selectedEndpoint,
427
  streamResponse,
428
  agentId,
429
+ teamId,
430
+ mode,
431
  setStreamingErrorMessage,
432
  setIsStreaming,
433
  focusChatInput,
434
  setSessionsData,
435
  sessionId,
436
  setSessionId,
437
+ processChunkToolCalls
438
  ]
439
  )
440
 
src/hooks/useChatActions.ts CHANGED
@@ -1,33 +1,30 @@
1
  import { useCallback } from 'react'
2
  import { toast } from 'sonner'
3
 
4
- import { usePlaygroundStore } from '../store'
5
 
6
- import { ComboboxAgent, type PlaygroundChatMessage } from '@/types/playground'
7
- import {
8
- getPlaygroundAgentsAPI,
9
- getPlaygroundStatusAPI
10
- } from '@/api/playground'
11
  import { useQueryState } from 'nuqs'
12
 
13
  const useChatActions = () => {
14
- const { chatInputRef } = usePlaygroundStore()
15
- const selectedEndpoint = usePlaygroundStore((state) => state.selectedEndpoint)
16
  const [, setSessionId] = useQueryState('session')
17
- const setMessages = usePlaygroundStore((state) => state.setMessages)
18
- const setIsEndpointActive = usePlaygroundStore(
19
- (state) => state.setIsEndpointActive
20
- )
21
- const setIsEndpointLoading = usePlaygroundStore(
22
- (state) => state.setIsEndpointLoading
23
- )
24
- const setAgents = usePlaygroundStore((state) => state.setAgents)
25
- const setSelectedModel = usePlaygroundStore((state) => state.setSelectedModel)
26
  const [agentId, setAgentId] = useQueryState('agent')
 
 
27
 
28
  const getStatus = useCallback(async () => {
29
  try {
30
- const status = await getPlaygroundStatusAPI(selectedEndpoint)
31
  return status
32
  } catch {
33
  return 503
@@ -36,7 +33,7 @@ const useChatActions = () => {
36
 
37
  const getAgents = useCallback(async () => {
38
  try {
39
- const agents = await getPlaygroundAgentsAPI(selectedEndpoint)
40
  return agents
41
  } catch {
42
  toast.error('Error fetching agents')
@@ -44,6 +41,16 @@ const useChatActions = () => {
44
  }
45
  }, [selectedEndpoint])
46
 
 
 
 
 
 
 
 
 
 
 
47
  const clearChat = useCallback(() => {
48
  setMessages([])
49
  setSessionId(null)
@@ -58,44 +65,113 @@ const useChatActions = () => {
58
  }, [])
59
 
60
  const addMessage = useCallback(
61
- (message: PlaygroundChatMessage) => {
62
  setMessages((prevMessages) => [...prevMessages, message])
63
  },
64
  [setMessages]
65
  )
66
 
67
- const initializePlayground = useCallback(async () => {
68
  setIsEndpointLoading(true)
69
  try {
70
  const status = await getStatus()
71
- let agents: ComboboxAgent[] = []
 
72
  if (status === 200) {
73
  setIsEndpointActive(true)
 
74
  agents = await getAgents()
75
- if (agents.length > 0 && !agentId) {
76
- const firstAgent = agents[0]
77
- setAgentId(firstAgent.value)
78
- setSelectedModel(firstAgent.model.provider || '')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
80
  } else {
81
  setIsEndpointActive(false)
 
 
 
 
82
  }
83
- setAgents(agents)
84
- return agents
85
- } catch {
86
- setIsEndpointLoading(false)
 
 
 
 
 
 
87
  } finally {
88
  setIsEndpointLoading(false)
89
  }
90
  }, [
91
  getStatus,
92
  getAgents,
 
93
  setIsEndpointActive,
94
  setIsEndpointLoading,
95
  setAgents,
 
96
  setAgentId,
97
  setSelectedModel,
98
- agentId
 
 
 
 
99
  ])
100
 
101
  return {
@@ -103,7 +179,8 @@ const useChatActions = () => {
103
  addMessage,
104
  getAgents,
105
  focusChatInput,
106
- initializePlayground
 
107
  }
108
  }
109
 
 
1
  import { useCallback } from 'react'
2
  import { toast } from 'sonner'
3
 
4
+ import { useStore } from '../store'
5
 
6
+ import { AgentDetails, TeamDetails, type ChatMessage } from '@/types/os'
7
+ import { getAgentsAPI, getStatusAPI, getTeamsAPI } from '@/api/os'
 
 
 
8
  import { useQueryState } from 'nuqs'
9
 
10
  const useChatActions = () => {
11
+ const { chatInputRef } = useStore()
12
+ const selectedEndpoint = useStore((state) => state.selectedEndpoint)
13
  const [, setSessionId] = useQueryState('session')
14
+ const setMessages = useStore((state) => state.setMessages)
15
+ const setIsEndpointActive = useStore((state) => state.setIsEndpointActive)
16
+ const setIsEndpointLoading = useStore((state) => state.setIsEndpointLoading)
17
+ const setAgents = useStore((state) => state.setAgents)
18
+ const setTeams = useStore((state) => state.setTeams)
19
+ const setSelectedModel = useStore((state) => state.setSelectedModel)
20
+ const setMode = useStore((state) => state.setMode)
 
 
21
  const [agentId, setAgentId] = useQueryState('agent')
22
+ const [teamId, setTeamId] = useQueryState('team')
23
+ const [, setDbId] = useQueryState('db_id')
24
 
25
  const getStatus = useCallback(async () => {
26
  try {
27
+ const status = await getStatusAPI(selectedEndpoint)
28
  return status
29
  } catch {
30
  return 503
 
33
 
34
  const getAgents = useCallback(async () => {
35
  try {
36
+ const agents = await getAgentsAPI(selectedEndpoint)
37
  return agents
38
  } catch {
39
  toast.error('Error fetching agents')
 
41
  }
42
  }, [selectedEndpoint])
43
 
44
+ const getTeams = useCallback(async () => {
45
+ try {
46
+ const teams = await getTeamsAPI(selectedEndpoint)
47
+ return teams
48
+ } catch {
49
+ toast.error('Error fetching teams')
50
+ return []
51
+ }
52
+ }, [selectedEndpoint])
53
+
54
  const clearChat = useCallback(() => {
55
  setMessages([])
56
  setSessionId(null)
 
65
  }, [])
66
 
67
  const addMessage = useCallback(
68
+ (message: ChatMessage) => {
69
  setMessages((prevMessages) => [...prevMessages, message])
70
  },
71
  [setMessages]
72
  )
73
 
74
+ const initialize = useCallback(async () => {
75
  setIsEndpointLoading(true)
76
  try {
77
  const status = await getStatus()
78
+ let agents: AgentDetails[] = []
79
+ let teams: TeamDetails[] = []
80
  if (status === 200) {
81
  setIsEndpointActive(true)
82
+ teams = await getTeams()
83
  agents = await getAgents()
84
+ console.log(' is active', teams, agents)
85
+
86
+ if (!agentId && !teamId) {
87
+ const currentMode = useStore.getState().mode
88
+ console.log('Current mode:', currentMode)
89
+
90
+ if (currentMode === 'team' && teams.length > 0) {
91
+ const firstTeam = teams[0]
92
+ setTeamId(firstTeam.id)
93
+ setSelectedModel(firstTeam.model?.provider || '')
94
+ setDbId(firstTeam.db_id || '')
95
+ setAgentId(null)
96
+ setTeams(teams)
97
+ } else if (currentMode === 'agent' && agents.length > 0) {
98
+ const firstAgent = agents[0]
99
+ setMode('agent')
100
+ setAgentId(firstAgent.id)
101
+ setSelectedModel(firstAgent.model?.model || '')
102
+ setDbId(firstAgent.db_id || '')
103
+ setAgents(agents)
104
+ }
105
+ } else {
106
+ setAgents(agents)
107
+ setTeams(teams)
108
+ if (agentId) {
109
+ const agent = agents.find((a) => a.id === agentId)
110
+ if (agent) {
111
+ setMode('agent')
112
+ setSelectedModel(agent.model?.model || '')
113
+ setDbId(agent.db_id || '')
114
+ setTeamId(null)
115
+ } else if (agents.length > 0) {
116
+ const firstAgent = agents[0]
117
+ setMode('agent')
118
+ setAgentId(firstAgent.id)
119
+ setSelectedModel(firstAgent.model?.model || '')
120
+ setDbId(firstAgent.db_id || '')
121
+ setTeamId(null)
122
+ }
123
+ } else if (teamId) {
124
+ const team = teams.find((t) => t.id === teamId)
125
+ if (team) {
126
+ setMode('team')
127
+ setSelectedModel(team.model?.provider || '')
128
+ setDbId(team.db_id || '')
129
+ setAgentId(null)
130
+ } else if (teams.length > 0) {
131
+ const firstTeam = teams[0]
132
+ setMode('team')
133
+ setTeamId(firstTeam.id)
134
+ setSelectedModel(firstTeam.model?.provider || '')
135
+ setDbId(firstTeam.db_id || '')
136
+ setAgentId(null)
137
+ }
138
+ }
139
  }
140
  } else {
141
  setIsEndpointActive(false)
142
+ setMode('agent')
143
+ setSelectedModel('')
144
+ setAgentId(null)
145
+ setTeamId(null)
146
  }
147
+ return { agents, teams }
148
+ } catch (error) {
149
+ console.error('Error initializing :', error)
150
+ setIsEndpointActive(false)
151
+ setMode('agent')
152
+ setSelectedModel('')
153
+ setAgentId(null)
154
+ setTeamId(null)
155
+ setAgents([])
156
+ setTeams([])
157
  } finally {
158
  setIsEndpointLoading(false)
159
  }
160
  }, [
161
  getStatus,
162
  getAgents,
163
+ getTeams,
164
  setIsEndpointActive,
165
  setIsEndpointLoading,
166
  setAgents,
167
+ setTeams,
168
  setAgentId,
169
  setSelectedModel,
170
+ setMode,
171
+ setTeamId,
172
+ setDbId,
173
+ agentId,
174
+ teamId
175
  ])
176
 
177
  return {
 
179
  addMessage,
180
  getAgents,
181
  focusChatInput,
182
+ getTeams,
183
+ initialize
184
  }
185
  }
186
 
src/hooks/useSessionLoader.tsx CHANGED
@@ -1,16 +1,8 @@
1
  import { useCallback } from 'react'
2
- import {
3
- getPlaygroundSessionAPI,
4
- getAllPlaygroundSessionsAPI
5
- } from '@/api/playground'
6
- import { usePlaygroundStore } from '../store'
7
  import { toast } from 'sonner'
8
- import {
9
- PlaygroundChatMessage,
10
- ToolCall,
11
- ReasoningMessage,
12
- ChatEntry
13
- } from '@/types/playground'
14
  import { getJsonMarkdown } from '@/lib/utils'
15
 
16
  interface SessionResponse {
@@ -25,26 +17,38 @@ interface SessionResponse {
25
  agent_data: Record<string, unknown>
26
  }
27
 
 
 
 
 
 
 
 
28
  const useSessionLoader = () => {
29
- const setMessages = usePlaygroundStore((state) => state.setMessages)
30
- const selectedEndpoint = usePlaygroundStore((state) => state.selectedEndpoint)
31
- const setIsSessionsLoading = usePlaygroundStore(
32
- (state) => state.setIsSessionsLoading
33
- )
34
- const setSessionsData = usePlaygroundStore((state) => state.setSessionsData)
35
 
36
  const getSessions = useCallback(
37
- async (agentId: string) => {
38
- if (!agentId || !selectedEndpoint) return
 
 
39
  try {
40
  setIsSessionsLoading(true)
41
- const sessions = await getAllPlaygroundSessionsAPI(
 
42
  selectedEndpoint,
43
- agentId
 
 
44
  )
45
- setSessionsData(sessions)
 
46
  } catch {
47
  toast.error('Error loading sessions')
 
48
  } finally {
49
  setIsSessionsLoading(false)
50
  }
@@ -53,39 +57,46 @@ const useSessionLoader = () => {
53
  )
54
 
55
  const getSession = useCallback(
56
- async (sessionId: string, agentId: string) => {
57
- if (!sessionId || !agentId || !selectedEndpoint) {
58
- return null
59
- }
 
 
 
 
 
 
 
 
 
 
60
 
61
  try {
62
- const response = (await getPlaygroundSessionAPI(
63
  selectedEndpoint,
64
- agentId,
65
- sessionId
66
- )) as SessionResponse
67
-
68
- if (response && response.memory) {
69
- const sessionHistory = response.runs
70
- ? response.runs
71
- : response.memory.runs
72
-
73
- if (sessionHistory && Array.isArray(sessionHistory)) {
74
- const messagesForPlayground = sessionHistory.flatMap((run) => {
75
- const filteredMessages: PlaygroundChatMessage[] = []
76
 
77
- if (run.message) {
78
  filteredMessages.push({
79
  role: 'user',
80
- content: run.message.content ?? '',
81
- created_at: run.message.created_at
82
  })
83
  }
84
 
85
- if (run.response) {
86
  const toolCalls = [
87
- ...(run.response.tools ?? []),
88
- ...(run.response.extra_data?.reasoning_messages ?? []).reduce(
89
  (acc: ToolCall[], msg: ReasoningMessage) => {
90
  if (msg.role === 'tool') {
91
  acc.push({
@@ -108,21 +119,21 @@ const useSessionLoader = () => {
108
 
109
  filteredMessages.push({
110
  role: 'agent',
111
- content: (run.response.content as string) ?? '',
112
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
113
- extra_data: run.response.extra_data,
114
- images: run.response.images,
115
- videos: run.response.videos,
116
- audio: run.response.audio,
117
- response_audio: run.response.response_audio,
118
- created_at: run.response.created_at
119
  })
120
  }
121
  return filteredMessages
122
  })
123
 
124
- const processedMessages = messagesForPlayground.map(
125
- (message: PlaygroundChatMessage) => {
126
  if (Array.isArray(message.content)) {
127
  const textContent = message.content
128
  .filter((item: { type: string }) => item.type === 'text')
 
1
  import { useCallback } from 'react'
2
+ import { getSessionAPI, getAllSessionsAPI } from '@/api/os'
3
+ import { useStore } from '../store'
 
 
 
4
  import { toast } from 'sonner'
5
+ import { ChatMessage, ToolCall, ReasoningMessage, ChatEntry } from '@/types/os'
 
 
 
 
 
6
  import { getJsonMarkdown } from '@/lib/utils'
7
 
8
  interface SessionResponse {
 
17
  agent_data: Record<string, unknown>
18
  }
19
 
20
+ interface LoaderArgs {
21
+ entityType: 'agent' | 'team' | null
22
+ agentId?: string | null
23
+ teamId?: string | null
24
+ dbId: string | null
25
+ }
26
+
27
  const useSessionLoader = () => {
28
+ const setMessages = useStore((state) => state.setMessages)
29
+ const selectedEndpoint = useStore((state) => state.selectedEndpoint)
30
+ const setIsSessionsLoading = useStore((state) => state.setIsSessionsLoading)
31
+ const setSessionsData = useStore((state) => state.setSessionsData)
 
 
32
 
33
  const getSessions = useCallback(
34
+ async ({ entityType, agentId, teamId, dbId }: LoaderArgs) => {
35
+ const selectedId = entityType === 'agent' ? agentId : teamId
36
+ if (!selectedEndpoint || !entityType || !selectedId || !dbId) return
37
+
38
  try {
39
  setIsSessionsLoading(true)
40
+
41
+ const sessions = await getAllSessionsAPI(
42
  selectedEndpoint,
43
+ entityType,
44
+ selectedId,
45
+ dbId
46
  )
47
+ console.log('Fetched sessions:', sessions)
48
+ setSessionsData(sessions.data ?? [])
49
  } catch {
50
  toast.error('Error loading sessions')
51
+ setSessionsData([])
52
  } finally {
53
  setIsSessionsLoading(false)
54
  }
 
57
  )
58
 
59
  const getSession = useCallback(
60
+ async (
61
+ { entityType, agentId, teamId, dbId }: LoaderArgs,
62
+ sessionId: string
63
+ ) => {
64
+ const selectedId = entityType === 'agent' ? agentId : teamId
65
+ if (
66
+ !selectedEndpoint ||
67
+ !sessionId ||
68
+ !entityType ||
69
+ !selectedId ||
70
+ !dbId
71
+ )
72
+ return
73
+ console.log(entityType)
74
 
75
  try {
76
+ const response: SessionResponse = await getSessionAPI(
77
  selectedEndpoint,
78
+ entityType,
79
+ sessionId,
80
+ dbId
81
+ )
82
+ console.log('Fetched session:', response)
83
+ if (response) {
84
+ if (Array.isArray(response)) {
85
+ const messagesFor = response.flatMap((run) => {
86
+ const filteredMessages: ChatMessage[] = []
 
 
 
87
 
88
+ if (run) {
89
  filteredMessages.push({
90
  role: 'user',
91
+ content: run.run_input ?? '',
92
+ created_at: run.created_at
93
  })
94
  }
95
 
96
+ if (run) {
97
  const toolCalls = [
98
+ ...(run.tools ?? []),
99
+ ...(run.extra_data?.reasoning_messages ?? []).reduce(
100
  (acc: ToolCall[], msg: ReasoningMessage) => {
101
  if (msg.role === 'tool') {
102
  acc.push({
 
119
 
120
  filteredMessages.push({
121
  role: 'agent',
122
+ content: (run.content as string) ?? '',
123
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
124
+ extra_data: run.extra_data,
125
+ images: run.images,
126
+ videos: run.videos,
127
+ audio: run.audio,
128
+ response_audio: run.response_audio,
129
+ created_at: run.created_at
130
  })
131
  }
132
  return filteredMessages
133
  })
134
 
135
+ const processedMessages = messagesFor.map(
136
+ (message: ChatMessage) => {
137
  if (Array.isArray(message.content)) {
138
  const textContent = message.content
139
  .filter((item: { type: string }) => item.type === 'text')
src/store.ts CHANGED
@@ -2,32 +2,25 @@ import { create } from 'zustand'
2
  import { persist, createJSONStorage } from 'zustand/middleware'
3
 
4
  import {
5
- type PlaygroundChatMessage,
6
- type SessionEntry
7
- } from '@/types/playground'
 
 
8
 
9
- interface Agent {
10
- value: string
11
- label: string
12
- model: {
13
- provider: string
14
- }
15
- storage?: boolean
16
- }
17
-
18
- interface PlaygroundStore {
19
  hydrated: boolean
20
  setHydrated: () => void
21
  streamingErrorMessage: string
22
  setStreamingErrorMessage: (streamingErrorMessage: string) => void
23
  endpoints: {
24
  endpoint: string
25
- id_playground_endpoint: string
26
  }[]
27
  setEndpoints: (
28
  endpoints: {
29
  endpoint: string
30
- id_playground_endpoint: string
31
  }[]
32
  ) => void
33
  isStreaming: boolean
@@ -36,21 +29,21 @@ interface PlaygroundStore {
36
  setIsEndpointActive: (isActive: boolean) => void
37
  isEndpointLoading: boolean
38
  setIsEndpointLoading: (isLoading: boolean) => void
39
- messages: PlaygroundChatMessage[]
40
  setMessages: (
41
- messages:
42
- | PlaygroundChatMessage[]
43
- | ((prevMessages: PlaygroundChatMessage[]) => PlaygroundChatMessage[])
44
  ) => void
45
- hasStorage: boolean
46
- setHasStorage: (hasStorage: boolean) => void
47
  chatInputRef: React.RefObject<HTMLTextAreaElement | null>
48
  selectedEndpoint: string
49
  setSelectedEndpoint: (selectedEndpoint: string) => void
50
- agents: Agent[]
51
- setAgents: (agents: Agent[]) => void
 
 
52
  selectedModel: string
53
  setSelectedModel: (model: string) => void
 
 
54
  sessionsData: SessionEntry[] | null
55
  setSessionsData: (
56
  sessionsData:
@@ -61,7 +54,7 @@ interface PlaygroundStore {
61
  setIsSessionsLoading: (isSessionsLoading: boolean) => void
62
  }
63
 
64
- export const usePlaygroundStore = create<PlaygroundStore>()(
65
  persist(
66
  (set) => ({
67
  hydrated: false,
@@ -85,17 +78,18 @@ export const usePlaygroundStore = create<PlaygroundStore>()(
85
  messages:
86
  typeof messages === 'function' ? messages(state.messages) : messages
87
  })),
88
- hasStorage: false,
89
- setHasStorage: (hasStorage) => set(() => ({ hasStorage })),
90
  chatInputRef: { current: null },
91
- selectedEndpoint:
92
- 'https://sifa-classification-agentic-rag-99329044472.asia-south1.run.app',
93
  setSelectedEndpoint: (selectedEndpoint) =>
94
  set(() => ({ selectedEndpoint })),
95
  agents: [],
96
  setAgents: (agents) => set({ agents }),
 
 
97
  selectedModel: '',
98
  setSelectedModel: (selectedModel) => set(() => ({ selectedModel })),
 
 
99
  sessionsData: null,
100
  setSessionsData: (sessionsData) =>
101
  set((state) => ({
 
2
  import { persist, createJSONStorage } from 'zustand/middleware'
3
 
4
  import {
5
+ AgentDetails,
6
+ SessionEntry,
7
+ TeamDetails,
8
+ type ChatMessage
9
+ } from '@/types/os'
10
 
11
+ interface Store {
 
 
 
 
 
 
 
 
 
12
  hydrated: boolean
13
  setHydrated: () => void
14
  streamingErrorMessage: string
15
  setStreamingErrorMessage: (streamingErrorMessage: string) => void
16
  endpoints: {
17
  endpoint: string
18
+ id__endpoint: string
19
  }[]
20
  setEndpoints: (
21
  endpoints: {
22
  endpoint: string
23
+ id__endpoint: string
24
  }[]
25
  ) => void
26
  isStreaming: boolean
 
29
  setIsEndpointActive: (isActive: boolean) => void
30
  isEndpointLoading: boolean
31
  setIsEndpointLoading: (isLoading: boolean) => void
32
+ messages: ChatMessage[]
33
  setMessages: (
34
+ messages: ChatMessage[] | ((prevMessages: ChatMessage[]) => ChatMessage[])
 
 
35
  ) => void
 
 
36
  chatInputRef: React.RefObject<HTMLTextAreaElement | null>
37
  selectedEndpoint: string
38
  setSelectedEndpoint: (selectedEndpoint: string) => void
39
+ agents: AgentDetails[]
40
+ setAgents: (agents: AgentDetails[]) => void
41
+ teams: TeamDetails[]
42
+ setTeams: (teams: TeamDetails[]) => void
43
  selectedModel: string
44
  setSelectedModel: (model: string) => void
45
+ mode: 'agent' | 'team'
46
+ setMode: (mode: 'agent' | 'team') => void
47
  sessionsData: SessionEntry[] | null
48
  setSessionsData: (
49
  sessionsData:
 
54
  setIsSessionsLoading: (isSessionsLoading: boolean) => void
55
  }
56
 
57
+ export const useStore = create<Store>()(
58
  persist(
59
  (set) => ({
60
  hydrated: false,
 
78
  messages:
79
  typeof messages === 'function' ? messages(state.messages) : messages
80
  })),
 
 
81
  chatInputRef: { current: null },
82
+ selectedEndpoint: 'http://localhost:7777',
 
83
  setSelectedEndpoint: (selectedEndpoint) =>
84
  set(() => ({ selectedEndpoint })),
85
  agents: [],
86
  setAgents: (agents) => set({ agents }),
87
+ teams: [],
88
+ setTeams: (teams) => set({ teams }),
89
  selectedModel: '',
90
  setSelectedModel: (selectedModel) => set(() => ({ selectedModel })),
91
+ mode: 'agent',
92
+ setMode: (mode) => set(() => ({ mode })),
93
  sessionsData: null,
94
  setSessionsData: (sessionsData) =>
95
  set((state) => ({
src/types/{playground.ts β†’ os.ts} RENAMED
@@ -69,6 +69,14 @@ export interface Agent {
69
  storage?: boolean
70
  }
71
 
 
 
 
 
 
 
 
 
72
  interface MessageContext {
73
  query: string
74
  docs?: Array<Record<string, object>>
@@ -77,16 +85,36 @@ interface MessageContext {
77
 
78
  export enum RunEvent {
79
  RunStarted = 'RunStarted',
80
- RunResponse = 'RunResponse',
81
  RunCompleted = 'RunCompleted',
 
 
 
82
  ToolCallStarted = 'ToolCallStarted',
83
  ToolCallCompleted = 'ToolCallCompleted',
84
- UpdatingMemory = 'UpdatingMemory',
 
85
  ReasoningStarted = 'ReasoningStarted',
86
  ReasoningStep = 'ReasoningStep',
87
  ReasoningCompleted = 'ReasoningCompleted',
88
- RunError = 'RunError'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  }
 
90
  export interface ResponseAudio {
91
  id?: string
92
  content?: string
@@ -94,6 +122,33 @@ export interface ResponseAudio {
94
  channels?: number
95
  sample_rate?: number
96
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  export interface RunResponse {
98
  content?: string | object
99
  content_type: string
@@ -106,9 +161,10 @@ export interface RunResponse {
106
  run_id?: string
107
  agent_id?: string
108
  session_id?: string
 
 
109
  created_at: number
110
- tools?: ToolCall[]
111
- extra_data?: PlaygroundAgentExtraData
112
  images?: ImageData[]
113
  videos?: VideoData[]
114
  audio?: AudioData[]
@@ -121,7 +177,7 @@ export interface AgentExtraData {
121
  references?: ReferenceData[]
122
  }
123
 
124
- export interface PlaygroundAgentExtraData extends AgentExtraData {
125
  reasoning_messages?: ReasoningMessage[]
126
  references?: ReferenceData[]
127
  }
@@ -138,7 +194,7 @@ export interface ReasoningMessage {
138
  }
139
  created_at?: number
140
  }
141
- export interface PlaygroundChatMessage {
142
  role: 'user' | 'agent' | 'system' | 'tool'
143
  content: string
144
  streamingError?: boolean
@@ -155,14 +211,23 @@ export interface PlaygroundChatMessage {
155
  response_audio?: ResponseAudio
156
  }
157
 
158
- export interface ComboboxAgent {
159
- value: string
160
- label: string
161
- model: {
162
- provider: string
163
- }
164
- storage?: boolean
165
  }
 
 
 
 
 
 
 
 
 
 
166
  export interface ImageData {
167
  revised_prompt: string
168
  url: string
@@ -201,8 +266,21 @@ export interface Reference {
201
 
202
  export interface SessionEntry {
203
  session_id: string
204
- title: string
205
  created_at: number
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  }
207
 
208
  export interface ChatEntry {
 
69
  storage?: boolean
70
  }
71
 
72
+ export interface Team {
73
+ team_id: string
74
+ name: string
75
+ description: string
76
+ model: Model
77
+ storage?: boolean
78
+ }
79
+
80
  interface MessageContext {
81
  query: string
82
  docs?: Array<Record<string, object>>
 
85
 
86
  export enum RunEvent {
87
  RunStarted = 'RunStarted',
88
+ RunContent = 'RunContent',
89
  RunCompleted = 'RunCompleted',
90
+ RunError = 'RunError',
91
+ RunOutput = 'RunOutput',
92
+ UpdatingMemory = 'UpdatingMemory',
93
  ToolCallStarted = 'ToolCallStarted',
94
  ToolCallCompleted = 'ToolCallCompleted',
95
+ MemoryUpdateStarted = 'MemoryUpdateStarted',
96
+ MemoryUpdateCompleted = 'MemoryUpdateCompleted',
97
  ReasoningStarted = 'ReasoningStarted',
98
  ReasoningStep = 'ReasoningStep',
99
  ReasoningCompleted = 'ReasoningCompleted',
100
+ RunCancelled = 'RunCancelled',
101
+ RunPaused = 'RunPaused',
102
+ RunContinued = 'RunContinued',
103
+ // Team Events
104
+ TeamRunStarted = 'TeamRunStarted',
105
+ TeamRunContent = 'TeamRunContent',
106
+ TeamRunCompleted = 'TeamRunCompleted',
107
+ TeamRunError = 'TeamRunError',
108
+ TeamRunCancelled = 'TeamRunCancelled',
109
+ TeamToolCallStarted = 'TeamToolCallStarted',
110
+ TeamToolCallCompleted = 'TeamToolCallCompleted',
111
+ TeamReasoningStarted = 'TeamReasoningStarted',
112
+ TeamReasoningStep = 'TeamReasoningStep',
113
+ TeamReasoningCompleted = 'TeamReasoningCompleted',
114
+ TeamMemoryUpdateStarted = 'TeamMemoryUpdateStarted',
115
+ TeamMemoryUpdateCompleted = 'TeamMemoryUpdateCompleted'
116
  }
117
+
118
  export interface ResponseAudio {
119
  id?: string
120
  content?: string
 
122
  channels?: number
123
  sample_rate?: number
124
  }
125
+
126
+ export interface NewRunResponse {
127
+ status: 'RUNNING' | 'PAUSED' | 'CANCELLED'
128
+ }
129
+
130
+ export interface RunResponseContent {
131
+ content?: string | object
132
+ content_type: string
133
+ context?: MessageContext[]
134
+ event: RunEvent
135
+ event_data?: object
136
+ messages?: ModelMessage[]
137
+ metrics?: object
138
+ model?: string
139
+ run_id?: string
140
+ agent_id?: string
141
+ session_id?: string
142
+ tool?: ToolCall
143
+ tools?: Array<ToolCall>
144
+ created_at: number
145
+ extra_data?: AgentExtraData
146
+ images?: ImageData[]
147
+ videos?: VideoData[]
148
+ audio?: AudioData[]
149
+ response_audio?: ResponseAudio
150
+ }
151
+
152
  export interface RunResponse {
153
  content?: string | object
154
  content_type: string
 
161
  run_id?: string
162
  agent_id?: string
163
  session_id?: string
164
+ tool?: ToolCall
165
+ tools?: Array<ToolCall>
166
  created_at: number
167
+ extra_data?: AgentExtraData
 
168
  images?: ImageData[]
169
  videos?: VideoData[]
170
  audio?: AudioData[]
 
177
  references?: ReferenceData[]
178
  }
179
 
180
+ export interface AgentExtraData {
181
  reasoning_messages?: ReasoningMessage[]
182
  references?: ReferenceData[]
183
  }
 
194
  }
195
  created_at?: number
196
  }
197
+ export interface ChatMessage {
198
  role: 'user' | 'agent' | 'system' | 'tool'
199
  content: string
200
  streamingError?: boolean
 
211
  response_audio?: ResponseAudio
212
  }
213
 
214
+ export interface AgentDetails {
215
+ id: string
216
+ name?: string
217
+ db_id?: string
218
+ // Model
219
+ model?: Model
 
220
  }
221
+
222
+ export interface TeamDetails {
223
+ id: string
224
+ name?: string
225
+ db_id?: string
226
+
227
+ // Model
228
+ model?: Model
229
+ }
230
+
231
  export interface ImageData {
232
  revised_prompt: string
233
  url: string
 
266
 
267
  export interface SessionEntry {
268
  session_id: string
269
+ session_name: string
270
  created_at: number
271
+ updated_at?: number
272
+ }
273
+
274
+ export interface Pagination {
275
+ page: number
276
+ limit: number
277
+ total_pages: number
278
+ total_count: number
279
+ }
280
+
281
+ export interface Sessions extends SessionEntry {
282
+ data: SessionEntry[]
283
+ meta: Pagination
284
  }
285
 
286
  export interface ChatEntry {
workflows/validate.yml DELETED
@@ -1,21 +0,0 @@
1
- name: Validate Build
2
-
3
- on:
4
- push:
5
-
6
- jobs:
7
- validate:
8
- runs-on: ubuntu-latest
9
- steps:
10
- - uses: actions/checkout@v4
11
-
12
- - name: Setup pnpm
13
- uses: pnpm/action-setup@v4
14
- with:
15
- version: latest
16
-
17
- - name: Install dependencies
18
- run: pnpm install
19
-
20
- - name: Run validate script
21
- run: pnpm run validate