feat: Add POC command page structure and routing
- Created CommandPocPage.tsx with basic layout structure - Added POC-specific TypeScript interfaces and types - Implemented basic UI components: PocHeader, PocMessageList, PocMessageBubble, PocCommandInput, PocStatusBar - Added /command-poc route to Router.tsx - Set up component folder structure following PRD specifications 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Python CLI Web Interface</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Python CLI Web Interface</h1>
|
||||
<div class="status">
|
||||
<span id="status-indicator" class="status-disconnected">●</span>
|
||||
<span id="status-text">Disconnected</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="chat-container">
|
||||
<div id="output" class="output-area"></div>
|
||||
|
||||
<div class="input-area">
|
||||
<div class="command-input-container">
|
||||
<input type="text" id="command-input" placeholder="Enter Python command (e.g., python script.py)" autocomplete="off">
|
||||
<button id="run-btn">Run</button>
|
||||
<button id="kill-btn" disabled>Kill</button>
|
||||
</div>
|
||||
|
||||
<div class="user-input-container" id="user-input-container" style="display: none;">
|
||||
<input type="text" id="user-input" placeholder="Enter input for Python script..." autocomplete="off">
|
||||
<button id="send-btn">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,221 @@
|
||||
class PythonWebInterface {
|
||||
constructor() {
|
||||
this.ws = null
|
||||
this.currentSessionId = null
|
||||
this.isConnected = false
|
||||
this.isRunning = false
|
||||
|
||||
this.initializeElements()
|
||||
this.setupEventListeners()
|
||||
this.connect()
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
this.statusIndicator = document.getElementById('status-indicator')
|
||||
this.statusText = document.getElementById('status-text')
|
||||
this.output = document.getElementById('output')
|
||||
this.commandInput = document.getElementById('command-input')
|
||||
this.userInput = document.getElementById('user-input')
|
||||
this.userInputContainer = document.getElementById('user-input-container')
|
||||
this.runBtn = document.getElementById('run-btn')
|
||||
this.killBtn = document.getElementById('kill-btn')
|
||||
this.sendBtn = document.getElementById('send-btn')
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.runBtn.addEventListener('click', () => this.runCommand())
|
||||
this.killBtn.addEventListener('click', () => this.killSession())
|
||||
this.sendBtn.addEventListener('click', () => this.sendInput())
|
||||
|
||||
this.commandInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter' && !this.isRunning) {
|
||||
this.runCommand()
|
||||
}
|
||||
})
|
||||
|
||||
this.userInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.sendInput()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
connect() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${window.location.host}`
|
||||
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.isConnected = true
|
||||
this.updateStatus('Connected', true)
|
||||
this.addSystemMessage('Connected to server')
|
||||
}
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.isConnected = false
|
||||
this.updateStatus('Disconnected', false)
|
||||
this.addSystemMessage('Disconnected from server. Attempting to reconnect...')
|
||||
|
||||
// Attempt to reconnect after 3 seconds
|
||||
setTimeout(() => {
|
||||
if (!this.isConnected) {
|
||||
this.connect()
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
this.addErrorMessage('WebSocket error occurred')
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.handleMessage(JSON.parse(event.data))
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(data) {
|
||||
switch (data.type) {
|
||||
case 'command_started':
|
||||
this.isRunning = true
|
||||
this.currentSessionId = data.sessionId
|
||||
this.updateUIState()
|
||||
this.showUserInput()
|
||||
this.addSystemMessage(`Started: ${data.command}`)
|
||||
break
|
||||
|
||||
case 'output':
|
||||
this.addOutput(data.data, data.stream === 'stderr')
|
||||
break
|
||||
|
||||
case 'command_finished':
|
||||
this.isRunning = false
|
||||
this.currentSessionId = null
|
||||
this.updateUIState()
|
||||
this.hideUserInput()
|
||||
this.addSystemMessage(`Process finished with exit code: ${data.exitCode}`)
|
||||
break
|
||||
|
||||
case 'error':
|
||||
this.addErrorMessage(data.message)
|
||||
this.isRunning = false
|
||||
this.currentSessionId = null
|
||||
this.updateUIState()
|
||||
this.hideUserInput()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
runCommand() {
|
||||
const command = this.commandInput.value.trim()
|
||||
if (!command || !this.isConnected || this.isRunning) return
|
||||
|
||||
this.addCommandMessage(command)
|
||||
|
||||
const sessionId = 'session_' + Date.now()
|
||||
this.ws.send(
|
||||
JSON.stringify({
|
||||
type: 'run_command',
|
||||
command: command,
|
||||
sessionId: sessionId
|
||||
})
|
||||
)
|
||||
|
||||
this.commandInput.value = ''
|
||||
}
|
||||
|
||||
sendInput() {
|
||||
const input = this.userInput.value
|
||||
if (!input || !this.currentSessionId) return
|
||||
|
||||
this.addInputMessage(input)
|
||||
|
||||
this.ws.send(
|
||||
JSON.stringify({
|
||||
type: 'send_input',
|
||||
input: input,
|
||||
sessionId: this.currentSessionId
|
||||
})
|
||||
)
|
||||
|
||||
this.userInput.value = ''
|
||||
}
|
||||
|
||||
killSession() {
|
||||
if (this.currentSessionId) {
|
||||
this.ws.send(
|
||||
JSON.stringify({
|
||||
type: 'kill_session',
|
||||
sessionId: this.currentSessionId
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(text, connected) {
|
||||
this.statusText.textContent = text
|
||||
this.statusIndicator.className = connected ? 'status-connected' : 'status-disconnected'
|
||||
}
|
||||
|
||||
updateUIState() {
|
||||
this.runBtn.disabled = !this.isConnected || this.isRunning
|
||||
this.killBtn.disabled = !this.isRunning
|
||||
this.commandInput.disabled = !this.isConnected || this.isRunning
|
||||
}
|
||||
|
||||
showUserInput() {
|
||||
this.userInputContainer.style.display = 'flex'
|
||||
this.userInput.focus()
|
||||
}
|
||||
|
||||
hideUserInput() {
|
||||
this.userInputContainer.style.display = 'none'
|
||||
}
|
||||
|
||||
addMessage(content, className) {
|
||||
const messageDiv = document.createElement('div')
|
||||
messageDiv.className = `message ${className}`
|
||||
|
||||
const timestamp = document.createElement('div')
|
||||
timestamp.className = 'timestamp'
|
||||
timestamp.textContent = new Date().toLocaleTimeString()
|
||||
|
||||
const contentDiv = document.createElement('pre')
|
||||
contentDiv.textContent = content
|
||||
|
||||
messageDiv.appendChild(timestamp)
|
||||
messageDiv.appendChild(contentDiv)
|
||||
|
||||
this.output.appendChild(messageDiv)
|
||||
this.scrollToBottom()
|
||||
}
|
||||
|
||||
addCommandMessage(command) {
|
||||
this.addMessage(`$ ${command}`, 'command')
|
||||
}
|
||||
|
||||
addInputMessage(input) {
|
||||
this.addMessage(`> ${input}`, 'command')
|
||||
}
|
||||
|
||||
addOutput(data, isError = false) {
|
||||
this.addMessage(data, isError ? 'error' : 'output')
|
||||
}
|
||||
|
||||
addSystemMessage(message) {
|
||||
this.addMessage(message, 'system')
|
||||
}
|
||||
|
||||
addErrorMessage(message) {
|
||||
this.addMessage(`Error: ${message}`, 'error')
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.output.scrollTop = this.output.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the interface when the page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new PythonWebInterface()
|
||||
})
|
||||
@@ -0,0 +1,207 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #1e1e1e;
|
||||
color: #ffffff;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #2d2d2d;
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 1px solid #404040;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.output-area {
|
||||
flex: 1;
|
||||
padding: 1rem 2rem;
|
||||
overflow-y: auto;
|
||||
background-color: #1e1e1e;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message.command {
|
||||
background-color: #0d7377;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message.output {
|
||||
background-color: #2d2d2d;
|
||||
border-left: 4px solid #4caf50;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background-color: #2d2d2d;
|
||||
border-left: 4px solid #f44336;
|
||||
}
|
||||
|
||||
.message.system {
|
||||
background-color: #2d2d2d;
|
||||
border-left: 4px solid #ff9800;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.message pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
background-color: #2d2d2d;
|
||||
padding: 1rem 2rem;
|
||||
border-top: 1px solid #404040;
|
||||
}
|
||||
|
||||
.command-input-container,
|
||||
.user-input-container {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-input-container {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 4px;
|
||||
background-color: #1e1e1e;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #0d7377;
|
||||
box-shadow: 0 0 0 2px rgba(13, 115, 119, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
#run-btn {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#run-btn:hover:not(:disabled) {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
#run-btn:disabled {
|
||||
background-color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#kill-btn {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#kill-btn:hover:not(:disabled) {
|
||||
background-color: #da190b;
|
||||
}
|
||||
|
||||
#kill-btn:disabled {
|
||||
background-color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#send-btn {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#send-btn:hover:not(:disabled) {
|
||||
background-color: #1976d2;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Scrollbar styling for webkit browsers */
|
||||
.output-area::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.output-area::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.output-area::-webkit-scrollbar-thumb {
|
||||
background: #404040;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.output-area::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
Reference in New Issue
Block a user