mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 14:22:04 +00:00
hotkeys
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { useProjects, useFeatures } from './hooks/useProjects'
|
import { useProjects, useFeatures } from './hooks/useProjects'
|
||||||
import { useProjectWebSocket } from './hooks/useWebSocket'
|
import { useProjectWebSocket } from './hooks/useWebSocket'
|
||||||
|
import { useFeatureSound } from './hooks/useFeatureSound'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'autonomous-coder-selected-project'
|
||||||
import { ProjectSelector } from './components/ProjectSelector'
|
import { ProjectSelector } from './components/ProjectSelector'
|
||||||
import { KanbanBoard } from './components/KanbanBoard'
|
import { KanbanBoard } from './components/KanbanBoard'
|
||||||
import { AgentControl } from './components/AgentControl'
|
import { AgentControl } from './components/AgentControl'
|
||||||
@@ -13,7 +16,14 @@ import { Plus, Loader2 } from 'lucide-react'
|
|||||||
import type { Feature } from './lib/types'
|
import type { Feature } from './lib/types'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [selectedProject, setSelectedProject] = useState<string | null>(null)
|
// Initialize selected project from localStorage
|
||||||
|
const [selectedProject, setSelectedProject] = useState<string | null>(() => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(STORAGE_KEY)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
const [showAddFeature, setShowAddFeature] = useState(false)
|
const [showAddFeature, setShowAddFeature] = useState(false)
|
||||||
const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
|
const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
|
||||||
const [setupComplete, setSetupComplete] = useState(true) // Start optimistic
|
const [setupComplete, setSetupComplete] = useState(true) // Start optimistic
|
||||||
@@ -23,6 +33,66 @@ function App() {
|
|||||||
const { data: features } = useFeatures(selectedProject)
|
const { data: features } = useFeatures(selectedProject)
|
||||||
const wsState = useProjectWebSocket(selectedProject)
|
const wsState = useProjectWebSocket(selectedProject)
|
||||||
|
|
||||||
|
// Play sounds when features move between columns
|
||||||
|
useFeatureSound(features)
|
||||||
|
|
||||||
|
// Persist selected project to localStorage
|
||||||
|
const handleSelectProject = useCallback((project: string | null) => {
|
||||||
|
setSelectedProject(project)
|
||||||
|
try {
|
||||||
|
if (project) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, project)
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(STORAGE_KEY)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// localStorage not available
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Validate stored project exists (clear if project was deleted)
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) {
|
||||||
|
handleSelectProject(null)
|
||||||
|
}
|
||||||
|
}, [selectedProject, projects, handleSelectProject])
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
// Ignore if user is typing in an input
|
||||||
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// D : Toggle debug window
|
||||||
|
if (e.key === 'd' || e.key === 'D') {
|
||||||
|
e.preventDefault()
|
||||||
|
setDebugOpen(prev => !prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
// N : Add new feature (when project selected)
|
||||||
|
if ((e.key === 'n' || e.key === 'N') && selectedProject) {
|
||||||
|
e.preventDefault()
|
||||||
|
setShowAddFeature(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape : Close modals
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
if (showAddFeature) {
|
||||||
|
setShowAddFeature(false)
|
||||||
|
} else if (selectedFeature) {
|
||||||
|
setSelectedFeature(null)
|
||||||
|
} else if (debugOpen) {
|
||||||
|
setDebugOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [selectedProject, showAddFeature, selectedFeature, debugOpen])
|
||||||
|
|
||||||
// Combine WebSocket progress with feature data
|
// Combine WebSocket progress with feature data
|
||||||
const progress = wsState.progress.total > 0 ? wsState.progress : {
|
const progress = wsState.progress.total > 0 ? wsState.progress : {
|
||||||
passing: features?.done.length ?? 0,
|
passing: features?.done.length ?? 0,
|
||||||
@@ -54,7 +124,7 @@ function App() {
|
|||||||
<ProjectSelector
|
<ProjectSelector
|
||||||
projects={projects ?? []}
|
projects={projects ?? []}
|
||||||
selectedProject={selectedProject}
|
selectedProject={selectedProject}
|
||||||
onSelectProject={setSelectedProject}
|
onSelectProject={handleSelectProject}
|
||||||
isLoading={projectsLoading}
|
isLoading={projectsLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -63,9 +133,13 @@ function App() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setShowAddFeature(true)}
|
onClick={() => setShowAddFeature(true)}
|
||||||
className="neo-btn neo-btn-primary text-sm"
|
className="neo-btn neo-btn-primary text-sm"
|
||||||
|
title="Press N"
|
||||||
>
|
>
|
||||||
<Plus size={18} />
|
<Plus size={18} />
|
||||||
Add Feature
|
Add Feature
|
||||||
|
<kbd className="ml-1.5 px-1.5 py-0.5 text-xs bg-black/20 rounded font-mono">
|
||||||
|
N
|
||||||
|
</kbd>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<AgentControl
|
<AgentControl
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ export function DebugLogViewer({
|
|||||||
<span className="font-mono text-sm text-white font-bold">
|
<span className="font-mono text-sm text-white font-bold">
|
||||||
Debug
|
Debug
|
||||||
</span>
|
</span>
|
||||||
|
<span className="px-1.5 py-0.5 text-xs font-mono bg-[#333] text-gray-500 rounded" title="Toggle debug panel">
|
||||||
|
D
|
||||||
|
</span>
|
||||||
{logs.length > 0 && (
|
{logs.length > 0 && (
|
||||||
<span className="px-2 py-0.5 text-xs font-mono bg-[#333] text-gray-300 rounded">
|
<span className="px-2 py-0.5 text-xs font-mono bg-[#333] text-gray-300 rounded">
|
||||||
{logs.length}
|
{logs.length}
|
||||||
|
|||||||
109
ui/src/hooks/useFeatureSound.ts
Normal file
109
ui/src/hooks/useFeatureSound.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* Hook for playing sounds when features move between columns
|
||||||
|
* Uses Web Audio API to generate pleasant chime sounds
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import type { FeatureListResponse } from '../lib/types'
|
||||||
|
|
||||||
|
// Sound frequencies for different transitions (in Hz)
|
||||||
|
const SOUNDS = {
|
||||||
|
// Feature started (pending -> in_progress): ascending tone
|
||||||
|
started: [523.25, 659.25], // C5 -> E5
|
||||||
|
// Feature completed (in_progress -> done): pleasant major chord arpeggio
|
||||||
|
completed: [523.25, 659.25, 783.99], // C5 -> E5 -> G5
|
||||||
|
}
|
||||||
|
|
||||||
|
type SoundType = keyof typeof SOUNDS
|
||||||
|
|
||||||
|
function playChime(type: SoundType): void {
|
||||||
|
try {
|
||||||
|
const audioContext = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)()
|
||||||
|
const frequencies = SOUNDS[type]
|
||||||
|
const duration = type === 'completed' ? 0.15 : 0.12
|
||||||
|
const totalDuration = frequencies.length * duration
|
||||||
|
|
||||||
|
frequencies.forEach((freq, index) => {
|
||||||
|
const oscillator = audioContext.createOscillator()
|
||||||
|
const gainNode = audioContext.createGain()
|
||||||
|
|
||||||
|
oscillator.connect(gainNode)
|
||||||
|
gainNode.connect(audioContext.destination)
|
||||||
|
|
||||||
|
oscillator.type = 'sine'
|
||||||
|
oscillator.frequency.setValueAtTime(freq, audioContext.currentTime)
|
||||||
|
|
||||||
|
// Envelope for smooth sound
|
||||||
|
const startTime = audioContext.currentTime + index * duration
|
||||||
|
gainNode.gain.setValueAtTime(0, startTime)
|
||||||
|
gainNode.gain.linearRampToValueAtTime(0.3, startTime + 0.02)
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, startTime + duration)
|
||||||
|
|
||||||
|
oscillator.start(startTime)
|
||||||
|
oscillator.stop(startTime + duration)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clean up audio context after sounds finish
|
||||||
|
setTimeout(() => {
|
||||||
|
audioContext.close()
|
||||||
|
}, totalDuration * 1000 + 100)
|
||||||
|
} catch {
|
||||||
|
// Audio not supported or blocked, fail silently
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FeatureState {
|
||||||
|
pendingIds: Set<number>
|
||||||
|
inProgressIds: Set<number>
|
||||||
|
doneIds: Set<number>
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFeatureState(features: FeatureListResponse | undefined): FeatureState {
|
||||||
|
return {
|
||||||
|
pendingIds: new Set(features?.pending.map(f => f.id) ?? []),
|
||||||
|
inProgressIds: new Set(features?.in_progress.map(f => f.id) ?? []),
|
||||||
|
doneIds: new Set(features?.done.map(f => f.id) ?? []),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFeatureSound(features: FeatureListResponse | undefined): void {
|
||||||
|
const prevStateRef = useRef<FeatureState | null>(null)
|
||||||
|
const isInitializedRef = useRef(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!features) return
|
||||||
|
|
||||||
|
const currentState = getFeatureState(features)
|
||||||
|
|
||||||
|
// Skip sound on initial load
|
||||||
|
if (!isInitializedRef.current) {
|
||||||
|
prevStateRef.current = currentState
|
||||||
|
isInitializedRef.current = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevState = prevStateRef.current
|
||||||
|
if (!prevState) {
|
||||||
|
prevStateRef.current = currentState
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for features that moved to in_progress (started)
|
||||||
|
for (const id of currentState.inProgressIds) {
|
||||||
|
if (prevState.pendingIds.has(id) && !prevState.inProgressIds.has(id)) {
|
||||||
|
playChime('started')
|
||||||
|
break // Only play once even if multiple features moved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for features that moved to done (completed)
|
||||||
|
for (const id of currentState.doneIds) {
|
||||||
|
if (!prevState.doneIds.has(id) && (prevState.inProgressIds.has(id) || prevState.pendingIds.has(id))) {
|
||||||
|
playChime('completed')
|
||||||
|
break // Only play once even if multiple features moved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prevStateRef.current = currentState
|
||||||
|
}, [features])
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/addfeatureform.tsx","./src/components/agentcontrol.tsx","./src/components/chatmessage.tsx","./src/components/debuglogviewer.tsx","./src/components/featurecard.tsx","./src/components/featuremodal.tsx","./src/components/kanbanboard.tsx","./src/components/kanbancolumn.tsx","./src/components/newprojectmodal.tsx","./src/components/progressdashboard.tsx","./src/components/projectselector.tsx","./src/components/questionoptions.tsx","./src/components/setupwizard.tsx","./src/components/speccreationchat.tsx","./src/components/typingindicator.tsx","./src/hooks/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"}
|
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/addfeatureform.tsx","./src/components/agentcontrol.tsx","./src/components/chatmessage.tsx","./src/components/debuglogviewer.tsx","./src/components/featurecard.tsx","./src/components/featuremodal.tsx","./src/components/kanbanboard.tsx","./src/components/kanbancolumn.tsx","./src/components/newprojectmodal.tsx","./src/components/progressdashboard.tsx","./src/components/projectselector.tsx","./src/components/questionoptions.tsx","./src/components/setupwizard.tsx","./src/components/speccreationchat.tsx","./src/components/typingindicator.tsx","./src/hooks/usefeaturesound.ts","./src/hooks/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"}
|
||||||
Reference in New Issue
Block a user