9fe14311fc
- Integrate complete Cherry Studio CSS variables and design patterns - Implement proper light/dark theme support with system preference detection - Enhance message bubbles to match Cherry Studio's chat interface styling - Add Cherry Studio-compatible scrollbar styling with theme-aware colors - Improve typography with Ubuntu font family and proper font stacks - Add comprehensive hover states, transitions, and micro-interactions - Implement accessibility improvements including focus states and reduced motion - Add theme toggle functionality with persistent preferences - Enhance header styling to match Cherry Studio's navbar design - Add animation effects consistent with Cherry Studio's motion design - Improve responsive design for mobile and tablet viewports - Add high contrast mode support for better accessibility The POC interface now provides a polished, professional appearance that seamlessly integrates with Cherry Studio's design language and user experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
282 lines
7.5 KiB
JavaScript
282 lines
7.5 KiB
JavaScript
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')
|
|
this.themeToggle = document.getElementById('theme-toggle')
|
|
|
|
// Initialize theme
|
|
this.initializeTheme()
|
|
}
|
|
|
|
setupEventListeners() {
|
|
this.runBtn.addEventListener('click', () => this.runCommand())
|
|
this.killBtn.addEventListener('click', () => this.killSession())
|
|
this.sendBtn.addEventListener('click', () => this.sendInput())
|
|
this.themeToggle.addEventListener('click', () => this.toggleTheme())
|
|
|
|
this.commandInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter' && !this.isRunning) {
|
|
this.runCommand()
|
|
}
|
|
})
|
|
|
|
this.userInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
this.sendInput()
|
|
}
|
|
})
|
|
|
|
// Listen for system theme changes
|
|
if (window.matchMedia) {
|
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
mediaQuery.addEventListener('change', () => {
|
|
if (!localStorage.getItem('theme-preference')) {
|
|
this.updateTheme(mediaQuery.matches ? 'dark' : 'light')
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Theme Management - Cherry Studio Style
|
|
initializeTheme() {
|
|
const savedTheme = localStorage.getItem('theme-preference')
|
|
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
|
|
let theme
|
|
if (savedTheme) {
|
|
theme = savedTheme
|
|
} else {
|
|
theme = systemPrefersDark ? 'dark' : 'light'
|
|
}
|
|
|
|
this.updateTheme(theme)
|
|
}
|
|
|
|
toggleTheme() {
|
|
const currentTheme = document.documentElement.getAttribute('theme-mode') || 'dark'
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'
|
|
|
|
localStorage.setItem('theme-preference', newTheme)
|
|
this.updateTheme(newTheme)
|
|
|
|
// Add feedback animation
|
|
this.themeToggle.style.transform = 'scale(0.9)'
|
|
setTimeout(() => {
|
|
this.themeToggle.style.transform = 'scale(1)'
|
|
}, 150)
|
|
}
|
|
|
|
updateTheme(theme) {
|
|
document.documentElement.setAttribute('theme-mode', theme)
|
|
|
|
// Update theme toggle icon
|
|
if (this.themeToggle) {
|
|
this.themeToggle.textContent = theme === 'dark' ? '☀️' : '🌙'
|
|
this.themeToggle.setAttribute('aria-label', `Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`)
|
|
this.themeToggle.title = `Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`
|
|
}
|
|
|
|
// Detect OS for header styling
|
|
if (navigator.platform.includes('Mac')) {
|
|
document.body.setAttribute('os', 'darwin')
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize the interface when the page loads
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
new PythonWebInterface()
|
|
})
|