mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 10:42:25 +08:00
✨ 新增 Web UI 功能以支援 SSH 遠端開發,並整合命令執行與即時回饋收集。
This commit is contained in:
parent
b3b9620608
commit
f93a6f9d87
@ -8,4 +8,8 @@ dependencies = [
|
||||
"fastmcp>=2.0.0",
|
||||
"psutil>=7.0.0",
|
||||
"pyside6>=6.8.2.1",
|
||||
"fastapi>=0.115.0",
|
||||
"uvicorn>=0.30.0",
|
||||
"jinja2>=3.1.0",
|
||||
"websockets>=13.0.0",
|
||||
]
|
||||
|
103
server.py
103
server.py
@ -15,7 +15,59 @@ from pydantic import Field
|
||||
# The log_level is necessary for Cline to work: https://github.com/jlowin/fastmcp/issues/81
|
||||
mcp = FastMCP("Interactive Feedback MCP", log_level="ERROR")
|
||||
|
||||
def is_ssh_session() -> bool:
|
||||
"""Check if we're running in an SSH session or remote environment"""
|
||||
# Check for SSH environment variables
|
||||
ssh_indicators = [
|
||||
'SSH_CONNECTION',
|
||||
'SSH_CLIENT',
|
||||
'SSH_TTY'
|
||||
]
|
||||
|
||||
for indicator in ssh_indicators:
|
||||
if os.getenv(indicator):
|
||||
return True
|
||||
|
||||
# Check if DISPLAY is not set (common in SSH without X11 forwarding)
|
||||
if sys.platform.startswith('linux') and not os.getenv('DISPLAY'):
|
||||
return True
|
||||
|
||||
# Check for other remote indicators
|
||||
if os.getenv('TERM_PROGRAM') == 'vscode' and os.getenv('VSCODE_INJECTION') == '1':
|
||||
# VSCode remote development
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def can_use_gui() -> bool:
|
||||
"""Check if GUI can be used in current environment"""
|
||||
if is_ssh_session():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Try to import Qt and check if display is available
|
||||
if sys.platform == 'win32':
|
||||
return True # Windows should generally support GUI
|
||||
elif sys.platform == 'darwin':
|
||||
return True # macOS should generally support GUI
|
||||
else:
|
||||
# Linux - check for DISPLAY
|
||||
return bool(os.getenv('DISPLAY'))
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
def launch_feedback_ui(project_directory: str, summary: str) -> dict[str, str]:
|
||||
"""Launch appropriate UI based on environment"""
|
||||
|
||||
if can_use_gui():
|
||||
# Use Qt GUI (original implementation)
|
||||
return launch_qt_feedback_ui(project_directory, summary)
|
||||
else:
|
||||
# Use Web UI
|
||||
return launch_web_feedback_ui(project_directory, summary)
|
||||
|
||||
def launch_qt_feedback_ui(project_directory: str, summary: str) -> dict[str, str]:
|
||||
"""Original Qt GUI implementation"""
|
||||
# Create a temporary file for the feedback result
|
||||
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
|
||||
output_file = tmp.name
|
||||
@ -58,6 +110,57 @@ def launch_feedback_ui(project_directory: str, summary: str) -> dict[str, str]:
|
||||
os.unlink(output_file)
|
||||
raise e
|
||||
|
||||
def launch_web_feedback_ui(project_directory: str, summary: str) -> dict[str, str]:
|
||||
"""Launch Web UI implementation"""
|
||||
try:
|
||||
from web_ui import launch_web_feedback_ui as launch_web
|
||||
return launch_web(project_directory, summary)
|
||||
except ImportError as e:
|
||||
# Fallback to command line if web UI fails
|
||||
print(f"Web UI not available: {e}")
|
||||
return launch_cli_feedback_ui(project_directory, summary)
|
||||
|
||||
def launch_cli_feedback_ui(project_directory: str, summary: str) -> dict[str, str]:
|
||||
"""Simple command line fallback"""
|
||||
print(f"\n{'='*60}")
|
||||
print("Interactive Feedback MCP")
|
||||
print(f"{'='*60}")
|
||||
print(f"專案目錄: {project_directory}")
|
||||
print(f"任務描述: {summary}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Ask for command to run
|
||||
command = input("要執行的命令 (留空跳過): ").strip()
|
||||
command_logs = ""
|
||||
|
||||
if command:
|
||||
print(f"執行命令: {command}")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
cwd=project_directory,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="ignore"
|
||||
)
|
||||
command_logs = f"$ {command}\n{result.stdout}{result.stderr}"
|
||||
print(command_logs)
|
||||
except Exception as e:
|
||||
command_logs = f"$ {command}\nError: {str(e)}\n"
|
||||
print(command_logs)
|
||||
|
||||
# Ask for feedback
|
||||
print(f"\n{'='*60}")
|
||||
print("請提供您的回饋意見:")
|
||||
feedback = input().strip()
|
||||
|
||||
return {
|
||||
"command_logs": command_logs,
|
||||
"interactive_feedback": feedback
|
||||
}
|
||||
|
||||
def first_line(text: str) -> str:
|
||||
return text.split("\n")[0].strip()
|
||||
|
||||
|
447
static/style.css
Normal file
447
static/style.css
Normal file
@ -0,0 +1,447 @@
|
||||
/* Interactive Feedback MCP - Modern Dark Theme */
|
||||
:root {
|
||||
--primary-color: #007acc;
|
||||
--primary-hover: #005999;
|
||||
--background-color: #1e1e1e;
|
||||
--surface-color: #2d2d30;
|
||||
--surface-hover: #383838;
|
||||
--text-primary: #cccccc;
|
||||
--text-secondary: #9e9e9e;
|
||||
--text-accent: #007acc;
|
||||
--border-color: #464647;
|
||||
--success-color: #4caf50;
|
||||
--warning-color: #ff9800;
|
||||
--error-color: #f44336;
|
||||
--console-bg: #1a1a1a;
|
||||
--input-bg: #2d2d30;
|
||||
--button-bg: #0e639c;
|
||||
--button-hover-bg: #1177bb;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: var(--text-accent);
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
h2, h3 {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.3em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: var(--surface-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.section:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.session-info {
|
||||
background: linear-gradient(135deg, var(--surface-color), var(--surface-hover));
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.session-info p {
|
||||
margin-bottom: 8px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.session-info strong {
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
width: 100%;
|
||||
background-color: var(--button-bg);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background-color: var(--button-hover-bg);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.command-section {
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
flex: 1;
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 12px 15px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.1);
|
||||
}
|
||||
|
||||
.input-group button {
|
||||
background-color: var(--button-bg);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.input-group button:hover {
|
||||
background-color: var(--button-hover-bg);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
#stop-btn {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
|
||||
#stop-btn:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--primary-color);
|
||||
}
|
||||
|
||||
#save-config {
|
||||
background-color: var(--success-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#save-config:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.console-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.console-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.console-header h4 {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
#clear-logs {
|
||||
background-color: var(--warning-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#clear-logs:hover {
|
||||
background-color: #f57c00;
|
||||
}
|
||||
|
||||
.console {
|
||||
background-color: var(--console-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.console-line {
|
||||
margin-bottom: 2px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.console::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.console::-webkit-scrollbar-track {
|
||||
background: var(--surface-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.console::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.console::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-secondary);
|
||||
}
|
||||
|
||||
.feedback-section {
|
||||
background: linear-gradient(135deg, var(--surface-color), var(--surface-hover));
|
||||
border-left: 4px solid var(--success-color);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.feedback-description {
|
||||
background-color: var(--input-bg);
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
border-left: 3px solid var(--primary-color);
|
||||
font-style: italic;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
#feedback-input {
|
||||
flex-grow: 1;
|
||||
min-height: 150px;
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#feedback-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.1);
|
||||
}
|
||||
|
||||
#feedback-input::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
#submit-feedback {
|
||||
background: linear-gradient(135deg, var(--success-color), #66bb6a);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
#submit-feedback:hover {
|
||||
background: linear-gradient(135deg, #45a049, #4caf50);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: var(--text-accent);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
color: var(--primary-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid var(--border-color);
|
||||
border-top: 4px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading p {
|
||||
color: var(--text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.input-group button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.console {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
#feedback-input {
|
||||
min-height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth transitions for all interactive elements */
|
||||
* {
|
||||
transition: color 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Focus styles for accessibility */
|
||||
button:focus,
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Custom selection colors */
|
||||
::selection {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
245
templates/feedback.html
Normal file
245
templates/feedback.html
Normal file
@ -0,0 +1,245 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Interactive Feedback MCP</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Interactive Feedback MCP</h1>
|
||||
|
||||
<div class="session-info">
|
||||
<h2>專案資訊</h2>
|
||||
<p><strong>工作目錄:</strong> <span id="project-dir">{{ project_directory }}</span></p>
|
||||
<p><strong>任務描述:</strong> <span id="summary">{{ summary }}</span></p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<button id="toggle-command" class="toggle-btn">顯示命令區塊</button>
|
||||
</div>
|
||||
|
||||
<div id="command-section" class="section command-section" style="display: none;">
|
||||
<h3>命令執行</h3>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="text" id="command-input" placeholder="輸入要執行的命令...">
|
||||
<button id="run-btn">執行</button>
|
||||
<button id="stop-btn" style="display: none;">停止</button>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="checkbox" id="auto-execute"> 下次自動執行
|
||||
</label>
|
||||
<button id="save-config">儲存設定</button>
|
||||
</div>
|
||||
|
||||
<div class="console-section">
|
||||
<div class="console-header">
|
||||
<h4>控制台輸出</h4>
|
||||
<button id="clear-logs">清除</button>
|
||||
</div>
|
||||
<div id="console" class="console"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section feedback-section">
|
||||
<h3>回饋意見</h3>
|
||||
<p class="feedback-description">{{ summary }}</p>
|
||||
<textarea id="feedback-input" placeholder="請在此輸入您的回饋意見... (Ctrl+Enter 提交)"></textarea>
|
||||
<button id="submit-feedback">提交回饋 (Ctrl+Enter)</button>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>需要改進?聯繫 Fábio Ferreira 在 <a href="https://x.com/fabiomlferreira" target="_blank">X.com</a> 或訪問 <a href="https://dotcursorrules.com/" target="_blank">dotcursorrules.com</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loading" class="loading" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
<p>正在處理...</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const sessionId = "{{ session_id }}";
|
||||
const wsUrl = `ws://${window.location.host}/ws/${sessionId}`;
|
||||
let socket;
|
||||
let commandRunning = false;
|
||||
|
||||
// DOM elements
|
||||
const toggleCommandBtn = document.getElementById('toggle-command');
|
||||
const commandSection = document.getElementById('command-section');
|
||||
const commandInput = document.getElementById('command-input');
|
||||
const runBtn = document.getElementById('run-btn');
|
||||
const stopBtn = document.getElementById('stop-btn');
|
||||
const autoExecuteCheck = document.getElementById('auto-execute');
|
||||
const saveConfigBtn = document.getElementById('save-config');
|
||||
const console = document.getElementById('console');
|
||||
const clearLogsBtn = document.getElementById('clear-logs');
|
||||
const feedbackInput = document.getElementById('feedback-input');
|
||||
const submitFeedbackBtn = document.getElementById('submit-feedback');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
// Initialize WebSocket connection
|
||||
function initWebSocket() {
|
||||
socket = new WebSocket(wsUrl);
|
||||
|
||||
socket.onopen = function() {
|
||||
console.log('WebSocket 已連接');
|
||||
};
|
||||
|
||||
socket.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
handleWebSocketMessage(data);
|
||||
};
|
||||
|
||||
socket.onclose = function() {
|
||||
console.log('WebSocket 連接已關閉');
|
||||
setTimeout(initWebSocket, 3000); // Reconnect after 3 seconds
|
||||
};
|
||||
|
||||
socket.onerror = function(error) {
|
||||
console.error('WebSocket 錯誤:', error);
|
||||
};
|
||||
}
|
||||
|
||||
function handleWebSocketMessage(data) {
|
||||
switch(data.type) {
|
||||
case 'init':
|
||||
// Set initial values
|
||||
commandInput.value = data.config.run_command || '';
|
||||
autoExecuteCheck.checked = data.config.execute_automatically || false;
|
||||
|
||||
// Load existing logs
|
||||
data.logs.forEach(log => {
|
||||
appendToConsole(log);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'log':
|
||||
appendToConsole(data.data);
|
||||
break;
|
||||
|
||||
case 'process_completed':
|
||||
commandRunning = false;
|
||||
updateRunButtonState();
|
||||
feedbackInput.focus();
|
||||
break;
|
||||
|
||||
case 'logs_cleared':
|
||||
console.innerHTML = '';
|
||||
break;
|
||||
|
||||
case 'feedback_submitted':
|
||||
showLoading();
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 2000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function appendToConsole(text) {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'console-line';
|
||||
line.textContent = text;
|
||||
console.appendChild(line);
|
||||
console.scrollTop = console.scrollHeight;
|
||||
}
|
||||
|
||||
function updateRunButtonState() {
|
||||
if (commandRunning) {
|
||||
runBtn.style.display = 'none';
|
||||
stopBtn.style.display = 'inline-block';
|
||||
} else {
|
||||
runBtn.style.display = 'inline-block';
|
||||
stopBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
loading.style.display = 'flex';
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
toggleCommandBtn.addEventListener('click', () => {
|
||||
const isVisible = commandSection.style.display !== 'none';
|
||||
commandSection.style.display = isVisible ? 'none' : 'block';
|
||||
toggleCommandBtn.textContent = isVisible ? '顯示命令區塊' : '隱藏命令區塊';
|
||||
});
|
||||
|
||||
runBtn.addEventListener('click', () => {
|
||||
const command = commandInput.value.trim();
|
||||
if (command) {
|
||||
commandRunning = true;
|
||||
updateRunButtonState();
|
||||
socket.send(JSON.stringify({
|
||||
type: 'run_command',
|
||||
command: command
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
stopBtn.addEventListener('click', () => {
|
||||
commandRunning = false;
|
||||
updateRunButtonState();
|
||||
socket.send(JSON.stringify({
|
||||
type: 'stop_command'
|
||||
}));
|
||||
});
|
||||
|
||||
commandInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
runBtn.click();
|
||||
}
|
||||
});
|
||||
|
||||
saveConfigBtn.addEventListener('click', () => {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'update_config',
|
||||
config: {
|
||||
run_command: commandInput.value,
|
||||
execute_automatically: autoExecuteCheck.checked
|
||||
}
|
||||
}));
|
||||
|
||||
// Visual feedback
|
||||
saveConfigBtn.textContent = '已儲存';
|
||||
setTimeout(() => {
|
||||
saveConfigBtn.textContent = '儲存設定';
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
clearLogsBtn.addEventListener('click', () => {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'clear_logs'
|
||||
}));
|
||||
});
|
||||
|
||||
submitFeedbackBtn.addEventListener('click', () => {
|
||||
const feedback = feedbackInput.value.trim();
|
||||
socket.send(JSON.stringify({
|
||||
type: 'submit_feedback',
|
||||
feedback: feedback
|
||||
}));
|
||||
});
|
||||
|
||||
feedbackInput.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
submitFeedbackBtn.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
initWebSocket();
|
||||
feedbackInput.focus();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
41
templates/index.html
Normal file
41
templates/index.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Interactive Feedback MCP Server</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Interactive Feedback MCP</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>服務器狀態</h2>
|
||||
<p>🟢 MCP 服務器正在運行</p>
|
||||
<p>等待來自 AI 助手的互動請求...</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>關於此服務</h3>
|
||||
<p>這是一個 Model Context Protocol (MCP) 服務器,用於在 AI 輔助開發工具中提供人在回路的互動回饋功能。</p>
|
||||
<p>當 AI 助手需要用戶回饋時,會自動在瀏覽器中開啟互動頁面。</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>功能特色</h3>
|
||||
<ul style="color: var(--text-primary); margin-left: 20px;">
|
||||
<li>🌐 Web UI 支援 SSH remote 開發</li>
|
||||
<li>💻 即時命令執行和輸出顯示</li>
|
||||
<li>💬 結構化回饋收集</li>
|
||||
<li>⚙️ 專案特定的設定管理</li>
|
||||
<li>🔄 WebSocket 即時通訊</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>開發者: Fábio Ferreira | <a href="https://x.com/fabiomlferreira" target="_blank">X.com</a> | <a href="https://dotcursorrules.com/" target="_blank">dotcursorrules.com</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
239
test_web_ui.py
Normal file
239
test_web_ui.py
Normal file
@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for Interactive Feedback MCP Web UI
|
||||
"""
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import socket
|
||||
from pathlib import Path
|
||||
|
||||
def find_free_port():
|
||||
"""Find a free port to use for testing"""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(('', 0))
|
||||
s.listen(1)
|
||||
port = s.getsockname()[1]
|
||||
return port
|
||||
|
||||
def test_web_ui(keep_running=False):
|
||||
"""Test the Web UI functionality"""
|
||||
|
||||
print("🧪 測試 Interactive Feedback MCP Web UI")
|
||||
print("=" * 50)
|
||||
|
||||
# Test import
|
||||
try:
|
||||
from web_ui import WebUIManager, launch_web_feedback_ui
|
||||
print("✅ Web UI 模組匯入成功")
|
||||
except ImportError as e:
|
||||
print(f"❌ Web UI 模組匯入失敗: {e}")
|
||||
return False, None
|
||||
|
||||
# Find free port
|
||||
try:
|
||||
free_port = find_free_port()
|
||||
print(f"🔍 找到可用端口: {free_port}")
|
||||
except Exception as e:
|
||||
print(f"❌ 尋找可用端口失敗: {e}")
|
||||
return False, None
|
||||
|
||||
# Test manager creation
|
||||
try:
|
||||
manager = WebUIManager(port=free_port)
|
||||
print("✅ WebUIManager 創建成功")
|
||||
except Exception as e:
|
||||
print(f"❌ WebUIManager 創建失敗: {e}")
|
||||
return False, None
|
||||
|
||||
# Test server start (with timeout)
|
||||
server_started = False
|
||||
try:
|
||||
print("🚀 啟動 Web 服務器...")
|
||||
|
||||
def start_server():
|
||||
try:
|
||||
manager.start_server()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"服務器啟動錯誤: {e}")
|
||||
return False
|
||||
|
||||
# Start server in thread
|
||||
server_thread = threading.Thread(target=start_server)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
# Wait a moment and test if server is responsive
|
||||
time.sleep(3)
|
||||
|
||||
# Test if port is listening
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.settimeout(1)
|
||||
result = s.connect_ex((manager.host, manager.port))
|
||||
if result == 0:
|
||||
server_started = True
|
||||
print("✅ Web 服務器啟動成功")
|
||||
print(f"🌐 服務器運行在: http://{manager.host}:{manager.port}")
|
||||
else:
|
||||
print(f"❌ 無法連接到服務器端口 {manager.port}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Web 服務器啟動失敗: {e}")
|
||||
return False, None
|
||||
|
||||
if not server_started:
|
||||
print("❌ 服務器未能正常啟動")
|
||||
return False, None
|
||||
|
||||
# Test session creation
|
||||
session_info = None
|
||||
try:
|
||||
project_dir = str(Path.cwd())
|
||||
summary = "測試 Web UI 功能"
|
||||
session_id = manager.create_session(project_dir, summary)
|
||||
session_info = {
|
||||
'manager': manager,
|
||||
'session_id': session_id,
|
||||
'url': f"http://{manager.host}:{manager.port}/session/{session_id}"
|
||||
}
|
||||
print(f"✅ 測試會話創建成功 (ID: {session_id[:8]}...)")
|
||||
print(f"🔗 測試 URL: {session_info['url']}")
|
||||
except Exception as e:
|
||||
print(f"❌ 會話創建失敗: {e}")
|
||||
return False, None
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("🎉 所有測試通過!Web UI 準備就緒")
|
||||
print("📝 注意事項:")
|
||||
print(" - Web UI 會在 SSH remote 環境下自動啟用")
|
||||
print(" - 本地環境會繼續使用 Qt GUI")
|
||||
print(" - 支援即時命令執行和 WebSocket 通訊")
|
||||
print(" - 提供現代化的深色主題界面")
|
||||
|
||||
return True, session_info
|
||||
|
||||
def test_environment_detection():
|
||||
"""Test environment detection logic"""
|
||||
print("🔍 測試環境檢測功能")
|
||||
print("-" * 30)
|
||||
|
||||
try:
|
||||
from server import is_ssh_session, can_use_gui
|
||||
|
||||
ssh_detected = is_ssh_session()
|
||||
gui_available = can_use_gui()
|
||||
|
||||
print(f"SSH 環境檢測: {'是' if ssh_detected else '否'}")
|
||||
print(f"GUI 可用性: {'是' if gui_available else '否'}")
|
||||
|
||||
if ssh_detected:
|
||||
print("✅ 將使用 Web UI (適合 SSH remote 開發)")
|
||||
else:
|
||||
print("✅ 將使用 Qt GUI (本地環境)")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 環境檢測失敗: {e}")
|
||||
return False
|
||||
|
||||
def test_mcp_integration():
|
||||
"""Test MCP server integration"""
|
||||
print("\n🔧 測試 MCP 整合功能")
|
||||
print("-" * 30)
|
||||
|
||||
try:
|
||||
from server import interactive_feedback
|
||||
print("✅ MCP 工具函數可用")
|
||||
|
||||
# Test would require actual MCP call, so just verify import
|
||||
print("✅ 準備接受來自 AI 助手的調用")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ MCP 整合測試失敗: {e}")
|
||||
return False
|
||||
|
||||
def interactive_demo(session_info):
|
||||
"""Run interactive demo with the Web UI"""
|
||||
print(f"\n🌐 Web UI 持久化運行模式")
|
||||
print("=" * 50)
|
||||
print(f"服務器地址: http://{session_info['manager'].host}:{session_info['manager'].port}")
|
||||
print(f"測試會話: {session_info['url']}")
|
||||
print("\n📖 操作指南:")
|
||||
print(" 1. 在瀏覽器中開啟上面的測試 URL")
|
||||
print(" 2. 嘗試以下功能:")
|
||||
print(" - 點擊 '顯示命令區塊' 按鈕")
|
||||
print(" - 輸入命令如 'echo Hello World' 並執行")
|
||||
print(" - 在回饋區域輸入文字")
|
||||
print(" - 使用 Ctrl+Enter 提交回饋")
|
||||
print(" 3. 測試 WebSocket 即時通訊功能")
|
||||
print("\n⌨️ 控制選項:")
|
||||
print(" - 按 Enter 繼續運行")
|
||||
print(" - 輸入 'q' 或 'quit' 停止服務器")
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n>>> ").strip().lower()
|
||||
if user_input in ['q', 'quit', 'exit']:
|
||||
print("🛑 停止服務器...")
|
||||
break
|
||||
elif user_input == '':
|
||||
print(f"🔄 服務器持續運行在: {session_info['url']}")
|
||||
print(" 瀏覽器應該仍可正常訪問")
|
||||
else:
|
||||
print("❓ 未知命令。按 Enter 繼續運行,或輸入 'q' 退出")
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 收到中斷信號,停止服務器...")
|
||||
break
|
||||
|
||||
print("✅ Web UI 測試完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Interactive Feedback MCP - Web UI 測試")
|
||||
print("=" * 60)
|
||||
|
||||
# Check if user wants persistent mode
|
||||
persistent_mode = len(sys.argv) > 1 and sys.argv[1] in ['--persistent', '-p', '--demo']
|
||||
|
||||
if not persistent_mode:
|
||||
print("💡 提示: 使用 'python test_web_ui.py --persistent' 啟動持久化測試模式")
|
||||
print()
|
||||
|
||||
# Test environment detection
|
||||
env_test = test_environment_detection()
|
||||
|
||||
# Test MCP integration
|
||||
mcp_test = test_mcp_integration()
|
||||
|
||||
# Test Web UI
|
||||
web_test, session_info = test_web_ui()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if env_test and mcp_test and web_test:
|
||||
print("🎊 所有測試完成!準備使用 Interactive Feedback MCP")
|
||||
print("\n📖 使用方法:")
|
||||
print(" 1. 在 Cursor/Cline 中配置此 MCP 服務器")
|
||||
print(" 2. AI 助手會自動調用 interactive_feedback 工具")
|
||||
print(" 3. 根據環境自動選擇 GUI 或 Web UI")
|
||||
print(" 4. 提供回饋後繼續工作流程")
|
||||
|
||||
print("\n✨ Web UI 新功能:")
|
||||
print(" - 支援 SSH remote 開發環境")
|
||||
print(" - 現代化深色主題界面")
|
||||
print(" - WebSocket 即時通訊")
|
||||
print(" - 自動瀏覽器啟動")
|
||||
print(" - 命令執行和即時輸出")
|
||||
|
||||
if persistent_mode and session_info:
|
||||
interactive_demo(session_info)
|
||||
else:
|
||||
print("\n✅ 測試完成 - 系統已準備就緒!")
|
||||
if session_info:
|
||||
print(f"💡 您可以現在就在瀏覽器中測試: {session_info['url']}")
|
||||
print(" (服務器會繼續運行一小段時間)")
|
||||
time.sleep(10) # Keep running for a short time for immediate testing
|
||||
else:
|
||||
print("❌ 部分測試失敗,請檢查錯誤信息")
|
||||
sys.exit(1)
|
355
web_ui.py
Normal file
355
web_ui.py
Normal file
@ -0,0 +1,355 @@
|
||||
# Interactive Feedback MCP Web UI
|
||||
# Developed by Fábio Ferreira (https://x.com/fabiomlferreira)
|
||||
# Web UI version for SSH remote development
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import uuid
|
||||
import asyncio
|
||||
import webbrowser
|
||||
import threading
|
||||
import subprocess
|
||||
import psutil
|
||||
import time
|
||||
from typing import Dict, Optional, List
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
import uvicorn
|
||||
|
||||
class WebFeedbackSession:
|
||||
def __init__(self, session_id: str, project_directory: str, summary: str):
|
||||
self.session_id = session_id
|
||||
self.project_directory = project_directory
|
||||
self.summary = summary
|
||||
self.websocket: Optional[WebSocket] = None
|
||||
self.feedback_result: Optional[str] = None
|
||||
self.command_logs: List[str] = []
|
||||
self.process: Optional[subprocess.Popen] = None
|
||||
self.completed = False
|
||||
self.config = {
|
||||
"run_command": "",
|
||||
"execute_automatically": False
|
||||
}
|
||||
|
||||
class WebUIManager:
|
||||
def __init__(self, host: str = "127.0.0.1", port: int = 8765):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.app = FastAPI(title="Interactive Feedback MCP")
|
||||
self.sessions: Dict[str, WebFeedbackSession] = {}
|
||||
self.server_process = None
|
||||
self.setup_routes()
|
||||
|
||||
# Setup static files and templates
|
||||
script_dir = Path(__file__).parent
|
||||
static_dir = script_dir / "static"
|
||||
templates_dir = script_dir / "templates"
|
||||
static_dir.mkdir(exist_ok=True)
|
||||
templates_dir.mkdir(exist_ok=True)
|
||||
|
||||
self.app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||
self.templates = Jinja2Templates(directory=templates_dir)
|
||||
|
||||
def setup_routes(self):
|
||||
@self.app.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request):
|
||||
return self.templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
@self.app.get("/session/{session_id}", response_class=HTMLResponse)
|
||||
async def session_page(request: Request, session_id: str):
|
||||
session = self.sessions.get(session_id)
|
||||
if not session:
|
||||
return HTMLResponse("Session not found", status_code=404)
|
||||
|
||||
return self.templates.TemplateResponse("feedback.html", {
|
||||
"request": request,
|
||||
"session_id": session_id,
|
||||
"project_directory": session.project_directory,
|
||||
"summary": session.summary
|
||||
})
|
||||
|
||||
@self.app.websocket("/ws/{session_id}")
|
||||
async def websocket_endpoint(websocket: WebSocket, session_id: str):
|
||||
await websocket.accept()
|
||||
|
||||
session = self.sessions.get(session_id)
|
||||
if not session:
|
||||
await websocket.close(code=4000, reason="Session not found")
|
||||
return
|
||||
|
||||
session.websocket = websocket
|
||||
|
||||
# Send initial data
|
||||
await websocket.send_json({
|
||||
"type": "init",
|
||||
"project_directory": session.project_directory,
|
||||
"summary": session.summary,
|
||||
"config": session.config,
|
||||
"logs": session.command_logs
|
||||
})
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
await self.handle_websocket_message(session, data)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
session.websocket = None
|
||||
|
||||
@self.app.post("/api/complete/{session_id}")
|
||||
async def complete_session(session_id: str, feedback_data: dict):
|
||||
session = self.sessions.get(session_id)
|
||||
if not session:
|
||||
return {"error": "Session not found"}
|
||||
|
||||
session.feedback_result = feedback_data.get("feedback", "")
|
||||
session.completed = True
|
||||
|
||||
return {"success": True}
|
||||
|
||||
async def handle_websocket_message(self, session: WebFeedbackSession, data: dict):
|
||||
message_type = data.get("type")
|
||||
|
||||
if message_type == "run_command":
|
||||
command = data.get("command", "")
|
||||
await self.run_command(session, command)
|
||||
|
||||
elif message_type == "stop_command":
|
||||
await self.stop_command(session)
|
||||
|
||||
elif message_type == "submit_feedback":
|
||||
feedback = data.get("feedback", "")
|
||||
session.feedback_result = feedback
|
||||
session.completed = True
|
||||
|
||||
await session.websocket.send_json({
|
||||
"type": "feedback_submitted",
|
||||
"message": "Feedback submitted successfully"
|
||||
})
|
||||
|
||||
elif message_type == "update_config":
|
||||
session.config.update(data.get("config", {}))
|
||||
|
||||
elif message_type == "clear_logs":
|
||||
session.command_logs.clear()
|
||||
await session.websocket.send_json({
|
||||
"type": "logs_cleared"
|
||||
})
|
||||
|
||||
async def run_command(self, session: WebFeedbackSession, command: str):
|
||||
if session.process:
|
||||
await self.stop_command(session)
|
||||
|
||||
if not command.strip():
|
||||
await session.websocket.send_json({
|
||||
"type": "log",
|
||||
"data": "Please enter a command to run\n"
|
||||
})
|
||||
return
|
||||
|
||||
session.command_logs.append(f"$ {command}\n")
|
||||
await session.websocket.send_json({
|
||||
"type": "log",
|
||||
"data": f"$ {command}\n"
|
||||
})
|
||||
|
||||
try:
|
||||
session.process = subprocess.Popen(
|
||||
command,
|
||||
shell=True,
|
||||
cwd=session.project_directory,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
encoding="utf-8",
|
||||
errors="ignore"
|
||||
)
|
||||
|
||||
# Start threads to read output
|
||||
threading.Thread(
|
||||
target=self.read_process_output,
|
||||
args=(session, session.process.stdout),
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
threading.Thread(
|
||||
target=self.read_process_output,
|
||||
args=(session, session.process.stderr),
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
# Monitor process completion
|
||||
threading.Thread(
|
||||
target=self.monitor_process,
|
||||
args=(session,),
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error running command: {str(e)}\n"
|
||||
session.command_logs.append(error_msg)
|
||||
await session.websocket.send_json({
|
||||
"type": "log",
|
||||
"data": error_msg
|
||||
})
|
||||
|
||||
def read_process_output(self, session: WebFeedbackSession, pipe):
|
||||
try:
|
||||
for line in iter(pipe.readline, ""):
|
||||
if not line:
|
||||
break
|
||||
session.command_logs.append(line)
|
||||
if session.websocket:
|
||||
# Use threading to send async message
|
||||
threading.Thread(
|
||||
target=self._send_websocket_message,
|
||||
args=(session.websocket, {
|
||||
"type": "log",
|
||||
"data": line
|
||||
}),
|
||||
daemon=True
|
||||
).start()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def monitor_process(self, session: WebFeedbackSession):
|
||||
if session.process:
|
||||
exit_code = session.process.wait()
|
||||
completion_msg = f"\nProcess exited with code {exit_code}\n"
|
||||
session.command_logs.append(completion_msg)
|
||||
|
||||
if session.websocket:
|
||||
threading.Thread(
|
||||
target=self._send_websocket_message,
|
||||
args=(session.websocket, {
|
||||
"type": "log",
|
||||
"data": completion_msg
|
||||
}),
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
threading.Thread(
|
||||
target=self._send_websocket_message,
|
||||
args=(session.websocket, {
|
||||
"type": "process_completed",
|
||||
"exit_code": exit_code
|
||||
}),
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
session.process = None
|
||||
|
||||
def _send_websocket_message(self, websocket: WebSocket, message: dict):
|
||||
"""Helper to send websocket message from thread"""
|
||||
try:
|
||||
import asyncio
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(websocket.send_json(message))
|
||||
loop.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def stop_command(self, session: WebFeedbackSession):
|
||||
if session.process:
|
||||
try:
|
||||
# Kill process tree
|
||||
parent = psutil.Process(session.process.pid)
|
||||
for child in parent.children(recursive=True):
|
||||
try:
|
||||
child.kill()
|
||||
except psutil.Error:
|
||||
pass
|
||||
parent.kill()
|
||||
session.process = None
|
||||
|
||||
await session.websocket.send_json({
|
||||
"type": "log",
|
||||
"data": "\nProcess stopped\n"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
await session.websocket.send_json({
|
||||
"type": "log",
|
||||
"data": f"\nError stopping process: {str(e)}\n"
|
||||
})
|
||||
|
||||
def create_session(self, project_directory: str, summary: str) -> str:
|
||||
session_id = str(uuid.uuid4())
|
||||
session = WebFeedbackSession(session_id, project_directory, summary)
|
||||
self.sessions[session_id] = session
|
||||
return session_id
|
||||
|
||||
def start_server(self):
|
||||
"""Start the web server in a separate thread"""
|
||||
if self.server_process is not None:
|
||||
return # Server already running
|
||||
|
||||
def run_server():
|
||||
uvicorn.run(
|
||||
self.app,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
log_level="error",
|
||||
access_log=False
|
||||
)
|
||||
|
||||
self.server_process = threading.Thread(target=run_server, daemon=True)
|
||||
self.server_process.start()
|
||||
|
||||
# Wait a moment for server to start
|
||||
time.sleep(1)
|
||||
|
||||
def open_browser(self, session_id: str):
|
||||
"""Open browser to the session page"""
|
||||
url = f"http://{self.host}:{self.port}/session/{session_id}"
|
||||
try:
|
||||
webbrowser.open(url)
|
||||
except Exception:
|
||||
print(f"Please open your browser and navigate to: {url}")
|
||||
|
||||
def wait_for_feedback(self, session_id: str, timeout: int = 300) -> dict:
|
||||
"""Wait for user feedback with timeout"""
|
||||
session = self.sessions.get(session_id)
|
||||
if not session:
|
||||
return {"command_logs": "", "interactive_feedback": "Session not found"}
|
||||
|
||||
# Wait for feedback with timeout
|
||||
start_time = time.time()
|
||||
while not session.completed:
|
||||
if time.time() - start_time > timeout:
|
||||
return {"command_logs": "", "interactive_feedback": "Timeout waiting for feedback"}
|
||||
time.sleep(0.1)
|
||||
|
||||
result = {
|
||||
"command_logs": "".join(session.command_logs),
|
||||
"interactive_feedback": session.feedback_result or ""
|
||||
}
|
||||
|
||||
# Clean up session
|
||||
del self.sessions[session_id]
|
||||
|
||||
return result
|
||||
|
||||
# Global instance
|
||||
web_ui_manager = WebUIManager()
|
||||
|
||||
def launch_web_feedback_ui(project_directory: str, summary: str) -> dict:
|
||||
"""Launch web UI and wait for feedback"""
|
||||
|
||||
# Start server if not running
|
||||
web_ui_manager.start_server()
|
||||
|
||||
# Create new session
|
||||
session_id = web_ui_manager.create_session(project_directory, summary)
|
||||
|
||||
# Open browser
|
||||
web_ui_manager.open_browser(session_id)
|
||||
|
||||
# Wait for feedback
|
||||
return web_ui_manager.wait_for_feedback(session_id)
|
Loading…
x
Reference in New Issue
Block a user