mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-31 14:43:35 +00:00
celebrate
This commit is contained in:
19
ui/package-lock.json
generated
19
ui/package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
"@tanstack/react-query": "^5.60.0",
|
"@tanstack/react-query": "^5.60.0",
|
||||||
|
"canvas-confetti": "^1.9.4",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.13.0",
|
"@eslint/js": "^9.13.0",
|
||||||
"@tailwindcss/vite": "^4.0.0-beta.4",
|
"@tailwindcss/vite": "^4.0.0-beta.4",
|
||||||
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
@@ -2287,6 +2289,13 @@
|
|||||||
"@babel/types": "^7.28.2"
|
"@babel/types": "^7.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/canvas-confetti": {
|
||||||
|
"version": "1.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz",
|
||||||
|
"integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -2787,6 +2796,16 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/canvas-confetti": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"funding": {
|
||||||
|
"type": "donate",
|
||||||
|
"url": "https://www.paypal.me/kirilvatev"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
"@tanstack/react-query": "^5.60.0",
|
"@tanstack/react-query": "^5.60.0",
|
||||||
|
"canvas-confetti": "^1.9.4",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.13.0",
|
"@eslint/js": "^9.13.0",
|
||||||
"@tailwindcss/vite": "^4.0.0-beta.4",
|
"@tailwindcss/vite": "^4.0.0-beta.4",
|
||||||
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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'
|
import { useFeatureSound } from './hooks/useFeatureSound'
|
||||||
|
import { useCelebration } from './hooks/useCelebration'
|
||||||
|
|
||||||
const STORAGE_KEY = 'autonomous-coder-selected-project'
|
const STORAGE_KEY = 'autonomous-coder-selected-project'
|
||||||
import { ProjectSelector } from './components/ProjectSelector'
|
import { ProjectSelector } from './components/ProjectSelector'
|
||||||
@@ -37,6 +38,9 @@ function App() {
|
|||||||
// Play sounds when features move between columns
|
// Play sounds when features move between columns
|
||||||
useFeatureSound(features)
|
useFeatureSound(features)
|
||||||
|
|
||||||
|
// Celebrate when all features are complete
|
||||||
|
useCelebration(features, selectedProject)
|
||||||
|
|
||||||
// Persist selected project to localStorage
|
// Persist selected project to localStorage
|
||||||
const handleSelectProject = useCallback((project: string | null) => {
|
const handleSelectProject = useCallback((project: string | null) => {
|
||||||
setSelectedProject(project)
|
setSelectedProject(project)
|
||||||
|
|||||||
184
ui/src/hooks/useCelebration.ts
Normal file
184
ui/src/hooks/useCelebration.ts
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* Hook for triggering celebration effects when all features are complete
|
||||||
|
* Plays confetti cannons from both sides and a triumphant fanfare
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import confetti from 'canvas-confetti'
|
||||||
|
import type { FeatureListResponse } from '../lib/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a triumphant fanfare using Web Audio API
|
||||||
|
* Rising major chord progression: C5 -> E5 -> G5 -> C6
|
||||||
|
*/
|
||||||
|
function playFanfare(): void {
|
||||||
|
try {
|
||||||
|
const audioContext = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)()
|
||||||
|
|
||||||
|
// Frequencies for triumphant fanfare (C major arpeggio going up)
|
||||||
|
const notes = [
|
||||||
|
{ freq: 523.25, start: 0 }, // C5
|
||||||
|
{ freq: 659.25, start: 0.15 }, // E5
|
||||||
|
{ freq: 783.99, start: 0.30 }, // G5
|
||||||
|
{ freq: 1046.50, start: 0.45 }, // C6 (octave higher)
|
||||||
|
]
|
||||||
|
|
||||||
|
const noteDuration = 0.25
|
||||||
|
|
||||||
|
notes.forEach(({ freq, start }) => {
|
||||||
|
const oscillator = audioContext.createOscillator()
|
||||||
|
const gainNode = audioContext.createGain()
|
||||||
|
|
||||||
|
oscillator.connect(gainNode)
|
||||||
|
gainNode.connect(audioContext.destination)
|
||||||
|
|
||||||
|
oscillator.type = 'sine'
|
||||||
|
oscillator.frequency.setValueAtTime(freq, audioContext.currentTime + start)
|
||||||
|
|
||||||
|
// Envelope for smooth, triumphant sound
|
||||||
|
const startTime = audioContext.currentTime + start
|
||||||
|
gainNode.gain.setValueAtTime(0, startTime)
|
||||||
|
gainNode.gain.linearRampToValueAtTime(0.4, startTime + 0.03)
|
||||||
|
gainNode.gain.setValueAtTime(0.4, startTime + noteDuration * 0.6)
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, startTime + noteDuration)
|
||||||
|
|
||||||
|
oscillator.start(startTime)
|
||||||
|
oscillator.stop(startTime + noteDuration)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add a final sustained chord for extra triumph
|
||||||
|
const chordFreqs = [523.25, 659.25, 783.99, 1046.50] // C major chord
|
||||||
|
const chordStart = 0.65
|
||||||
|
const chordDuration = 0.5
|
||||||
|
|
||||||
|
chordFreqs.forEach((freq) => {
|
||||||
|
const oscillator = audioContext.createOscillator()
|
||||||
|
const gainNode = audioContext.createGain()
|
||||||
|
|
||||||
|
oscillator.connect(gainNode)
|
||||||
|
gainNode.connect(audioContext.destination)
|
||||||
|
|
||||||
|
oscillator.type = 'sine'
|
||||||
|
oscillator.frequency.setValueAtTime(freq, audioContext.currentTime + chordStart)
|
||||||
|
|
||||||
|
const startTime = audioContext.currentTime + chordStart
|
||||||
|
gainNode.gain.setValueAtTime(0, startTime)
|
||||||
|
gainNode.gain.linearRampToValueAtTime(0.2, startTime + 0.05)
|
||||||
|
gainNode.gain.setValueAtTime(0.2, startTime + chordDuration * 0.5)
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, startTime + chordDuration)
|
||||||
|
|
||||||
|
oscillator.start(startTime)
|
||||||
|
oscillator.stop(startTime + chordDuration)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clean up audio context after sounds finish
|
||||||
|
setTimeout(() => {
|
||||||
|
audioContext.close()
|
||||||
|
}, 1500)
|
||||||
|
} catch {
|
||||||
|
// Audio not supported or blocked, fail silently
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire confetti cannons from both sides of the screen
|
||||||
|
*/
|
||||||
|
function fireConfetti(): void {
|
||||||
|
const duration = 2000
|
||||||
|
const end = Date.now() + duration
|
||||||
|
|
||||||
|
// Initial burst from both sides
|
||||||
|
confetti({
|
||||||
|
particleCount: 100,
|
||||||
|
spread: 70,
|
||||||
|
origin: { x: 0, y: 0.6 },
|
||||||
|
angle: 60,
|
||||||
|
colors: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dfe6e9']
|
||||||
|
})
|
||||||
|
|
||||||
|
confetti({
|
||||||
|
particleCount: 100,
|
||||||
|
spread: 70,
|
||||||
|
origin: { x: 1, y: 0.6 },
|
||||||
|
angle: 120,
|
||||||
|
colors: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dfe6e9']
|
||||||
|
})
|
||||||
|
|
||||||
|
// Continue firing for a bit
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (Date.now() > end) {
|
||||||
|
clearInterval(interval)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
confetti({
|
||||||
|
particleCount: 30,
|
||||||
|
spread: 60,
|
||||||
|
origin: { x: 0, y: 0.6 },
|
||||||
|
angle: 60,
|
||||||
|
colors: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dfe6e9']
|
||||||
|
})
|
||||||
|
|
||||||
|
confetti({
|
||||||
|
particleCount: 30,
|
||||||
|
spread: 60,
|
||||||
|
origin: { x: 1, y: 0.6 },
|
||||||
|
angle: 120,
|
||||||
|
colors: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dfe6e9']
|
||||||
|
})
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if all features are complete (none pending or in progress, at least one done)
|
||||||
|
*/
|
||||||
|
function isAllComplete(features: FeatureListResponse | undefined): boolean {
|
||||||
|
if (!features) return false
|
||||||
|
return (
|
||||||
|
features.pending.length === 0 &&
|
||||||
|
features.in_progress.length === 0 &&
|
||||||
|
features.done.length > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that triggers celebration when all features are complete
|
||||||
|
* Tracks per-project to allow re-celebration when switching between completed projects
|
||||||
|
*/
|
||||||
|
export function useCelebration(
|
||||||
|
features: FeatureListResponse | undefined,
|
||||||
|
projectName: string | null
|
||||||
|
): void {
|
||||||
|
// Track which projects have celebrated in this session
|
||||||
|
const celebratedProjectsRef = useRef<Set<string>>(new Set())
|
||||||
|
// Track if we've initialized for the current project (to avoid celebrating on initial load)
|
||||||
|
const initializedForProjectRef = useRef<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!features || !projectName) return
|
||||||
|
|
||||||
|
const isComplete = isAllComplete(features)
|
||||||
|
|
||||||
|
// If this is a new project, mark as initialized but don't celebrate yet
|
||||||
|
// This prevents celebrating when first loading an already-complete project
|
||||||
|
if (initializedForProjectRef.current !== projectName) {
|
||||||
|
initializedForProjectRef.current = projectName
|
||||||
|
// If project is already complete on first load, mark it as celebrated
|
||||||
|
// so we don't trigger when data refreshes
|
||||||
|
if (isComplete) {
|
||||||
|
celebratedProjectsRef.current.add(projectName)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should celebrate
|
||||||
|
if (isComplete && !celebratedProjectsRef.current.has(projectName)) {
|
||||||
|
// Mark as celebrated before firing to prevent double-triggers
|
||||||
|
celebratedProjectsRef.current.add(projectName)
|
||||||
|
|
||||||
|
// Fire the celebration!
|
||||||
|
fireConfetti()
|
||||||
|
playFanfare()
|
||||||
|
}
|
||||||
|
}, [features, projectName])
|
||||||
|
}
|
||||||
@@ -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/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"}
|
{"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/usecelebration.ts","./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