052591281d
- Complete agent system with planning and execution - Bridge API with FastAPI server - WhatsApp-style mobile dashboard - Enhanced web search with query improvement - Security guards and policy checks - Comprehensive documentation
342 lines
9.6 KiB
Python
342 lines
9.6 KiB
Python
"""Simple Dashboard - WhatsApp-style mobile UI for Agent Maxim."""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import HTMLResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
import uvicorn
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
app = FastAPI(title="Agent Maxim Dashboard")
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def dashboard():
|
|
"""Serve WhatsApp-style chat dashboard."""
|
|
html_content = """
|
|
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Agent Maxim</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
background: #e5ddd5;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#chat {
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: #e5ddd5;
|
|
}
|
|
|
|
#header {
|
|
background: #128C7E;
|
|
color: white;
|
|
padding: 12px 16px;
|
|
font-weight: 600;
|
|
font-size: 18px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
z-index: 10;
|
|
}
|
|
|
|
#messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.message {
|
|
max-width: 85%;
|
|
padding: 10px 14px;
|
|
border-radius: 12px;
|
|
font-size: 15px;
|
|
line-height: 1.4;
|
|
word-wrap: break-word;
|
|
animation: slideIn 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.user {
|
|
background: #dcf8c6;
|
|
margin-left: auto;
|
|
border-bottom-right-radius: 4px;
|
|
}
|
|
|
|
.agent {
|
|
background: white;
|
|
margin-right: auto;
|
|
border-bottom-left-radius: 4px;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.loading {
|
|
background: white;
|
|
margin-right: auto;
|
|
border-bottom-left-radius: 4px;
|
|
color: #666;
|
|
font-style: italic;
|
|
}
|
|
|
|
#inputBox {
|
|
display: flex;
|
|
padding: 10px;
|
|
background: #f0f0f0;
|
|
border-top: 1px solid #ddd;
|
|
gap: 8px;
|
|
}
|
|
|
|
input {
|
|
flex: 1;
|
|
padding: 12px 16px;
|
|
border-radius: 24px;
|
|
border: 1px solid #ccc;
|
|
outline: none;
|
|
font-size: 15px;
|
|
background: white;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
input:focus {
|
|
border-color: #128C7E;
|
|
box-shadow: 0 0 0 2px rgba(18, 140, 126, 0.1);
|
|
}
|
|
|
|
button {
|
|
padding: 12px 20px;
|
|
border: none;
|
|
border-radius: 24px;
|
|
background: #128C7E;
|
|
color: white;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 50px;
|
|
}
|
|
|
|
button:hover {
|
|
background: #0d6e63;
|
|
}
|
|
|
|
button:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
button:disabled {
|
|
background: #ccc;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.result-title {
|
|
font-weight: 600;
|
|
color: #128C7E;
|
|
margin-bottom: 4px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.result-snippet {
|
|
color: #555;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.result-link {
|
|
color: #06c;
|
|
text-decoration: none;
|
|
font-size: 12px;
|
|
display: block;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.result-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.divider {
|
|
border-top: 1px solid #eee;
|
|
margin: 8px 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="chat">
|
|
<div id="header">Agent Maxim</div>
|
|
<div id="messages"></div>
|
|
<div id="inputBox">
|
|
<input id="input" placeholder="Напиши задачу..." autocomplete="off" />
|
|
<button id="sendBtn" onclick="send()">➤</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_URL = '/runs';
|
|
const input = document.getElementById('input');
|
|
const sendBtn = document.getElementById('sendBtn');
|
|
|
|
function addMessage(text, cls) {
|
|
const msg = document.createElement('div');
|
|
msg.className = 'message ' + cls;
|
|
msg.innerHTML = text;
|
|
document.getElementById('messages').appendChild(msg);
|
|
scrollToBottom();
|
|
return msg;
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
const messages = document.getElementById('messages');
|
|
messages.scrollTop = messages.scrollHeight;
|
|
}
|
|
|
|
function formatResponse(text) {
|
|
if (!text) return 'Нет ответа';
|
|
|
|
// Check if it contains web search results
|
|
if (text.includes('Title:') && text.includes('Snippet:')) {
|
|
return formatSearchResults(text);
|
|
}
|
|
|
|
// Format numbered items
|
|
return text.replace(/\n/g, '<br>');
|
|
}
|
|
|
|
function formatSearchResults(text) {
|
|
const lines = text.split('\n');
|
|
let html = '';
|
|
let currentResult = null;
|
|
|
|
for (const line of lines) {
|
|
line = line.trim();
|
|
|
|
if (line.startsWith('Title:')) {
|
|
if (currentResult) {
|
|
html += formatSingleResult(currentResult);
|
|
}
|
|
currentResult = {
|
|
title: line.replace('Title:', '').trim(),
|
|
snippet: '',
|
|
url: ''
|
|
};
|
|
} else if (line.startsWith('Snippet:')) {
|
|
if (currentResult) {
|
|
currentResult.snippet = line.replace('Snippet:', '').trim();
|
|
}
|
|
} else if (line.startsWith('URL:')) {
|
|
if (currentResult) {
|
|
currentResult.url = line.replace('URL:', '').trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentResult) {
|
|
html += formatSingleResult(currentResult);
|
|
}
|
|
|
|
return html || text.replace(/\n/g, '<br>');
|
|
}
|
|
|
|
function formatSingleResult(result) {
|
|
html = '<div class="result-title">' + result.title + '</div>';
|
|
html += '<div class="result-snippet">' + result.snippet + '</div>';
|
|
if (result.url) {
|
|
html += '<a href="' + result.url + '" target="_blank" class="result-link">' + result.url + '</a>';
|
|
}
|
|
html += '<div class="divider"></div>';
|
|
return html;
|
|
}
|
|
|
|
async function send() {
|
|
const text = input.value.trim();
|
|
if (!text) return;
|
|
|
|
input.value = '';
|
|
input.disabled = true;
|
|
sendBtn.disabled = true;
|
|
|
|
addMessage(text, 'user');
|
|
const loadingMsg = addMessage('⏳ Думаю...', 'loading');
|
|
|
|
try {
|
|
const res = await fetch(API_URL, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
goal: text,
|
|
adapter: 'agent'
|
|
})
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
loadingMsg.remove();
|
|
|
|
if (data.response) {
|
|
addMessage(formatResponse(data.response), 'agent');
|
|
} else {
|
|
addMessage('Ошибка: нет ответа от агента', 'agent');
|
|
}
|
|
} catch (err) {
|
|
loadingMsg.remove();
|
|
addMessage('Ошибка: ' + err.message, 'agent');
|
|
}
|
|
|
|
input.disabled = false;
|
|
sendBtn.disabled = false;
|
|
input.focus();
|
|
}
|
|
|
|
input.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
send();
|
|
}
|
|
});
|
|
|
|
// Focus input on load
|
|
input.focus();
|
|
|
|
// Welcome message
|
|
setTimeout(() => {
|
|
addMessage('Привет! Я Agent Maxim. Чем могу помочь?', 'agent');
|
|
}, 500);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
return HTMLResponse(content=html_content)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("Starting Agent Maxim Dashboard on http://0.0.0.0:8610")
|
|
print("Mobile-friendly WhatsApp-style UI")
|
|
uvicorn.run(app, host="0.0.0.0", port=8610, reload=False)
|