ac-tool/frontend/src/components/sheet/CommandBar.tsx
Vadym Samoilenko 72c50b2c92 Initial commit — AC Tool unified application
Merges ac-helper (PHP Activation Calendar) and brief-extractor (Python AI)
into a single Docker app with React/TypeScript frontend.

Features:
- Brief upload → AI extraction → review → Activation Calendar import
- Handsontable v17 spreadsheet with dependent dropdowns (148 categories)
- AI natural language commands via Gemini (YOLO mode, voice input)
- Azure AD MSAL SPA PKCE authentication, user roles (user/admin)
- CSV Activation Calendar export
- Real-time WebSocket job progress
- Admin: user management, dropdown Excel upload
- Multi-stage Dockerfile, docker-compose, nginx proxy instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 13:24:46 +00:00

107 lines
3.5 KiB
TypeScript

import { useState, useRef } from 'react'
import { useSpeechRecognition } from '../../hooks/useSpeechRecognition'
interface Props {
onCommand: (command: string, yolo: boolean) => void
loading: boolean
yolo: boolean
onYoloChange: (val: boolean) => void
}
export default function CommandBar({ onCommand, loading, yolo, onYoloChange }: Props) {
const [input, setInput] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
const { listening, start, stop, supported } = useSpeechRecognition((text) => {
setInput(text)
})
const handleSend = () => {
const cmd = input.trim()
if (!cmd || loading) return
onCommand(cmd, yolo)
setInput('')
}
const quickStarters = [
'Add 5 social media banners for UK',
'Add 3 email newsletters for DE, FR, ES',
'Create 10 OOH Print A4 deliverables',
]
return (
<div className="space-y-2">
<div className="flex gap-2 items-center">
<input
ref={inputRef}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) handleSend() }}
placeholder="Type a command… e.g. 'Add 5 social banners for UK'"
disabled={loading}
className="flex-1 px-3 py-2 rounded text-sm outline-none"
style={{
background: 'var(--bg-card)',
color: 'var(--text-primary)',
border: '1px solid var(--border)',
}}
/>
{supported && (
<button
onMouseDown={start}
onMouseUp={stop}
className="px-3 py-2 rounded text-sm transition-colors"
style={{
background: listening ? 'var(--danger)' : 'var(--bg-card)',
color: listening ? '#fff' : 'var(--text-secondary)',
border: '1px solid var(--border)',
}}
title="Hold to speak"
>
🎤
</button>
)}
<button
onClick={handleSend}
disabled={loading || !input.trim()}
className="px-4 py-2 rounded text-sm font-medium transition-colors disabled:opacity-40"
style={{ background: 'var(--accent)', color: '#000' }}
>
{loading ? '…' : 'Send'}
</button>
<label className="flex items-center gap-1 cursor-pointer select-none" title="YOLO mode — AI never asks questions">
<div
className="relative w-10 h-5 rounded-full transition-colors"
style={{ background: yolo ? 'var(--accent)' : 'var(--border)' }}
onClick={() => onYoloChange(!yolo)}
>
<div
className="absolute top-0.5 w-4 h-4 rounded-full transition-transform"
style={{
background: yolo ? '#000' : 'var(--text-muted)',
transform: yolo ? 'translateX(22px)' : 'translateX(2px)',
}}
/>
</div>
<span className="text-xs" style={{ color: 'var(--text-secondary)' }}>YOLO</span>
</label>
</div>
<div className="flex flex-wrap gap-2">
{quickStarters.map(qs => (
<button
key={qs}
onClick={() => { setInput(qs); inputRef.current?.focus() }}
className="px-2 py-1 rounded text-xs transition-colors hover:opacity-80"
style={{ background: 'var(--bg-card)', color: 'var(--text-secondary)', border: '1px solid var(--border)' }}
>
{qs}
</button>
))}
</div>
</div>
)
}