从零构建AI聊天应用:AG-UI协议全栈实战指南 - 完整前端代码
https://blog.csdn.net/weixin_40582941/article/details/155031787?spm=1011.2124.3001.6209
构建后端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AG-UI IM Client</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.header {
background: #2c3e50;
color: white;
padding: 20px;
text-align: center;
}
.header h1 {
font-size: 24px;
margin-bottom: 5px;
}
.header p {
opacity: 0.8;
font-size: 14px;
}
.chat-container {
display: flex;
flex-direction: column;
height: 600px;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #f8f9fa;
}
.message {
margin-bottom: 15px;
display: flex;
align-items: flex-start;
}
.message.user {
justify-content: flex-end;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 18px;
position: relative;
word-wrap: break-word;
}
.message.user .message-content {
background: #007bff;
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant .message-content {
background: white;
color: #333;
border: 1px solid #e0e0e0;
border-bottom-left-radius: 4px;
}
.message-role {
font-size: 12px;
font-weight: bold;
margin-bottom: 4px;
opacity: 0.7;
}
.message-time {
font-size: 11px;
text-align: right;
margin-top: 5px;
opacity: 0.6;
}
.typing-indicator {
display: none;
padding: 12px 16px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 18px;
border-bottom-left-radius: 4px;
max-width: 70%;
margin-bottom: 15px;
}
.typing-dots {
display: flex;
gap: 4px;
}
.typing-dot {
width: 8px;
height: 8px;
background: #999;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(1) { animation-delay: -0.32s; }
.typing-dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
40% { transform: scale(1); opacity: 1; }
}
.input-area {
padding: 20px;
background: white;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 10px;
}
.message-input {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 25px;
outline: none;
font-size: 14px;
transition: border-color 0.3s;
}
.message-input:focus {
border-color: #007bff;
}
.send-button {
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: background 0.3s;
}
.send-button:hover {
background: #0056b3;
}
.send-button:disabled {
background: #6c757d;
cursor: not-allowed;
}
.session-info {
padding: 10px 20px;
background: #e9ecef;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
}
.session-id {
font-family: monospace;
background: #2c3e50;
color: white;
padding: 4px 8px;
border-radius: 4px;
}
.controls {
display: flex;
gap: 10px;
}
.control-button {
padding: 6px 12px;
background: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.control-button:hover {
background: #5a6268;
}
.event-log {
background: #2c3e50;
color: #00ff00;
font-family: monospace;
padding: 10px;
max-height: 150px;
overflow-y: auto;
font-size: 12px;
border-top: 1px solid #34495e;
display: none;
}
.toggle-log {
background: #34495e;
color: white;
border: none;
padding: 5px 10px;
font-size: 12px;
cursor: pointer;
border-radius: 4px;
}
.status {
padding: 8px 20px;
background: #fff3cd;
border-bottom: 1px solid #ffeaa7;
font-size: 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.connection-status {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #6c757d;
}
.status-dot.connected {
background: #28a745;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 10px;
margin: 10px 20px;
border-radius: 5px;
border: 1px solid #f5c6cb;
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>AG-UI IM Client</h1>
<p>Real-time chat using AG-UI Protocol with FastAPI</p>
</div>
<div class="status">
<div class="connection-status">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Ready</span>
</div>
<button class="toggle-log" onclick="toggleEventLog()">Toggle Event Log</button>
</div>
<div class="error-message" id="errorMessage"></div>
<div class="session-info">
<div>Session: <span class="session-id" id="sessionId">loading...</span></div>
<div class="controls">
<button class="control-button" onclick="clearChat()">Clear Chat</button>
<button class="control-button" onclick="newSession()">New Session</button>
</div>
</div>
<div class="chat-container">
<div class="chat-messages" id="chatMessages">
<div class="message assistant">
<div class="message-content">
<div class="message-role">AG-UI Assistant</div>
Welcome to the AG-UI IM Client! Send a message to start chatting.
</div>
</div>
</div>
<div class="typing-indicator" id="typingIndicator">
<div class="typing-dots">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
<div class="input-area">
<input type="text"
class="message-input"
id="messageInput"
placeholder="Type your message..."
onkeypress="handleKeyPress(event)">
<button class="send-button" id="sendButton" onclick="sendMessage()">Send</button>
</div>
</div>
<div class="event-log" id="eventLog">
<div>AG-UI Event Log:</div>
</div>
</div>
<script>
// Configuration
const API_BASE_URL = 'http://localhost:8000';
let currentSessionId = null;
let currentRunId = null;
let currentMessageId = null;
let isProcessing = false;
// DOM Elements
const chatMessages = document.getElementById('chatMessages');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
const typingIndicator = document.getElementById('typingIndicator');
const sessionIdElement = document.getElementById('sessionId');
const eventLog = document.getElementById('eventLog');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const errorMessage = document.getElementById('errorMessage');
// Initialize
document.addEventListener('DOMContentLoaded', async () => {
await initializeSession();
messageInput.focus();
});
// Initialize or create session
async function initializeSession() {
try {
setStatus('Connecting...', 'connecting');
// Try to get existing session from localStorage
const savedSessionId = localStorage.getItem('agui_session_id');
if (savedSessionId) {
// Verify session exists
const response = await fetch(`${API_BASE_URL}/api/sessions/${savedSessionId}/messages`);
if (response.ok) {
currentSessionId = savedSessionId;
const data = await response.json();
loadChatHistory(data.messages);
} else {
await createNewSession();
}
} else {
await createNewSession();
}
setStatus('Connected', 'connected');
} catch (error) {
console.error('Failed to initialize session:', error);
setStatus('Connection Failed', 'error');
showError('Failed to connect to server. Please check if the FastAPI server is running.');
}
}
// Create new session
async function createNewSession() {
try {
const response = await fetch(`${API_BASE_URL}/api/sessions/new`);
const data = await response.json();
currentSessionId = data.session_id;
localStorage.setItem('agui_session_id', currentSessionId);
sessionIdElement.textContent = currentSessionId;
clearChatUI();
} catch (error) {
console.error('Failed to create session:', error);
throw error;
}
}
// Load chat history
function loadChatHistory(messages) {
clearChatUI();
messages.forEach(message => {
addMessageToChat(message.role, message.message, false);
});
sessionIdElement.textContent = currentSessionId;
}
// Send message to AG-UI API
async function sendMessage() {
const message = messageInput.value.trim();
if (!message || isProcessing) return;
// Clear input and disable send button
messageInput.value = '';
sendButton.disabled = true;
isProcessing = true;
// Add user message to chat
addMessageToChat('user', message, true);
// Show typing indicator
typingIndicator.style.display = 'block';
scrollToBottom();
try {
setStatus('Processing...', 'processing');
// Prepare request data
const requestData = {
message: message
};
// Make POST request to stream endpoint
const response = await fetch(`${API_BASE_URL}/api/chat/${currentSessionId}/stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Create a new assistant message element
const messageElement = createMessageElement('assistant');
chatMessages.appendChild(messageElement);
// Process the stream
const reader = response.body.getReader();
const decoder = new TextDecoder();
let assistantMessage = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.trim()) {
try {
var data = line.trim();
if (data.startsWith('data: ')) {
data = data.substring(6).trim();
}
const event = JSON.parse(data);
logEvent(event);
switch (event.type) {
case 'RUN_STARTED':
currentRunId = event.run_id;
setStatus('AI Processing', 'processing');
break;
case 'TEXT_MESSAGE_CONTENT':
assistantMessage += event.delta;
messageElement.querySelector('.message-text').textContent = assistantMessage;
scrollToBottom();
break;
case 'RUN_FINISHED':
setStatus('Connected', 'connected');
break;
case 'RUN_ERROR':
showError(`AG-UI Error: ${event.message}`);
break;
}
} catch (e) {
console.error('Error parsing event:', e, line);
}
}
}
}
// Hide typing indicator
typingIndicator.style.display = 'none';
} catch (error) {
console.error('Error sending message:', error);
showError(`Failed to send message: ${error.message}`);
typingIndicator.style.display = 'none';
setStatus('Error', 'error');
} finally {
// Re-enable input
sendButton.disabled = false;
isProcessing = false;
messageInput.focus();
}
}
// Add message to chat UI
function addMessageToChat(role, text, scroll = true) {
const messageElement = createMessageElement(role);
messageElement.querySelector('.message-text').textContent = text;
chatMessages.appendChild(messageElement);
if (scroll) {
scrollToBottom();
}
}
// Create message element
function createMessageElement(role) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}`;
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
const roleDiv = document.createElement('div');
roleDiv.className = 'message-role';
roleDiv.textContent = role === 'user' ? 'You' : 'AG-UI Assistant';
const textDiv = document.createElement('div');
textDiv.className = 'message-text';
const timeDiv = document.createElement('div');
timeDiv.className = 'message-time';
timeDiv.textContent = new Date().toLocaleTimeString();
messageContent.appendChild(roleDiv);
messageContent.appendChild(textDiv);
messageContent.appendChild(timeDiv);
messageDiv.appendChild(messageContent);
return messageDiv;
}
// Clear chat UI
function clearChatUI() {
chatMessages.innerHTML = `
<div class="message assistant">
<div class="message-content">
<div class="message-role">AG-UI Assistant</div>
Welcome to the AG-UI IM Client! Send a message to start chatting.
</div>
</div>
`;
}
// Clear chat (API call)
async function clearChat() {
try {
await fetch(`${API_BASE_URL}/api/sessions/${currentSessionId}/clear`, {
method: 'POST'
});
clearChatUI();
} catch (error) {
console.error('Error clearing chat:', error);
showError('Failed to clear chat history');
}
}
// Create new session
async function newSession() {
try {
await createNewSession();
showError(''); // Clear any previous errors
} catch (error) {
console.error('Error creating new session:', error);
showError('Failed to create new session');
}
}
// Handle Enter key press
function handleKeyPress(event) {
if (event.key === 'Enter') {
sendMessage();
}
}
// Scroll to bottom of chat
function scrollToBottom() {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// Set connection status
function setStatus(text, state) {
statusText.textContent = text;
statusDot.className = 'status-dot';
if (state) {
statusDot.classList.add(state);
}
}
// Show error message
function showError(message) {
if (message) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
} else {
errorMessage.style.display = 'none';
}
}
// Log AG-UI events
function logEvent(event) {
const logEntry = document.createElement('div');
logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${event.type}: ${JSON.stringify(event)}`;
eventLog.appendChild(logEntry);
eventLog.scrollTop = eventLog.scrollHeight;
}
// Toggle event log visibility
function toggleEventLog() {
eventLog.style.display = eventLog.style.display === 'none' ? 'block' : 'none';
}
</script>
</body>
</html>
https://blog.csdn.net/weixin_40582941/article/details/155031787
更多推荐

所有评论(0)