Programmer-RD-AI
commited on
Commit
Β·
bbe4eea
1
Parent(s):
ae11e81
Update Agent UI Verion
Browse files- Dockerfile +0 -66
- README.md +40 -17
- next.config.js +0 -3
- next.config.ts +1 -2
- package-lock.json +0 -0
- package.json +2 -2
- pnpm-lock.yaml +0 -0
- public/favicon.ico +0 -0
- src/api/os.ts +133 -0
- src/api/playground.ts +0 -89
- src/api/routes.ts +14 -20
- src/app/page.tsx +2 -2
- src/components/{playground β chat}/ChatArea/ChatArea.tsx +0 -0
- src/components/{playground β chat}/ChatArea/ChatInput/ChatInput.tsx +8 -5
- src/components/{playground β chat}/ChatArea/ChatInput/index.ts +0 -0
- src/components/{playground β chat}/ChatArea/MessageArea.tsx +3 -3
- src/components/{playground β chat}/ChatArea/Messages/AgentThinkingLoader.tsx +0 -0
- src/components/{playground β chat}/ChatArea/Messages/ChatBlankState.tsx +3 -6
- src/components/{playground β chat}/ChatArea/Messages/MessageItem.tsx +10 -12
- src/components/{playground β chat}/ChatArea/Messages/Messages.tsx +7 -6
- src/components/{playground β chat}/ChatArea/Messages/Multimedia/Audios/Audios.tsx +1 -1
- src/components/{playground β chat}/ChatArea/Messages/Multimedia/Audios/index.ts +0 -0
- src/components/{playground β chat}/ChatArea/Messages/Multimedia/Images/Images.tsx +1 -1
- src/components/{playground β chat}/ChatArea/Messages/Multimedia/Images/index.ts +0 -0
- src/components/{playground β chat}/ChatArea/Messages/Multimedia/Videos/Videos.tsx +1 -1
- src/components/{playground β chat}/ChatArea/Messages/Multimedia/Videos/index.ts +0 -0
- src/components/{playground β chat}/ChatArea/Messages/index.ts +0 -0
- src/components/{playground β chat}/ChatArea/ScrollToBottom.tsx +0 -0
- src/components/{playground β chat}/ChatArea/index.ts +0 -0
- src/components/{playground/Sidebar/AgentSelector.tsx β chat/Sidebar/EntitySelector.tsx} +53 -35
- src/components/chat/Sidebar/ModeSelector.tsx +57 -0
- src/components/{playground β chat}/Sidebar/NewChatButton.tsx +2 -2
- src/components/{playground β chat}/Sidebar/Sessions/DeleteSessionModal.tsx +0 -0
- src/components/{playground β chat}/Sidebar/Sessions/SessionBlankState.tsx +2 -18
- src/components/{playground β chat}/Sidebar/Sessions/SessionItem.tsx +54 -33
- src/components/{playground β chat}/Sidebar/Sessions/Sessions.tsx +62 -68
- src/components/{playground β chat}/Sidebar/Sessions/index.ts +0 -0
- src/components/{playground β chat}/Sidebar/Sidebar.tsx +27 -18
- src/components/{playground β chat}/Sidebar/index.ts +0 -0
- src/components/ui/typography/MarkdownRenderer/inlineStyles.tsx +3 -3
- src/components/ui/typography/MarkdownRenderer/styles.tsx +7 -4
- src/hooks/useAIResponseStream.tsx +118 -38
- src/hooks/useAIStreamHandler.tsx +184 -38
- src/hooks/useChatActions.ts +109 -32
- src/hooks/useSessionLoader.tsx +65 -54
- src/store.ts +22 -28
- src/types/{playground.ts β os.ts} +93 -15
- 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
|
15 |
|
16 |
-
<img src="https://
|
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
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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": "^
|
32 |
-
"react-dom": "^
|
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 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
`${
|
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 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
) =>
|
22 |
-
`${
|
|
|
|
|
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/
|
3 |
-
import { ChatArea } from '@/components/
|
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 {
|
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 } =
|
13 |
|
14 |
const { handleStreamResponse } = useAIChatStreamHandler()
|
15 |
const [selectedAgent] = useQueryState('agent')
|
|
|
16 |
const [inputMessage, setInputMessage] = useState('')
|
17 |
-
const 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={
|
|
|
|
|
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 {
|
4 |
import Messages from './Messages'
|
5 |
-
import ScrollToBottom from '@/components/
|
6 |
import { StickToBottom } from 'use-stick-to-bottom'
|
7 |
|
8 |
const MessageArea = () => {
|
9 |
-
const { messages } =
|
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 |
-
|
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
|
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 {
|
4 |
-
import type {
|
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:
|
13 |
}
|
14 |
|
15 |
const AgentMessage = ({ message }: MessageProps) => {
|
16 |
-
const { streamingErrorMessage } =
|
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
|
85 |
-
<
|
86 |
-
|
87 |
-
|
88 |
-
|
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 {
|
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/
|
13 |
import React, { type FC } from 'react'
|
14 |
-
|
15 |
import Icon from '@/components/ui/icon'
|
|
|
16 |
|
17 |
interface MessageListProps {
|
18 |
-
messages:
|
19 |
}
|
20 |
|
21 |
interface MessageWrapperProps {
|
22 |
-
message:
|
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-
|
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/
|
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/
|
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/
|
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 {
|
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
|
18 |
-
const { agents, setMessages, setSelectedModel
|
19 |
-
|
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 |
-
|
|
|
|
|
|
|
28 |
useEffect(() => {
|
29 |
-
if (
|
30 |
-
const
|
31 |
-
if (
|
32 |
-
setSelectedModel(
|
33 |
-
|
34 |
-
|
|
|
|
|
35 |
focusChatInput()
|
36 |
}
|
37 |
-
} else {
|
38 |
-
setAgentId(agents[0].value)
|
39 |
}
|
40 |
}
|
41 |
// eslint-disable-next-line react-hooks/exhaustive-deps
|
42 |
-
}, [
|
43 |
|
44 |
const handleOnValueChange = (value: string) => {
|
45 |
-
const
|
46 |
-
const
|
47 |
-
|
48 |
-
|
49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
setMessages([])
|
51 |
setSessionId(null)
|
52 |
-
|
|
|
53 |
focusChatInput()
|
54 |
}
|
55 |
}
|
56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
return (
|
58 |
<Select
|
59 |
-
value={
|
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=
|
64 |
</SelectTrigger>
|
65 |
<SelectContent className="border-none bg-primaryAccent font-dmmono shadow-lg">
|
66 |
-
{
|
67 |
<SelectItem
|
68 |
className="cursor-pointer"
|
69 |
-
key={`${
|
70 |
-
value={
|
71 |
>
|
72 |
<div className="flex items-center gap-3 text-xs font-medium uppercase">
|
73 |
-
<Icon type={'
|
74 |
-
{
|
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 {
|
7 |
|
8 |
function NewChatButton() {
|
9 |
const { clearChat } = useChatActions()
|
10 |
-
const { messages } =
|
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 {
|
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
|
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/
|
3 |
import { Button } from '../../../ui/button'
|
4 |
import useSessionLoader from '@/hooks/useSessionLoader'
|
5 |
-
import {
|
6 |
-
import {
|
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
|
|
|
26 |
const [, setSessionId] = useQueryState('session')
|
27 |
-
const {
|
28 |
-
|
29 |
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
|
|
30 |
const { clearChat } = useChatActions()
|
31 |
|
32 |
const handleGetSession = async () => {
|
33 |
-
if (agentId)
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
}
|
39 |
|
40 |
const handleDeleteSession = async () => {
|
41 |
-
if (agentId)
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
|
|
|
|
|
|
52 |
clearChat()
|
53 |
-
toast.success('Session deleted')
|
54 |
-
} else {
|
55 |
-
toast.error('Failed to delete session')
|
56 |
}
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
|
|
|
|
61 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
}
|
63 |
}
|
64 |
return (
|
65 |
<>
|
66 |
<div
|
67 |
className={cn(
|
68 |
-
'group flex h-11 w-full
|
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={
|
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,
|
4 |
-
import dayjs from 'dayjs'
|
5 |
-
import utc from 'dayjs/plugin/utc'
|
6 |
-
|
7 |
-
import { usePlaygroundStore } from '@/store'
|
8 |
import { useQueryState } from 'nuqs'
|
9 |
-
|
10 |
-
import
|
11 |
import useSessionLoader from '@/hooks/useSessionLoader'
|
12 |
|
13 |
-
import
|
14 |
-
import
|
15 |
import { Skeleton } from '@/components/ui/skeleton'
|
|
|
16 |
|
17 |
interface SkeletonListProps {
|
18 |
skeletonCount: number
|
19 |
}
|
20 |
-
|
21 |
const SkeletonList: FC<SkeletonListProps> = ({ skeletonCount }) => {
|
22 |
-
const
|
23 |
() => Array.from({ length: skeletonCount }, (_, i) => i),
|
24 |
[skeletonCount]
|
25 |
)
|
26 |
|
27 |
-
return
|
28 |
<Skeleton
|
29 |
-
key={
|
30 |
className={cn(
|
31 |
'mb-1 h-11 rounded-lg px-3 py-2',
|
32 |
-
|
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: (
|
53 |
history: 'push'
|
54 |
})
|
|
|
55 |
const [sessionId] = useQueryState('session')
|
|
|
|
|
56 |
const {
|
57 |
selectedEndpoint,
|
|
|
58 |
isEndpointActive,
|
59 |
isEndpointLoading,
|
60 |
-
sessionsData,
|
61 |
hydrated,
|
62 |
-
|
63 |
-
setSessionsData
|
64 |
-
|
|
|
|
|
|
|
|
|
65 |
const [isScrolling, setIsScrolling] = useState(false)
|
66 |
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(
|
67 |
null
|
68 |
)
|
69 |
-
|
|
|
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 (
|
97 |
-
|
|
|
98 |
}
|
99 |
// eslint-disable-next-line react-hooks/exhaustive-deps
|
100 |
-
}, [hydrated])
|
101 |
|
102 |
useEffect(() => {
|
103 |
-
if (!selectedEndpoint ||
|
104 |
-
|
|
|
105 |
return
|
106 |
}
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
|
|
|
|
|
|
|
|
111 |
}, [
|
112 |
selectedEndpoint,
|
113 |
agentId,
|
114 |
-
|
|
|
115 |
isEndpointLoading,
|
116 |
-
|
117 |
-
|
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 ${
|
|
|
|
|
|
|
|
|
155 |
onScroll={handleScroll}
|
156 |
onMouseOver={() => setIsScrolling(true)}
|
157 |
onMouseLeave={handleScroll}
|
158 |
>
|
159 |
{!isEndpointActive ||
|
160 |
-
!
|
161 |
-
|
162 |
<SessionBlankState />
|
163 |
) : (
|
164 |
<div className="flex flex-col gap-y-1 pr-1">
|
165 |
-
{
|
166 |
<SessionItem
|
167 |
-
key={`${entry
|
168 |
-
{
|
169 |
-
isSelected={selectedSessionId === entry
|
170 |
-
onSessionClick={handleSessionClick(entry
|
|
|
|
|
|
|
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 {
|
|
|
4 |
import useChatActions from '@/hooks/useChatActions'
|
5 |
-
import {
|
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 |
-
} =
|
61 |
-
const {
|
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
|
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">
|
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
|
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 |
-
|
172 |
-
:
|
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,
|
204 |
const {
|
205 |
messages,
|
206 |
selectedEndpoint,
|
207 |
isEndpointActive,
|
208 |
selectedModel,
|
209 |
hydrated,
|
210 |
-
isEndpointLoading
|
211 |
-
|
|
|
212 |
const [isMounted, setIsMounted] = useState(false)
|
213 |
const [agentId] = useQueryState('agent')
|
|
|
|
|
214 |
useEffect(() => {
|
215 |
setIsMounted(true)
|
216 |
-
|
217 |
-
|
|
|
|
|
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 |
-
|
269 |
</div>
|
270 |
{isEndpointLoading ? (
|
271 |
<div className="flex w-full flex-col gap-2">
|
272 |
-
{Array.from({ length:
|
273 |
<Skeleton
|
274 |
key={index}
|
275 |
className="h-9 w-full rounded-xl"
|
@@ -278,8 +286,9 @@ const Sidebar = () => {
|
|
278 |
</div>
|
279 |
) : (
|
280 |
<>
|
281 |
-
<
|
282 |
-
|
|
|
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={
|
163 |
target="_blank"
|
164 |
className="max-w-md truncate underline"
|
165 |
>
|
166 |
-
{
|
167 |
</Link>
|
168 |
</div>
|
169 |
) : (
|
170 |
<Image
|
171 |
-
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 |
-
<
|
|
|
|
|
|
|
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={
|
187 |
target="_blank"
|
188 |
className="max-w-md truncate underline"
|
189 |
>
|
190 |
-
{
|
191 |
</Link>
|
192 |
</div>
|
193 |
) : (
|
194 |
<Image
|
195 |
-
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
|
7 |
* @param onChunk - Callback to handle the chunk.
|
8 |
*/
|
9 |
function processChunk(
|
10 |
-
chunk:
|
11 |
-
onChunk: (chunk:
|
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:
|
34 |
): string {
|
35 |
-
let
|
36 |
-
let
|
37 |
|
38 |
-
|
|
|
39 |
let braceCount = 0
|
40 |
let inString = false
|
|
|
|
|
|
|
41 |
|
42 |
-
//
|
43 |
-
for (
|
44 |
const char = buffer[i]
|
45 |
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
if (char === '
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
|
|
|
|
|
|
|
|
62 |
}
|
63 |
}
|
64 |
|
65 |
-
// If we found a complete JSON object,
|
66 |
if (jsonEndIndex !== -1) {
|
67 |
const jsonString = buffer.slice(jsonStartIndex, jsonEndIndex + 1)
|
|
|
68 |
try {
|
69 |
-
const parsed = JSON.parse(jsonString)
|
70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
} catch {
|
72 |
-
//
|
73 |
-
|
|
|
74 |
}
|
75 |
-
|
76 |
-
|
77 |
-
|
|
|
|
|
|
|
|
|
|
|
78 |
} else {
|
79 |
-
//
|
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:
|
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 {
|
7 |
-
import { RunEvent, type RunResponse } from '@/types/
|
8 |
import { constructEndpointUrl } from '@/lib/constructEndpointUrl'
|
9 |
import useAIResponseStream from './useAIResponseStream'
|
10 |
-
import { ToolCall } from '@/types/
|
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 =
|
20 |
const { addMessage, focusChatInput } = useChatActions()
|
21 |
const [agentId] = useQueryState('agent')
|
|
|
22 |
const [sessionId, setSessionId] = useQueryState('session')
|
23 |
-
const selectedEndpoint =
|
24 |
-
const
|
|
|
25 |
(state) => state.setStreamingErrorMessage
|
26 |
)
|
27 |
-
const setIsStreaming =
|
28 |
-
const 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 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
|
92 |
formData.append('stream', 'true')
|
93 |
formData.append('session_id', sessionId ?? '')
|
94 |
|
95 |
await streamResponse({
|
96 |
-
apiUrl:
|
97 |
requestBody: formData,
|
98 |
onChunk: (chunk: RunResponse) => {
|
99 |
if (
|
100 |
chunk.event === RunEvent.RunStarted ||
|
101 |
-
chunk.event === RunEvent.
|
|
|
|
|
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 |
-
|
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 (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
139 |
-
|
140 |
-
|
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 (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
191 |
updateMessagesWithErrorState()
|
192 |
-
const errorContent =
|
|
|
|
|
|
|
|
|
193 |
setStreamingErrorMessage(errorContent)
|
194 |
-
if (
|
195 |
setSessionsData(
|
196 |
(prevSessionsData) =>
|
197 |
prevSessionsData?.filter(
|
@@ -199,7 +334,16 @@ const useAIChatStreamHandler = () => {
|
|
199 |
) ?? null
|
200 |
)
|
201 |
}
|
202 |
-
} else if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
224 |
-
|
225 |
-
|
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 (
|
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 (
|
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 |
-
|
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 {
|
5 |
|
6 |
-
import {
|
7 |
-
import {
|
8 |
-
getPlaygroundAgentsAPI,
|
9 |
-
getPlaygroundStatusAPI
|
10 |
-
} from '@/api/playground'
|
11 |
import { useQueryState } from 'nuqs'
|
12 |
|
13 |
const useChatActions = () => {
|
14 |
-
const { chatInputRef } =
|
15 |
-
const selectedEndpoint =
|
16 |
const [, setSessionId] = useQueryState('session')
|
17 |
-
const setMessages =
|
18 |
-
const setIsEndpointActive =
|
19 |
-
|
20 |
-
)
|
21 |
-
const
|
22 |
-
|
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
|
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
|
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:
|
62 |
setMessages((prevMessages) => [...prevMessages, message])
|
63 |
},
|
64 |
[setMessages]
|
65 |
)
|
66 |
|
67 |
-
const
|
68 |
setIsEndpointLoading(true)
|
69 |
try {
|
70 |
const status = await getStatus()
|
71 |
-
let agents:
|
|
|
72 |
if (status === 200) {
|
73 |
setIsEndpointActive(true)
|
|
|
74 |
agents = await getAgents()
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
}
|
80 |
} else {
|
81 |
setIsEndpointActive(false)
|
|
|
|
|
|
|
|
|
82 |
}
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
} finally {
|
88 |
setIsEndpointLoading(false)
|
89 |
}
|
90 |
}, [
|
91 |
getStatus,
|
92 |
getAgents,
|
|
|
93 |
setIsEndpointActive,
|
94 |
setIsEndpointLoading,
|
95 |
setAgents,
|
|
|
96 |
setAgentId,
|
97 |
setSelectedModel,
|
98 |
-
|
|
|
|
|
|
|
|
|
99 |
])
|
100 |
|
101 |
return {
|
@@ -103,7 +179,8 @@ const useChatActions = () => {
|
|
103 |
addMessage,
|
104 |
getAgents,
|
105 |
focusChatInput,
|
106 |
-
|
|
|
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 |
-
|
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 =
|
30 |
-
const selectedEndpoint =
|
31 |
-
const setIsSessionsLoading =
|
32 |
-
|
33 |
-
)
|
34 |
-
const setSessionsData = usePlaygroundStore((state) => state.setSessionsData)
|
35 |
|
36 |
const getSessions = useCallback(
|
37 |
-
async (agentId:
|
38 |
-
|
|
|
|
|
39 |
try {
|
40 |
setIsSessionsLoading(true)
|
41 |
-
|
|
|
42 |
selectedEndpoint,
|
43 |
-
|
|
|
|
|
44 |
)
|
45 |
-
|
|
|
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 (
|
57 |
-
|
58 |
-
|
59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
|
61 |
try {
|
62 |
-
const response =
|
63 |
selectedEndpoint,
|
64 |
-
|
65 |
-
sessionId
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
if (sessionHistory && Array.isArray(sessionHistory)) {
|
74 |
-
const messagesForPlayground = sessionHistory.flatMap((run) => {
|
75 |
-
const filteredMessages: PlaygroundChatMessage[] = []
|
76 |
|
77 |
-
if (run
|
78 |
filteredMessages.push({
|
79 |
role: 'user',
|
80 |
-
content: run.
|
81 |
-
created_at: run.
|
82 |
})
|
83 |
}
|
84 |
|
85 |
-
if (run
|
86 |
const toolCalls = [
|
87 |
-
...(run.
|
88 |
-
...(run.
|
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.
|
112 |
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
113 |
-
extra_data: run.
|
114 |
-
images: run.
|
115 |
-
videos: run.
|
116 |
-
audio: run.
|
117 |
-
response_audio: run.
|
118 |
-
created_at: run.
|
119 |
})
|
120 |
}
|
121 |
return filteredMessages
|
122 |
})
|
123 |
|
124 |
-
const processedMessages =
|
125 |
-
(message:
|
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 |
-
|
6 |
-
|
7 |
-
|
|
|
|
|
8 |
|
9 |
-
interface
|
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 |
-
|
26 |
}[]
|
27 |
setEndpoints: (
|
28 |
endpoints: {
|
29 |
endpoint: string
|
30 |
-
|
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:
|
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:
|
51 |
-
setAgents: (agents:
|
|
|
|
|
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
|
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 |
-
|
81 |
RunCompleted = 'RunCompleted',
|
|
|
|
|
|
|
82 |
ToolCallStarted = 'ToolCallStarted',
|
83 |
ToolCallCompleted = 'ToolCallCompleted',
|
84 |
-
|
|
|
85 |
ReasoningStarted = 'ReasoningStarted',
|
86 |
ReasoningStep = 'ReasoningStep',
|
87 |
ReasoningCompleted = 'ReasoningCompleted',
|
88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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
|
125 |
reasoning_messages?: ReasoningMessage[]
|
126 |
references?: ReferenceData[]
|
127 |
}
|
@@ -138,7 +194,7 @@ export interface ReasoningMessage {
|
|
138 |
}
|
139 |
created_at?: number
|
140 |
}
|
141 |
-
export interface
|
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
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
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 |
-
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|