evo-ai/frontend/app/documentation/components/FrontendImplementationSection.tsx

796 lines
25 KiB
TypeScript

/*
┌──────────────────────────────────────────────────────────────────────────────┐
│ @author: Davidson Gomes │
│ @file: /app/documentation/components/FrontendImplementationSection.tsx │
│ Developed by: Davidson Gomes │
│ Creation date: May 13, 2025 │
│ Contact: contato@evolution-api.com │
├──────────────────────────────────────────────────────────────────────────────┤
│ @copyright © Evolution API 2025. All rights reserved. │
│ Licensed under the Apache License, Version 2.0 │
│ │
│ You may not use this file except in compliance with the License. │
│ You may obtain a copy of the License at │
│ │
│ http://www.apache.org/licenses/LICENSE-2.0 │
│ │
│ Unless required by applicable law or agreed to in writing, software │
│ distributed under the License is distributed on an "AS IS" BASIS, │
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
│ See the License for the specific language governing permissions and │
│ limitations under the License. │
├──────────────────────────────────────────────────────────────────────────────┤
│ @important │
│ For any future changes to the code in this file, it is recommended to │
│ include, together with the modification, the information of the developer │
│ who changed it and the date of modification. │
└──────────────────────────────────────────────────────────────────────────────┘
*/
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ClipboardCopy } from "lucide-react";
import { CodeBlock } from "@/app/documentation/components/CodeBlock";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface FrontendImplementationSectionProps {
copyToClipboard: (text: string) => void;
}
export function FrontendImplementationSection({ copyToClipboard }: FrontendImplementationSectionProps) {
return (
<Card className="bg-[#1a1a1a] border-[#333] text-white">
<CardHeader>
<CardTitle className="text-emerald-400">Frontend implementation</CardTitle>
<CardDescription className="text-neutral-400">
Practical examples for implementation in React applications
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<Tabs defaultValue="standard">
<TabsList className="bg-[#222] border-[#333] mb-4">
<TabsTrigger value="standard" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
Standard HTTP
</TabsTrigger>
<TabsTrigger value="streaming" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
Streaming SSE
</TabsTrigger>
<TabsTrigger value="react-component" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
React component
</TabsTrigger>
</TabsList>
<TabsContent value="standard">
<div>
<h3 className="text-emerald-400 text-lg font-medium mb-2">Implementation of message/send</h3>
<p className="text-neutral-300 mb-4">
Example of standard implementation in JavaScript/React:
</p>
<div className="relative">
<CodeBlock
text={`async function sendTask(agentId, message) {
// Generate unique IDs
const taskId = crypto.randomUUID();
const callId = crypto.randomUUID();
// Prepare request data
const requestData = {
jsonrpc: "2.0",
id: callId,
method: "message/send",
params: {
id: taskId,
sessionId: "session-" + Math.random().toString(36).substring(2, 9),
message: {
role: "user",
parts: [
{
type: "text",
text: message
}
]
}
}
};
try {
// Indicate loading state
setIsLoading(true);
// Send the request
const response = await fetch(\`/api/v1/a2a/\${agentId}\`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY_HERE'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error(\`HTTP error: \${response.status}\`);
}
// Process the response
const data = await response.json();
// Check for errors
if (data.error) {
console.error('Error in response:', data.error);
return null;
}
// Extract the agent response
const task = data.result;
// Show response in UI
if (task.status.message && task.status.message.parts) {
const responseText = task.status.message.parts
.filter(part => part.type === 'text')
.map(part => part.text)
.join('');
// Here you would update your React state
// setResponse(responseText);
return responseText;
}
return task;
} catch (error) {
console.error('Error sending task:', error);
return null;
} finally {
// Finalize loading state
setIsLoading(false);
}
}`}
language="javascript"
/>
<Button
size="sm"
variant="ghost"
className="absolute top-2 right-2 text-white hover:bg-[#333]"
onClick={() => copyToClipboard(`async function sendTask(agentId, message) {
// Generate unique IDs
const taskId = crypto.randomUUID();
const callId = crypto.randomUUID();
// Prepare request data
const requestData = {
jsonrpc: "2.0",
id: callId,
method: "message/send",
params: {
id: taskId,
sessionId: "session-" + Math.random().toString(36).substring(2, 9),
message: {
role: "user",
parts: [
{
type: "text",
text: message
}
]
}
}
};
try {
// Indicate loading state
setIsLoading(true);
// Send the request
const response = await fetch(\`/api/v1/a2a/\${agentId}\`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY_HERE'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error(\`HTTP error: \${response.status}\`);
}
// Process the response
const data = await response.json();
// Check for errors
if (data.error) {
console.error('Error in response:', data.error);
return null;
}
// Extract the agent response
const task = data.result;
// Show response in UI
if (task.status.message && task.status.message.parts) {
const responseText = task.status.message.parts
.filter(part => part.type === 'text')
.map(part => part.text)
.join('');
// Here you would update your React state
// setResponse(responseText);
return responseText;
}
return task;
} catch (error) {
console.error('Error sending task:', error);
return null;
} finally {
// Finalize loading state
setIsLoading(false);
}
}`)}
>
<ClipboardCopy className="h-4 w-4" />
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="streaming">
<div>
<h3 className="text-emerald-400 text-lg font-medium mb-2">Implementation of message/stream (Streaming)</h3>
<p className="text-neutral-300 mb-4">
Example of implementation of streaming with Server-Sent Events (SSE):
</p>
<div className="relative">
<CodeBlock
text={`async function initAgentStream(agentId, message, onUpdateCallback) {
// Generate unique IDs
const taskId = crypto.randomUUID();
const callId = crypto.randomUUID();
const sessionId = "session-" + Math.random().toString(36).substring(2, 9);
// Prepare request data for streaming
const requestData = {
jsonrpc: "2.0",
id: callId,
method: "message/stream",
params: {
id: taskId,
sessionId: sessionId,
message: {
role: "user",
parts: [
{
type: "text",
text: message
}
]
}
}
};
try {
// Start initial POST request
const response = await fetch(\`/api/v1/a2a/\${agentId}\`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY_HERE',
'Accept': 'text/event-stream' // Important for SSE
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error(\`HTTP error: \${response.status}\`);
}
// Check content type of the response
const contentType = response.headers.get('content-type');
// If the response is already SSE, use EventSource
if (contentType?.includes('text/event-stream')) {
// Use EventSource to process the stream
setupEventSource(\`/api/v1/a2a/\${agentId}/stream?taskId=\${taskId}&key=YOUR_API_KEY_HERE\`);
return;
}
// Function to configure EventSource
function setupEventSource(url) {
const eventSource = new EventSource(url);
// Handler for received messages
eventSource.onmessage = (event) => {
try {
// Process data from the event
const data = JSON.parse(event.data);
// Process the event
if (data.result) {
// Process status if available
if (data.result.status) {
const status = data.result.status;
// Extract text if available
let currentText = '';
if (status.message && status.message.parts) {
const parts = status.message.parts.filter(part => part.type === 'text');
if (parts.length > 0) {
currentText = parts.map(part => part.text).join('');
}
}
// Call callback with updates
onUpdateCallback({
text: currentText,
state: status.state,
complete: data.result.final === true
});
// If it's the final event, close the connection
if (data.result.final === true) {
eventSource.close();
onUpdateCallback({
complete: true,
state: status.state
});
}
}
// Process artifact if available
if (data.result.artifact) {
const artifact = data.result.artifact;
if (artifact.parts && artifact.parts.length > 0) {
const parts = artifact.parts.filter(part => part.type === 'text');
if (parts.length > 0) {
const artifactText = parts.map(part => part.text).join('');
// Call callback with artifact
onUpdateCallback({
text: artifactText,
state: 'artifact',
complete: artifact.lastChunk === true
});
// If it's the last chunk, close the connection
if (artifact.lastChunk === true) {
eventSource.close();
}
}
}
}
}
} catch (error) {
console.error('Error processing event:', error);
onUpdateCallback({ error: error.message });
}
};
// Handler for errors
eventSource.onerror = (error) => {
console.error('Error in EventSource:', error);
eventSource.close();
onUpdateCallback({
error: 'Connection with server interrupted',
complete: true,
state: 'error'
});
};
}
} catch (error) {
console.error('Error in streaming:', error);
// Notify error through callback
onUpdateCallback({
error: error.message,
complete: true,
state: 'error'
});
return null;
}
}`}
language="javascript"
/>
<Button
size="sm"
variant="ghost"
className="absolute top-2 right-2 text-white hover:bg-[#333]"
onClick={() => copyToClipboard(`async function initAgentStream(agentId, message, onUpdateCallback) {
// Generate unique IDs
const taskId = crypto.randomUUID();
const callId = crypto.randomUUID();
const sessionId = "session-" + Math.random().toString(36).substring(2, 9);
// Prepare request data for streaming
const requestData = {
jsonrpc: "2.0",
id: callId,
method: "message/stream",
params: {
id: taskId,
sessionId: sessionId,
message: {
role: "user",
parts: [
{
type: "text",
text: message
}
]
}
}
};
try {
// Start initial POST request
const response = await fetch(\`/api/v1/a2a/\${agentId}\`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY_HERE',
'Accept': 'text/event-stream' // Important for SSE
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error(\`HTTP error: \${response.status}\`);
}
// Check content type of the response
const contentType = response.headers.get('content-type');
// If the response is already SSE, use EventSource
if (contentType?.includes('text/event-stream')) {
// Use EventSource to process the stream
setupEventSource(\`/api/v1/a2a/\${agentId}/stream?taskId=\${taskId}&key=YOUR_API_KEY_HERE\`);
return;
}
// Function to configure EventSource
function setupEventSource(url) {
const eventSource = new EventSource(url);
// Handler for received messages
eventSource.onmessage = (event) => {
try {
// Process data from the event
const data = JSON.parse(event.data);
// Process the event
if (data.result) {
// Process status if available
if (data.result.status) {
const status = data.result.status;
// Extract text if available
let currentText = '';
if (status.message && status.message.parts) {
const parts = status.message.parts.filter(part => part.type === 'text');
if (parts.length > 0) {
currentText = parts.map(part => part.text).join('');
}
}
// Call callback with updates
onUpdateCallback({
text: currentText,
state: status.state,
complete: data.result.final === true
});
// If it's the final event, close the connection
if (data.result.final === true) {
eventSource.close();
onUpdateCallback({
complete: true,
state: status.state
});
}
}
// Process artifact if available
if (data.result.artifact) {
const artifact = data.result.artifact;
if (artifact.parts && artifact.parts.length > 0) {
const parts = artifact.parts.filter(part => part.type === 'text');
if (parts.length > 0) {
const artifactText = parts.map(part => part.text).join('');
// Call callback with artifact
onUpdateCallback({
text: artifactText,
state: 'artifact',
complete: artifact.lastChunk === true
});
// If it's the last chunk, close the connection
if (artifact.lastChunk === true) {
eventSource.close();
}
}
}
}
}
} catch (error) {
console.error('Error processing event:', error);
onUpdateCallback({ error: error.message });
}
};
// Handler for errors
eventSource.onerror = (error) => {
console.error('Error in EventSource:', error);
eventSource.close();
onUpdateCallback({
error: 'Connection with server interrupted',
complete: true,
state: 'error'
});
};
}
} catch (error) {
console.error('Error in streaming:', error);
// Notify error through callback
onUpdateCallback({
error: error.message,
complete: true,
state: 'error'
});
return null;
}
}`)}
>
<ClipboardCopy className="h-4 w-4" />
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="react-component">
<div className="mt-4">
<h4 className="font-medium text-emerald-400 mb-2">React component with streaming support:</h4>
<div className="relative">
<CodeBlock
text={`import React, { useState, useRef } from 'react';
function ChatComponentA2A() {
const [message, setMessage] = useState('');
const [response, setResponse] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const [status, setStatus] = useState('');
// Reference to the agentId
const agentId = 'your-agent-id';
// Callback for streaming updates
const handleStreamUpdate = (update) => {
if (update.error) {
// Handle error
setStatus('error');
return;
}
// Update text
setResponse(update.text);
// Update status
setStatus(update.state);
// If it's complete, finish streaming
if (update.complete) {
setIsStreaming(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!message.trim()) return;
// Clear previous response
setResponse('');
setStatus('submitted');
// Start streaming
setIsStreaming(true);
try {
// Start stream with the agent
await initAgentStream(agentId, message, handleStreamUpdate);
// Clear message field after sending
setMessage('');
} catch (error) {
console.error('Error:', error);
setStatus('error');
setIsStreaming(false);
}
};
// Render status indicator based on status
const renderStatusIndicator = () => {
switch (status) {
case 'submitted':
return <span className="badge badge-info">Sent</span>;
case 'working':
return <span className="badge badge-warning">Processing</span>;
case 'completed':
return <span className="badge badge-success">Completed</span>;
case 'error':
return <span className="badge badge-danger">Error</span>;
default:
return null;
}
};
return (
<div className="chat-container">
<div className="chat-messages">
{response && (
<div className="message agent-message">
<div className="message-header">
<div className="agent-name">A2A Agent</div>
{renderStatusIndicator()}
</div>
<div className="message-content">
{response}
{status === 'working' && !response && (
<div className="typing-indicator">
<span></span><span></span><span></span>
</div>
)}
</div>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="chat-input-form">
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type your message..."
disabled={isStreaming}
className="chat-input"
/>
<button
type="submit"
disabled={isStreaming || !message.trim()}
className="send-button"
>
{isStreaming ? 'Processing...' : 'Send'}
</button>
</form>
</div>
);
}`}
language="javascript"
/>
<Button
size="sm"
variant="ghost"
className="absolute top-2 right-2 text-white hover:bg-[#333]"
onClick={() => copyToClipboard(`import React, { useState, useRef } from 'react';
function ChatComponentA2A() {
const [message, setMessage] = useState('');
const [response, setResponse] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const [status, setStatus] = useState('');
// Reference to the agentId
const agentId = 'your-agent-id';
// Callback for streaming updates
const handleStreamUpdate = (update) => {
if (update.error) {
// Handle error
setStatus('error');
return;
}
// Update text
setResponse(update.text);
// Update status
setStatus(update.state);
// If it's complete, finish streaming
if (update.complete) {
setIsStreaming(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!message.trim()) return;
// Clear previous response
setResponse('');
setStatus('submitted');
// Start streaming
setIsStreaming(true);
try {
// Start stream with the agent
await initAgentStream(agentId, message, handleStreamUpdate);
// Clear message field after sending
setMessage('');
} catch (error) {
console.error('Error:', error);
setStatus('error');
setIsStreaming(false);
}
};
// Render status indicator based on status
const renderStatusIndicator = () => {
switch (status) {
case 'submitted':
return <span className="badge badge-info">Sent</span>;
case 'working':
return <span className="badge badge-warning">Processing</span>;
case 'completed':
return <span className="badge badge-success">Completed</span>;
case 'error':
return <span className="badge badge-danger">Error</span>;
default:
return null;
}
};
return (
<div className="chat-container">
<div className="chat-messages">
{response && (
<div className="message agent-message">
<div className="message-header">
<div className="agent-name">A2A Agent</div>
{renderStatusIndicator()}
</div>
<div className="message-content">
{response}
{status === 'working' && !response && (
<div className="typing-indicator">
<span></span><span></span><span></span>
</div>
)}
</div>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="chat-input-form">
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type your message..."
disabled={isStreaming}
className="chat-input"
/>
<button
type="submit"
disabled={isStreaming || !message.trim()}
className="send-button"
>
{isStreaming ? 'Processing...' : 'Send'}
</button>
</form>
</div>
);
}`)}
>
<ClipboardCopy className="h-4 w-4" />
</Button>
</div>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}