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>
107 lines
3.5 KiB
TypeScript
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>
|
|
)
|
|
}
|