diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml new file mode 100644 index 0000000..00b8b84 --- /dev/null +++ b/.github/workflows/windows-release.yml @@ -0,0 +1,56 @@ +name: Windows EXE + +on: + push: + branches: + - main + - master + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-windows: + name: Build Windows installer + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build app + run: npm run build + + - name: Build Windows EXE + run: npm run dist + + - name: Upload Windows artifact + uses: actions/upload-artifact@v4 + with: + name: EveFlow-Windows-${{ github.ref_name }} + path: | + out/*.exe + out/*.exe.blockmap + out/latest.yml + if-no-files-found: error + + - name: Publish GitHub release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: | + out/*.exe + out/*.exe.blockmap + out/latest.yml diff --git a/.gitignore b/.gitignore index 71da7d5..b9efa84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ dist/ +out/ .env .env.local *.log diff --git a/main.js b/main.js index 7e31e05..ba38660 100644 --- a/main.js +++ b/main.js @@ -199,10 +199,10 @@ async function loadApp() { function createWindow() { mainWindow = new BrowserWindow({ - width: 1000, - height: 700, - minWidth: 400, - minHeight: 500, + width: 1120, + height: 860, + minWidth: 900, + minHeight: 680, frame: false, // Supprime la barre de titre classique pour un style rétro-futuriste personnalisé transparent: true, // Permet la transparence (fond de fenêtre transparent) backgroundColor: '#00000000', // Fond transparent @@ -294,14 +294,38 @@ ipcMain.on('set-window-mode', (event, mode) => { } else { // Mode normal : chat complet mainWindow.setAlwaysOnTop(false); - mainWindow.setMinimumSize(400, 500); - mainWindow.setSize(1000, 700); + mainWindow.setMinimumSize(900, 680); + mainWindow.setSize(1120, 860); mainWindow.center(); event.reply('window-mode-changed', 'normal'); } }); +// IPC Handler pour ouvrir/fermer le cockpit sans ecraser Eve. +ipcMain.on('set-cockpit-mode', (_event, isOpen) => { + if (!mainWindow) return; + if (mainWindow.isMaximized()) return; + + const { screen } = require('electron'); + const display = screen.getDisplayMatching(mainWindow.getBounds()); + const workArea = display.workArea; + const bounds = mainWindow.getBounds(); + const targetWidth = isOpen ? 1460 : 1120; + const nextWidth = Math.min(targetWidth, Math.max(900, workArea.width - 40)); + const nextHeight = Math.min(Math.max(bounds.height, 820), workArea.height - 40); + const nextX = Math.min(bounds.x, workArea.x + workArea.width - nextWidth - 20); + const nextY = Math.min(bounds.y, workArea.y + workArea.height - nextHeight - 20); + + mainWindow.setMinimumSize(isOpen ? 1180 : 900, 680); + mainWindow.setBounds({ + x: Math.max(workArea.x + 20, nextX), + y: Math.max(workArea.y + 20, nextY), + width: nextWidth, + height: nextHeight + }, true); +}); + // ── IPC Handler : écriture des logs du renderer vers disque ───────────────── ipcMain.on('write-log', (event, entry) => { writeLogEntry(entry); diff --git a/package-lock.json b/package-lock.json index e0cf5a1..8289b3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "eveflow", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "eveflow", - "version": "1.0.2", + "version": "1.0.3", "dependencies": { "@react-three/drei": "^9.105.0", "@react-three/fiber": "^8.16.0", diff --git a/package.json b/package.json index f8069a8..25bf9d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eveflow", - "version": "1.0.2", + "version": "1.0.3", "description": "Retro-futuristic Windows companion with 3D Eve bot, local TTS/STT, and multi-agent connectivity", "main": "main.js", "scripts": { diff --git a/preload.js b/preload.js index d855c33..95101ff 100644 --- a/preload.js +++ b/preload.js @@ -4,6 +4,7 @@ const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electronAPI', { windowControl: (action) => ipcRenderer.send('window-control', action), setWindowMode: (mode) => ipcRenderer.send('set-window-mode', mode), + setCockpitMode: (isOpen) => ipcRenderer.send('set-cockpit-mode', isOpen), onWindowModeChanged: (callback) => { const subscription = (event, mode) => callback(mode); ipcRenderer.on('window-mode-changed', subscription); diff --git a/public/avatars/eve-round.png b/public/avatars/eve-round.png new file mode 100644 index 0000000..6a2d664 Binary files /dev/null and b/public/avatars/eve-round.png differ diff --git a/src/App.tsx b/src/App.tsx index 16569f6..49b1b4b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,9 +3,10 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Send, Mic, MicOff, Settings, MessageSquare, Volume2, VolumeX, - Sparkles, Cpu, Minimize2, X, AlertCircle, StopCircle, Paperclip, + Cpu, Minimize2, X, AlertCircle, StopCircle, Paperclip, CalendarClock, History, Wrench, Play, Pause, RefreshCw, Trash2, Plus, - CheckCircle2, WifiOff, Edit3, Save + CheckCircle2, WifiOff, Edit3, Save, ChevronUp, ExternalLink, + BriefcaseBusiness, AlertTriangle } from 'lucide-react'; import { ThreeCanvas, ToolEvent } from './components/ThreeCanvas'; import { AudioService } from './services/audioService'; @@ -13,7 +14,6 @@ import { AgentService, AgentConfig, AgentProvider } from './services/agentServic import { EmotionService, EveAvatar, EveEmotion } from './services/emotionService'; import { HermesJob, HermesJobDraft, PollingService } from './services/pollingService'; import { AppCallbacks } from './services/toolRegistry'; -import { version } from '../package.json'; interface Message { id: string; @@ -585,6 +585,7 @@ const HermesCockpitWindow: React.FC<{ onRun: (job: HermesJob) => void; onDelete: (job: HermesJob) => void; onOpenRun: (run: HermesRunRecord) => void; + onClose: () => void; }> = ({ events, isWorking, @@ -606,7 +607,8 @@ const HermesCockpitWindow: React.FC<{ onResume, onRun, onDelete, - onOpenRun + onOpenRun, + onClose }) => { const latestEvents = events.slice(-8).reverse(); const [activeTab, setActiveTab] = useState('tools'); @@ -726,9 +728,14 @@ const HermesCockpitWindow: React.FC<{
COCKPIT HERMES - +
+ + +
{syncState === 'offline' ? : } @@ -863,11 +870,12 @@ export const App: React.FC = () => { const [currentEmotion, setCurrentEmotion] = useState('neutral'); const [isSpeaking, setIsSpeaking] = useState(false); const [isListening, setIsListening] = useState(false); - const [isMuted, setIsMuted] = useState(false); + const [isMuted] = useState(false); // Fenêtre & Navigation const [isFloatingMode, setIsFloatingMode] = useState(false); const [showSettings, setShowSettings] = useState(false); + const [showHermesCockpit, setShowHermesCockpit] = useState(false); // Paramètres d'Agents et de Voix (sauvegardés localement) const [agentConfig, setAgentConfig] = useState({ @@ -886,6 +894,12 @@ export const App: React.FC = () => { const [sttLang, setSttLang] = useState('fr-FR'); const [ttsProvider, setTtsProvider] = useState<'system' | 'google-free'>('google-free'); const activeAvatar = AVATAR_PROFILES[avatarId]; + const hermesUnreadRuns = hermesRunHistory.filter(run => !run.read).length; + const hermesSyncLabel = hermesSyncState === 'offline' + ? 'Hermes offline' + : hermesSyncState === 'syncing' + ? 'Hermes sync' + : 'Hermes OK'; const pushToolEvent = (event: Omit) => { setToolEvents(prev => [ ...prev.slice(-11), @@ -1296,6 +1310,17 @@ export const App: React.FC = () => { } }; + const toggleHermesCockpit = () => { + const nextOpen = !showHermesCockpit; + setShowHermesCockpit(nextOpen); + window.electronAPI?.setCockpitMode?.(nextOpen); + }; + + const closeHermesCockpit = () => { + setShowHermesCockpit(false); + window.electronAPI?.setCockpitMode?.(false); + }; + const handleSendMessage = async (text: string) => { if (!text.trim() && attachments.length === 0) return; if (isSending) return; @@ -1540,6 +1565,8 @@ export const App: React.FC = () => { + + const handlePlayLongMessage = (messageId: string, content: string) => { if (!audioServiceRef.current) return; @@ -1567,8 +1594,8 @@ export const App: React.FC = () => { setIsSpeaking(false); setCurrentEmotion('neutral'); }) - .catch((err) => { - console.error("[TTS] Échec de la lecture audio complète :", err); + .catch(err => { + console.error("Long message audio speak error:", err); setCurrentlyPlayingMsgId(null); setIsSpeaking(false); setCurrentEmotion('neutral'); @@ -1576,84 +1603,55 @@ export const App: React.FC = () => { } }; - - return (
- {/* 1. APP HEADER */} {!isFloatingMode && (
-
-
-

- EVEFLOW - // ANALOG SPACE OS - SYS_READY_V{version} -

+
+ + EVEFLOW
-
- {/* Statut Agent */} -
- - [ RECEIVER: {agentConfig.provider} ] + {agentConfig.provider === 'hermes' && ( +
+ + {hermesSyncState === 'offline' ? 'Hermes OFFLINE' : 'Hermes OK'}
+ )} - {/* Sourdine */} - - - {/* Paramètres */} - - - {/* Mode Widget Flottant */} - - - {/* Electron Controls */} {window.electronAPI && ( -
- - -
+ )}
@@ -1661,130 +1659,165 @@ export const App: React.FC = () => { {/* 2. MAIN LAYOUT CONTAINER */}
- {/* CHAT SECTION (De gauche) */} {!isFloatingMode && !showSettings && (
-
- {messages.map((msg) => ( -
+
+ {hermesSyncState === 'offline' ? : } + {hermesSyncLabel} +
+ | +
+ + {hermesJobs.length} job{hermesJobs.length > 1 ? 's' : ''} +
+ | +
0 ? 'red' : 'blue'}`}> + + {hermesUnreadRuns} alerte{hermesUnreadRuns > 1 ? 's' : ''} +
+ +
+ )} +
+ {messages.map((msg) => { + const isUser = msg.role === 'user'; + + const markdownAndMedia = ( +
+ ( + + ), + a: ({ node, ...props }) => { + const isLocal = props.href?.startsWith('file://') || /^[a-zA-Z]:/.test(props.href || ''); + if (isLocal) { + return ( + { + e.preventDefault(); + let cleanPath = props.href || ''; + if (cleanPath.startsWith('file:///')) { + cleanPath = cleanPath.substring(8); + } else if (cleanPath.startsWith('file://')) { + cleanPath = cleanPath.substring(7); + } + cleanPath = cleanPath.replace(/\//g, '\\'); + (window as any).electronAPI?.openLocalFile?.(cleanPath) + .catch((err: any) => console.error("Impossible d'ouvrir le fichier :", err)); + }} + className="markdown-link-local" + style={{ color: 'var(--text-neon)', textDecoration: 'underline', fontWeight: 'bold' }} + title={`Ouvrir le fichier local : ${props.href}`} + > + {props.children} + + ); + } + return {props.children}; + } + }} + > + {preprocessMessageContent(msg.content)} + + + {msg.images && msg.images.length > 0 && ( +
+ {msg.images.map((imgUrl, i) => ( + + ))}
)} -
- ( - - ), - a: ({ node, ...props }) => { - const isLocal = props.href?.startsWith('file://') || /^[a-zA-Z]:/.test(props.href || ''); - if (isLocal) { - return ( - { - e.preventDefault(); - let cleanPath = props.href || ''; - if (cleanPath.startsWith('file:///')) { - cleanPath = cleanPath.substring(8); - } else if (cleanPath.startsWith('file://')) { - cleanPath = cleanPath.substring(7); - } - cleanPath = cleanPath.replace(/\//g, '\\'); - (window as any).electronAPI?.openLocalFile?.(cleanPath) - .catch((err: any) => console.error("Impossible d'ouvrir le fichier :", err)); - }} - className="markdown-link-local" - style={{ color: 'var(--text-neon)', textDecoration: 'underline', fontWeight: 'bold' }} - title={`Ouvrir le fichier local : ${props.href}`} - > - {props.children} - - ); - } - return {props.children}; - } - }} - > - {preprocessMessageContent(msg.content)} - - {msg.images && msg.images.length > 0 && ( -
- {msg.images.map((imgUrl, i) => ( - - ))} -
- )} - - {msg.role === 'assistant' && msg.content.length >= TTS_LONG_TEXT_THRESHOLD && ( -
- -
- )} -
- - {msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - + {!isUser && msg.content.length >= TTS_LONG_TEXT_THRESHOLD && ( +
+ +
+ )}
-
- ))} - + ); + if (isUser) { + return ( +
+
+ {markdownAndMedia} +
+ {msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + ✓✓ +
+
+
+ ); + } else { + return ( +
+
+ # [TRANSMISSION: {activeAvatar.name.toUpperCase()} // TELEMETRY: {msg.emotion?.toUpperCase() || 'NEUTRAL'}] + {msg.source && msg.source !== 'eveflow' && ( + + {msg.source} + + )} +
+
+
+ {activeAvatar.name} +
+
+ {markdownAndMedia} +
+ {msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+
+
+
+ ); + } + })} {errorMessage && (
@@ -1848,7 +1881,7 @@ export const App: React.FC = () => { value={inputText} onChange={(e) => setInputText(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSendMessage(inputText)} - placeholder="Tape ton message ici..." + placeholder="Message a Eve..." className="glow-input" disabled={isSending} /> @@ -1857,14 +1890,10 @@ export const App: React.FC = () => { {isSpeaking && (
@@ -2165,17 +2187,21 @@ export const App: React.FC = () => { )} {/* COMPAGNON 3D SECTION (De droite) */} -
-
- - - - - - - - -
+
+ {!isFloatingMode && ( +
+ + {isListening ? 'Eve ecoute' : isSpeaking ? 'Eve parle' : isSending ? 'Eve reflechit' : 'Eve ecoute'} +
+ )} + + {!isFloatingMode && ( + + )} + + {/* Header flottant discret pour contrôle en mode Widget */} {isFloatingMode && ( @@ -2207,37 +2233,12 @@ export const App: React.FC = () => { )} {/* Rendu Canvas 3D */} -
+
- {agentConfig.provider === 'hermes' && ( - runHermesCronAction(job, 'pause')} - onResume={(job) => runHermesCronAction(job, 'resume')} - onRun={(job) => runHermesCronAction(job, 'run')} - onDelete={(job) => runHermesCronAction(job, 'delete')} - onOpenRun={openHermesRunInChat} - /> - )} {avatarId !== 'eve' && activeAvatar.assetPath && (
{activeAvatar.name}: asset premium attendu @@ -2245,7 +2246,7 @@ export const App: React.FC = () => { )} {/* Badge d'humeur en bas */} - {!isFloatingMode && ( + {!isFloatingMode && false && (
Humeur: @@ -2281,8 +2282,37 @@ export const App: React.FC = () => {
)} + +
+ {!isFloatingMode && !showSettings && agentConfig.provider === 'hermes' && showHermesCockpit && ( + runHermesCronAction(job, 'pause')} + onResume={(job) => runHermesCronAction(job, 'resume')} + onRun={(job) => runHermesCronAction(job, 'run')} + onDelete={(job) => runHermesCronAction(job, 'delete')} + onOpenRun={openHermesRunInChat} + onClose={closeHermesCockpit} + /> + )} +
); diff --git a/src/components/AvatarModelLoader.tsx b/src/components/AvatarModelLoader.tsx index e3f397a..3e7dcce 100644 --- a/src/components/AvatarModelLoader.tsx +++ b/src/components/AvatarModelLoader.tsx @@ -22,7 +22,11 @@ const LoadedGlbAvatar: React.FC<{ isSpeaking: boolean; }> = ({ path, avatarId, emotion, isSpeaking }) => { const gltf = useLoader(GLTFLoader, path); - const scene = useMemo(() => gltf.scene.clone(true), [gltf.scene]); + const scene = useMemo(() => { + const cloned = gltf.scene.clone(true); + cloned.position.y = 0.27; + return cloned; + }, [gltf.scene]); useEffect(() => { const style = EmotionService.getEmotionStyle(emotion, avatarId); @@ -48,7 +52,7 @@ const LoadedGlbAvatar: React.FC<{ useFrame((state) => { const t = state.clock.getElapsedTime(); - scene.position.y = Math.sin(t * 1.4) * 0.05 + 0.22; + scene.position.y = Math.sin(t * 1.4) * 0.05 + 0.27; scene.rotation.y = Math.sin(t * 0.45) * 0.05; if (isSpeaking) { diff --git a/src/components/EveModel.tsx b/src/components/EveModel.tsx index bacdb15..29eac67 100644 --- a/src/components/EveModel.tsx +++ b/src/components/EveModel.tsx @@ -555,8 +555,8 @@ export const EveModel: React.FC = ({ emotion, isSpeaking, avatarI // 1. Animation globale de flottaison (Lévitation magnétique) if (modelRef.current) { - // Oscillation en hauteur stabilisée autour de y = 0.22 (remonté pour un centrage élégant) - modelRef.current.position.y = Math.sin(t * 1.5) * 0.06 + 0.22; + // Oscillation en hauteur stabilisée autour de y = 0.27 pour mieux occuper la fenetre avatar. + modelRef.current.position.y = Math.sin(t * 1.5) * 0.06 + 0.27; // Légère rotation sur elle-même (mouvement de veille) modelRef.current.rotation.y = Math.sin(t * 0.5) * 0.05; } @@ -760,7 +760,7 @@ export const EveModel: React.FC = ({ emotion, isSpeaking, avatarI }); return ( - + ); diff --git a/src/components/ThreeCanvas.tsx b/src/components/ThreeCanvas.tsx index eb95ab9..cf8d30b 100644 --- a/src/components/ThreeCanvas.tsx +++ b/src/components/ThreeCanvas.tsx @@ -1,6 +1,7 @@ -import React, { Suspense } from 'react'; -import { Canvas } from '@react-three/fiber'; +import React, { Suspense, useRef } from 'react'; +import { Canvas, useFrame } from '@react-three/fiber'; import { OrbitControls } from '@react-three/drei'; +import * as THREE from 'three'; import { AvatarModelLoader } from './AvatarModelLoader'; import { EveEmotion, EveAvatar } from '../services/emotionService'; @@ -18,11 +19,566 @@ interface ThreeCanvasProps { avatarId?: EveAvatar; } +interface HologramProjectionProps { + emotion: EveEmotion; + isSpeaking: boolean; + avatarId?: EveAvatar; +} + +interface ParticleInstance { + x: number; + y: number; + z: number; + speed: number; + scale: number; + phase: number; +} + +/** + * 3D Holographic rising particles representing active data streams. + * Flowing upwards in an expanding spiral fashion, strictly confined within the restricted projection space (below the avatar). + * Restrained dynamically to the avatar's real-time bottom baseline to prevent overlapping with the avatar's body. + */ +const HologramParticles: React.FC<{ color: THREE.Color; avatarId?: EveAvatar }> = ({ color, avatarId = 'eve' }) => { + const count = 54; // restrained density so the projector stays clean and readable + const particlesRef = useRef(null); + + // Persist random initial coordinates within the cone volume, speed modifiers, and scaling factors + const particleData = React.useMemo(() => { + const data: ParticleInstance[] = []; + for (let i = 0; i < count; i++) { + const y = -0.8 + Math.random() * 0.6; + const ratio = (y + 0.8) / 0.6; + const radiusAtY = 0.17 + 0.17 * ratio; + const angle = Math.random() * Math.PI * 2; + const radius = Math.random() * radiusAtY; + data.push({ + x: Math.cos(angle) * radius, + y: y, + z: Math.sin(angle) * radius, + speed: 0.002 + Math.random() * 0.004, // Elegant floating speed below EVE + scale: 0.16 + Math.random() * 0.4, + phase: Math.random() * Math.PI * 2 + }); + } + return data; + }, []); + + useFrame((state) => { + if (!particlesRef.current) return; + const children = particlesRef.current.children; + const t = state.clock.getElapsedTime(); + + // Compute dynamic avatar bottom in real-time + const isEve = avatarId === 'eve'; + const avatarY = isEve + ? Math.sin(t * 1.5) * 0.06 + 0.27 + : Math.sin(t * 1.4) * 0.05 + 0.27; + const yBottomLocal = (avatarY + 0.1) - 0.59; + + for (let i = 0; i < children.length; i++) { + const child = children[i] as THREE.Mesh; + const data = particleData[i]; + + // Rise animation + data.y += data.speed; + + // Spiral micro-rotation effect + const currentRadius = Math.sqrt(data.x * data.x + data.z * data.z); + const angle = Math.atan2(data.z, data.x) + 0.014; + data.x = Math.cos(angle) * currentRadius; + data.z = Math.sin(angle) * currentRadius; + + // Radial constraints: inverted cone is narrower at the bottom (0.17 radius) than top (0.34 radius) + const ratio = (data.y + 0.8) / 0.6; + const radiusAtY = 0.17 + 0.17 * ratio; + + // Reset position if exceeding top or radial boundaries + if (data.y > yBottomLocal || currentRadius > radiusAtY) { + data.y = -0.8; + const newAngle = Math.random() * Math.PI * 2; + const newRadius = Math.random() * 0.11; // Start close to the lens focal point + data.x = Math.cos(newAngle) * newRadius; + data.z = Math.sin(newAngle) * newRadius; + } + + child.position.set(data.x, data.y, data.z); + + // Sinuous individual flickering + if (child.material) { + const mat = child.material as THREE.MeshBasicMaterial; + mat.opacity = 0.45 + Math.sin(t * 5.0 + data.phase) * 0.15; + } + } + }); + + return ( + + {particleData.map((data, i) => ( + + {/* Energy crystals shape using Dodecahedron for premium specular feel */} + + + + ))} + + ); +}; + +/** + * Animated holographic chamber behind Eve. + * Strong enough to be visible, but abstract enough to avoid the old planet/star look. + */ +const TechnicalBackdrop: React.FC = () => { + const haloRef = useRef(null); + const sweepRef = useRef(null); + const gateRef = useRef(null); + const nodeMat = React.useMemo(() => ( + new THREE.MeshBasicMaterial({ + color: '#00f2ff', + transparent: true, + opacity: 0.72, + depthWrite: false, + blending: THREE.AdditiveBlending + }) + ), []); + const dataLines = React.useMemo(() => ([ + { y: 1.02, width: 2.15, phase: 0.1, speed: 0.36, opacity: 0.34 }, + { y: 0.68, width: 2.55, phase: 1.6, speed: 0.28, opacity: 0.26 }, + { y: 0.24, width: 2.18, phase: 2.8, speed: 0.42, opacity: 0.32 }, + { y: -0.22, width: 2.44, phase: 4.2, speed: 0.31, opacity: 0.24 }, + { y: -0.68, width: 1.92, phase: 5.4, speed: 0.48, opacity: 0.31 } + ]), []); + const orbitNodes = React.useMemo(() => ([ + { radius: 1.04, angle: 0.18, size: 0.034 }, + { radius: 1.36, angle: 1.7, size: 0.024 }, + { radius: 1.68, angle: 2.78, size: 0.028 }, + { radius: 1.28, angle: 4.35, size: 0.022 }, + { radius: 1.52, angle: 5.52, size: 0.026 } + ]), []); + + useFrame((state) => { + const t = state.clock.getElapsedTime(); + if (haloRef.current) { + haloRef.current.rotation.z = t * 0.035; + haloRef.current.scale.setScalar(1 + Math.sin(t * 0.7) * 0.025); + } + if (gateRef.current) { + gateRef.current.rotation.z = Math.sin(t * 0.22) * 0.035; + } + if (sweepRef.current) { + sweepRef.current.children.forEach((child, index) => { + const line = dataLines[index]; + child.position.x = Math.sin(t * line.speed + line.phase) * 0.82; + const mesh = child as THREE.Mesh; + const material = mesh.material as THREE.MeshBasicMaterial; + material.opacity = line.opacity + (Math.sin(t * 1.4 + line.phase) + 1) * 0.08; + }); + } + }); + + return ( + + + + + + + + {dataLines.map((line, index) => ( + + + + + ))} + + + + {[-1.28, -0.86, 0.86, 1.28].map((x, index) => ( + + + + + ))} + + + + {[0.86, 1.14, 1.48, 1.78].map((radius, index) => ( + + + + + ))} + + + + + + {orbitNodes.map((node, index) => ( + + + + ))} + + + + + + + + {[ + [0, -0.94, 0] + ].map((position, index) => ( + + + + + ))} + + ); +}; + +/** + * 3D Hologram Projection platform and light cone visualizer. + * Fully interactive and reactive to speech and emotional state. + * Fixed to a premium cyan hue (#00f2ff) to align perfectly with the EVEFLOW brand. + * Confines the light cone strictly below the avatar's body, stretching to touch her base dynamically. + */ +const HologramProjection: React.FC = ({ emotion, isSpeaking, avatarId = 'eve' }) => { + const beamRef = useRef(null); + const glowRef = useRef(null); + const baseRef = useRef(null); + + // Cybernetic rotative diagnostic ring groups + const ringGroup1Ref = useRef(null); + const ringGroup2Ref = useRef(null); + const ringGroup3Ref = useRef(null); + + // Permanently fixed to signature premium cyan to preserve brand identity and theme consistency + const stateColor = React.useMemo(() => new THREE.Color('#00f2ff'), []); + const isThinking = emotion === 'thinking'; + + // Custom ShaderMaterial to generate a smooth, high-fidelity opacity gradient fading towards the top + const coneMaterial = React.useMemo(() => { + return new THREE.ShaderMaterial({ + uniforms: { + color: { value: stateColor }, + opacity: { value: 0.42 }, + }, + vertexShader: ` + varying vec3 vLocalPosition; + void main() { + vLocalPosition = position; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform vec3 color; + uniform float opacity; + varying vec3 vLocalPosition; + void main() { + // Normalizing local Y from -0.3 (bottom) to 0.3 (top) for a total height of 0.6 + float factor = (vLocalPosition.y + 0.3) / 0.6; + // Smooth progressive fade out to absolute transparency at the top + float grad = clamp(1.0 - factor, 0.0, 1.0); + // Cubic curve for ultra-soft, premium cinematic light scattering + grad = pow(grad, 1.6); + gl_FragColor = vec4(color, grad * opacity); + } + `, + transparent: true, + side: THREE.DoubleSide, + depthWrite: false, + blending: THREE.AdditiveBlending, + }); + }, [stateColor]); + + useFrame((state) => { + const t = state.clock.getElapsedTime(); + + // Dynamic tracking of avatar's float baseline to keep perfect visual contact + const isEve = avatarId === 'eve'; + const avatarY = isEve + ? Math.sin(t * 1.5) * 0.06 + 0.27 + : Math.sin(t * 1.4) * 0.05 + 0.27; + + // The bottom of the avatar shell is approximately Y_avatar - 0.59 + const yBottomLocal = (avatarY + 0.1) - 0.59; + + // Lens source is at Y = -0.80 local (aligned to the new flat holographic emitter lens) + const yLensLocal = -0.80; + const currentHeight = yBottomLocal - yLensLocal; + const currentCenterY = yLensLocal + currentHeight / 2; + + // Scale vertically relative to the default geometry height of 0.6 + if (beamRef.current) { + beamRef.current.scale.y = currentHeight / 0.6; + beamRef.current.position.y = currentCenterY; + } + + // Volumetric base flicker intensity simulation (high-fidelity holographic noise) - Boosted for pronounced cyan look + let baseFlicker = 0.42 + Math.sin(t * 7.5) * 0.05 + Math.cos(t * 3.2) * 0.025; + + if (isThinking) { + baseFlicker += Math.sin(t * 22.0) * 0.035; + } + + if (isSpeaking) { + const voiceFrequencyOscillation = Math.sin(t * 18.0) * 0.08 + Math.abs(Math.cos(t * 12.0)) * 0.05; + baseFlicker += voiceFrequencyOscillation; + } + + // Volumetric soft cone opacity adjustment through custom shader uniforms + if (beamRef.current && beamRef.current.material) { + const mat = beamRef.current.material as THREE.ShaderMaterial; + if (mat.uniforms && mat.uniforms.opacity && mat.uniforms.color) { + mat.uniforms.opacity.value = baseFlicker; + mat.uniforms.color.value.lerp(stateColor, 0.08); + } + } + + // Smooth cybernetic diagnostics rotations + if (ringGroup1Ref.current) ringGroup1Ref.current.rotation.z = t * 0.16; + if (ringGroup2Ref.current) ringGroup2Ref.current.rotation.z = -t * 0.10; + if (ringGroup3Ref.current) ringGroup3Ref.current.rotation.z = t * 0.06; + if (baseRef.current) baseRef.current.scale.setScalar(1 + Math.sin(t * 2.2) * 0.018); + + // Glow plate adjustment - Pulsing emission intensity simulated via opacity for BasicMaterial + if (glowRef.current && glowRef.current.material) { + const mat = glowRef.current.material as THREE.MeshBasicMaterial; + mat.opacity = 0.70 + Math.sin(t * 5.0) * 0.15; + } + }); + + return ( + + {/* 1. Volumetric hologram light cone - Dynamically scaled and positioned to touch EVE's bottom */} + + + + + + {/* High-fidelity upward particle flow synchronized to avatar bottom limit */} + + + {/* 2. Émetteur Holographique Pur (100% Lumineux & Intégré) */} + + + + + + + + + + + + + + + + + + + + + + {/* A. Lentille émissive centrale - Source principale de lumière */} + + + + + + {/* B. Premier anneau concentrique de confinement d'énergie */} + + + + + + {/* C. Anneau segmenté rotatif interne (Sens horaire) */} + + + + + + + + + + + + {/* D. Anneau segmenté rotatif externe (Sens anti-horaire) */} + + + + + + + + + + + + {/* E. Anneau de ciblage tech externe avec micro-graduations */} + + {[0, 1, 2, 3, 4, 5].map((i) => ( + + + + + ))} + + + {/* F. Grand anneau de balayage de diagnostic extérieur pulsant */} + + + + + + + ); +}; + export const ThreeCanvas: React.FC = ({ emotion, isSpeaking, avatarId = 'eve' }) => { return (
{ gl.setClearColor(0x000000, 0); @@ -41,24 +597,26 @@ export const ThreeCanvas: React.FC = ({ emotion, isSpeaking, a color="#e8f4ff" /> - {/* Cyan atmosphere rim — far enough to avoid harsh specular band on helmet */} + {/* Cyan atmosphere rim */} - {/* Desaturated blue fill — subtle depth without coloring the white lacquer */} + {/* Desaturated blue fill */} {/* Soft white back fill to keep geometry readable */} + + {/* OrbitControls calibrated to prevent clipping or losing focus */} .flex-row:last-child { + gap: 14px !important; +} + +.app-header button.glow-input { + width: 38px !important; + height: 38px !important; + padding: 0 !important; + border-radius: 8px !important; + border: 1px solid rgba(226, 240, 255, 0.18) !important; + background: rgba(8, 15, 31, 0.9) !important; +} + +.app-main { + min-height: 0; + gap: 0; + padding: 0; + border: 1px solid rgba(226, 240, 255, 0.14); + border-top: 0; + border-radius: 0 0 8px 8px; + overflow: hidden; +} + +.chat-section { + width: 63.5%; + padding: 16px; + gap: 14px; + border-right: 1px solid rgba(226, 240, 255, 0.28); + background: rgba(5, 12, 26, 0.58); +} + +.hermes-summary-strip { + min-height: 68px; + padding: 0 22px; + gap: 22px; + border: 1px solid rgba(226, 240, 255, 0.16); + border-radius: 8px; + background: rgba(5, 13, 29, 0.88); + font-family: var(--font-body); + font-size: 16px; + font-weight: 600; + letter-spacing: 0; +} + +.hermes-summary-main { + font-size: 17px; + gap: 12px; + text-transform: none; +} + +.hermes-summary-main::after, +.hermes-summary-pill::after { + content: ""; + width: 1px; + height: 30px; + margin-left: 18px; + background: rgba(226, 240, 255, 0.28); +} + +.hermes-summary-pill { + min-height: auto; + padding: 0; + border: 0; + background: transparent; + color: var(--text-primary); + font-size: 16px; +} + +.hermes-summary-button { + min-width: 112px; + min-height: 42px; + padding: 0 22px; + border-radius: 999px; + border-color: var(--accent-cyan); + font-size: 15px; + letter-spacing: 0; + text-transform: none; + background: rgba(0, 212, 245, 0.06); +} + +.messages-container { + min-height: 0; + margin: 0; + padding: 30px 32px; + gap: 28px; + border: 1px solid rgba(226, 240, 255, 0.16); + border-radius: 8px; + background: + linear-gradient(rgba(0, 212, 245, 0.025) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 212, 245, 0.025) 1px, transparent 1px), + rgba(3, 12, 25, 0.64); + background-size: 52px 52px; +} + +.message-bubble { + max-width: 78%; + padding: 20px 24px; + border-radius: 10px; + font-size: 19px; + line-height: 1.55; + box-shadow: 0 16px 32px rgba(0, 0, 0, 0.22); +} + +.message-bubble.assistant { + background: rgba(11, 24, 42, 0.96); + border: 1px solid rgba(226, 240, 255, 0.16); + border-radius: 10px; +} + +.message-bubble.user { + background: linear-gradient(135deg, #008cc2, #005c8a); + border: 1px solid rgba(0, 229, 255, 0.38); + border-radius: 10px 10px 2px 10px; +} + +.message-row.assistant::before { + content: none !important; + display: none !important; +} + +.message-meta { + font-size: 12px; + margin-bottom: 12px; +} + +.input-container { + padding: 14px 20px; + gap: 18px; + border: 1px solid rgba(226, 240, 255, 0.16); + border-radius: 8px; + background: rgba(5, 13, 29, 0.9); +} + +.input-container .glow-input[type="text"] { + height: 58px; + border-radius: 8px; + border-color: rgba(226, 240, 255, 0.14); + background: rgba(11, 24, 42, 0.9); + font-size: 18px; +} + +.input-container button.glow-input, +.input-container .neon-btn { + width: 58px !important; + height: 58px; + min-width: 58px; + padding: 0 !important; + border-radius: 50% !important; +} + +.input-container .neon-btn { + border-color: var(--accent-cyan); + background: linear-gradient(135deg, #00bfe8, #006da8); +} + +.input-container .btn-text { + display: none !important; +} + +.diagnostic-bar { + display: none !important; +} + +.eve-section { + width: 36.5%; + padding: 0; + border: 0; + background: + radial-gradient(circle at 50% 42%, rgba(0, 229, 255, 0.18), transparent 32%), + radial-gradient(circle at 50% 70%, rgba(0, 229, 255, 0.08), transparent 24%), + rgba(4, 11, 24, 0.68); +} + +.eve-section > div[style*="position: relative"] { + z-index: 2; + height: 100%; +} + +.eve-section canvas { + transform: scale(1.42); + transform-origin: 50% 50%; +} + +.eve-companion-status { + position: absolute; + z-index: 4; + left: 50%; + bottom: 112px; + transform: translateX(-50%); + display: inline-flex; + align-items: center; + gap: 14px; + color: var(--text-neon); + font-family: var(--font-body); + font-size: 19px; + font-weight: 500; + pointer-events: none; +} + +.eve-live-dot { + width: 34px; + height: 34px; + border-radius: 50%; + border: 1px solid rgba(0, 229, 255, 0.26); + background: radial-gradient(circle, var(--accent-cyan) 0 7px, rgba(0, 212, 245, 0.12) 8px); + box-shadow: 0 0 22px rgba(0, 229, 255, 0.32); +} + +.eve-section::before { + content: none !important; + display: none !important; +} + +.eve-section::after { + content: none !important; + display: none !important; +} + +.eve-section .avatar-ambient-field { + opacity: 0.55; +} + +.eve-section .avatar-ambient-field::before { + background: radial-gradient(circle at 50% 48%, rgba(0, 229, 255, 0.18), transparent 38%); +} + +.eve-section .avatar-ambient-field::after { + opacity: 0.42; + transform: scale(1); +} + +.eve-section .pulsing-led { + animation-duration: 1.5s; +} + +.eve-section .humeur, +.eve-section [style*="Humeur"] { + display: none; +} + +.eve-section .floating-widget { + border-radius: 10px; +} + +.hermes-cockpit-window { + top: 92px; + right: 22px; + bottom: 22px; + width: min(420px, 36vw); + height: auto; + border-radius: 8px; + background: + linear-gradient(rgba(0, 229, 255, 0.055) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 229, 255, 0.04) 1px, transparent 1px), + rgba(3, 11, 24, 0.94); +} + +/* Cockpit open state: continuous right-side panel, not an overlay window. */ +.app-main:has(> .hermes-cockpit-window) .chat-section { + width: auto; + flex: 1 1 auto; + min-width: 480px; +} + +.app-main:has(> .hermes-cockpit-window) .eve-section { + width: 390px; + flex: 0 0 390px; +} + +.app-main:has(> .hermes-cockpit-window) .eve-section canvas { + transform: scale(1.34); +} + +.app-main:has(> .hermes-cockpit-window) .eve-companion-status { + bottom: 96px; + font-size: 15px; +} + +.app-main:has(> .hermes-cockpit-window) .eve-live-dot { + width: 28px; + height: 28px; +} + +.app-main > .hermes-cockpit-window { + position: relative !important; + inset: auto !important; + flex: 0 0 360px; + width: 360px !important; + height: 100% !important; + min-width: 0; + padding: 16px; + border: 0; + border-left: 1px solid rgba(226, 240, 255, 0.22); + border-radius: 0; + color: var(--text-neon); + background: + linear-gradient(rgba(0, 216, 255, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 216, 255, 0.032) 1px, transparent 1px), + radial-gradient(circle at 50% 20%, rgba(0, 216, 255, 0.09), transparent 34%), + #050914; + background-size: 32px 32px, 32px 32px, auto, auto; + box-shadow: inset 1px 0 0 rgba(0, 216, 255, 0.12); + animation: cockpit-panel-in 0.2s ease-out; +} + +.app-main > .hermes-cockpit-window .tool-screen-header { + height: 52px; + padding: 0 0 14px; + border-bottom: 1px solid rgba(226, 240, 255, 0.16); + font-size: 13px; + letter-spacing: 0.8px; +} + +.app-main > .hermes-cockpit-window .hermes-cockpit-status { + min-height: 44px; + padding: 10px 0; + font-size: 12px; +} + +.app-main > .hermes-cockpit-window .hermes-cockpit-tabs { + gap: 8px; + padding: 12px 0; +} + +.app-main > .hermes-cockpit-window .hermes-cockpit-tabs button, +.app-main > .hermes-cockpit-window .hermes-icon-btn { + height: 34px; + border-radius: 8px; + border-color: rgba(0, 216, 255, 0.24); + background: rgba(7, 18, 33, 0.84); +} + +.app-main > .hermes-cockpit-window .tool-screen-body, +.app-main > .hermes-cockpit-window .hermes-cron-panel, +.app-main > .hermes-cockpit-window .hermes-history-list { + height: calc(100% - 122px); +} + +.app-main > .hermes-cockpit-window .tool-screen-empty, +.app-main > .hermes-cockpit-window .tool-screen-line, +.app-main > .hermes-cockpit-window .hermes-job-name, +.app-main > .hermes-cockpit-window .hermes-run-content strong { + font-size: 12px; +} + +.app-main > .hermes-cockpit-window .hermes-job-meta, +.app-main > .hermes-cockpit-window .hermes-job-result, +.app-main > .hermes-cockpit-window .hermes-run-content span, +.app-main > .hermes-cockpit-window .hermes-run-time { + font-size: 10.5px; +} + +@keyframes cockpit-panel-in { + from { opacity: 0; transform: translateX(12px); } + to { opacity: 1; transform: translateX(0); } +} + +/* Animation active obsolète supprimée */ + +.eve-section.eve-active .eve-live-dot { + animation: eve-live-pulse 1s ease-in-out infinite; +} + +@keyframes eve-waveform-talk { + 0%, 100% { + opacity: 0.42; + transform: scaleY(0.58); + } + 35% { + opacity: 0.95; + transform: scaleY(1.08); + } + 65% { + opacity: 0.7; + transform: scaleY(0.78); + } +} + +@keyframes eve-live-pulse { + 0%, 100% { + box-shadow: 0 0 18px rgba(0, 229, 255, 0.24); + transform: scale(1); + } + 50% { + box-shadow: 0 0 32px rgba(0, 229, 255, 0.55); + transform: scale(1.08); + } +} + +/* --- PREMIUM REDESIGN ACCORDING TO USER MOCKUP --- */ + +/* AppHeader Clean Redesign */ +.app-header { + height: 52px; + min-height: 52px; + background-color: #050914; + border-bottom: 1px solid rgba(226, 240, 255, 0.12); + padding: 0 16px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.logo-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background-color: var(--accent-cyan); + box-shadow: 0 0 10px var(--accent-cyan); + margin-right: 8px; + display: inline-block; +} + +.logo-text { + font-family: var(--font-title); + font-weight: 800; + font-size: 16px; + letter-spacing: 1.5px; + color: #ffffff; +} + +/* Hermes Central Status Pill in Header */ +.header-hermes-status { + display: flex; + align-items: center; + gap: 8px; + background-color: rgba(5, 9, 20, 0.85); + border: 1px solid rgba(0, 242, 255, 0.2); + border-radius: 20px; + padding: 6px 16px; + font-family: var(--font-body); + font-size: 11px; + font-weight: 700; + color: #10b981; /* Hermes OK text green */ +} + +.header-hermes-status .status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #10b981; + box-shadow: 0 0 10px #10b981; + display: inline-block; + animation: pulse-green 2s infinite; +} + +@keyframes pulse-green { + 0%, 100% { opacity: 0.8; transform: scale(1); } + 50% { opacity: 1; transform: scale(1.1); } +} + +/* Window Controls (Top-Right) */ +.window-controls { + display: flex; + align-items: center; + gap: 8px; +} + +.control-btn { + width: 32px; + height: 32px; + border-radius: 6px; + border: 1px solid rgba(226, 240, 255, 0.12); + background-color: rgba(14, 21, 40, 0.4); + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: var(--transition-smooth); +} + +.control-btn:hover { + background-color: rgba(14, 21, 40, 0.8); + border-color: rgba(0, 242, 255, 0.3); + color: #ffffff; +} + +.control-btn.close:hover { + background-color: rgba(239, 68, 68, 0.15); + border-color: rgba(239, 68, 68, 0.4); + color: #ff6b6b; +} + +/* Chat Status Bar under Header */ +.chat-status-bar { + display: flex; + align-items: center; + padding: 8px 16px; + background-color: rgba(3, 7, 18, 0.4); + border-bottom: 1px solid rgba(226, 240, 255, 0.1); + gap: 12px; + font-family: var(--font-body); + font-size: 12px; + width: 100%; +} + +.chat-status-bar .status-item { + display: flex; + align-items: center; + gap: 6px; + font-weight: 600; +} + +.chat-status-bar .status-item.green { + color: #10b981; +} + +.chat-status-bar .status-item.blue { + color: #38bdf8; +} + +.chat-status-bar .status-item.red { + color: #ef4444; +} + +.chat-status-bar .status-separator { + color: rgba(226, 240, 255, 0.15); + font-weight: 300; +} + +.chat-status-bar .cockpit-btn { + margin-left: auto; + background-color: rgba(5, 9, 20, 0.6); + border: 1px solid rgba(0, 242, 255, 0.3); + border-radius: 20px; + padding: 4px 14px; + color: var(--text-neon); + font-family: var(--font-body); + font-weight: 700; + font-size: 11px; + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + transition: var(--transition-smooth); +} + +.chat-status-bar .cockpit-btn:hover { + background-color: rgba(0, 242, 255, 0.1); + box-shadow: 0 0 10px rgba(0, 242, 255, 0.2); +} + +.chat-status-bar .cockpit-btn .chevron { + transition: transform 0.2s ease; +} + +.chat-status-bar .cockpit-btn .chevron.rotated { + transform: rotate(180deg); +} + +/* Chat section CRT effect subtly active */ +.chat-section { + background-color: #030712; + border-right: 1px solid rgba(226, 240, 255, 0.12); +} + +/* Transmission Tag above assistant messages */ +.transmission-tag { + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-neon); + letter-spacing: 1px; + margin: 12px 0 4px 52px; +} + +/* Message Rows with precise avatar grid */ +.messages-container .message-row { + display: grid; + grid-template-columns: 40px 1fr; + gap: 12px; + align-items: start; + width: 100%; +} + +.messages-container .message-row.user { + display: flex; + justify-content: flex-end; +} + +.avatar-wrapper { + width: 40px; + height: 40px; + border-radius: 50%; + border: 1.5px solid rgba(0, 242, 255, 0.3); + overflow: hidden; + box-shadow: 0 0 8px rgba(0, 242, 255, 0.2); +} + +.avatar-img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.messages-container .message-bubble { + max-width: 85%; + border-radius: 12px; + padding: 14px 20px; + font-size: 13.5px; + line-height: 1.5; +} + +.messages-container .message-bubble.assistant { + background-color: #0b1220; + border: 1px solid rgba(0, 242, 255, 0.1); + color: #ffffff; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.messages-container .message-bubble.user { + background: linear-gradient(135deg, #026095, #004570); + border: 1.5px solid rgba(0, 242, 255, 0.45); + color: #ffffff; + box-shadow: 0 0 15px rgba(0, 242, 255, 0.12); + margin-left: auto; + position: relative; + border-radius: 12px 12px 2px 12px; +} + +.messages-container .message-bubble.user .message-time { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + color: rgba(255, 255, 255, 0.6); + font-size: 9px; + font-weight: 500; + margin-top: 6px; +} + +.double-check { + color: var(--text-neon); + font-weight: bold; +} + +/* Input Container Stylized */ +.input-container { + padding: 14px 18px; + background-color: #030712; + border-top: 1px solid rgba(226, 240, 255, 0.1); + display: flex; + align-items: center; + gap: 10px; +} + +.input-container .glow-input { + background-color: #050914; + border: 1px solid rgba(0, 242, 255, 0.15); + color: #ffffff; + border-radius: 20px; + padding: 10px 18px; + font-size: 13px; + outline: none; +} + +.input-container .glow-input:focus { + border-color: rgba(0, 242, 255, 0.6); + box-shadow: 0 0 12px rgba(0, 242, 255, 0.15); +} + +.input-circle-btn { + width: 38px; + height: 38px; + border-radius: 50%; + border: 1px solid rgba(226, 240, 255, 0.12); + background-color: #050914; + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: var(--transition-smooth); + flex-shrink: 0; +} + +.input-circle-btn:hover { + background-color: rgba(0, 242, 255, 0.08); + border-color: rgba(0, 242, 255, 0.4); + color: #ffffff; +} + +.input-send-btn { + width: 42px; + height: 42px; + border-radius: 50%; + background: linear-gradient(135deg, #026095, #004570); + border: 1.5px solid rgba(0, 242, 255, 0.5); + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: var(--transition-smooth); + box-shadow: 0 0 10px rgba(0, 242, 255, 0.2); + flex-shrink: 0; +} + +.input-send-btn:hover { + transform: scale(1.05); + box-shadow: 0 0 15px rgba(0, 242, 255, 0.4); + border-color: var(--accent-cyan); +} + +/* --- EVE PANNEAU LATÉRAL PREMIUM --- */ + +.eve-section { + width: 35%; + background: radial-gradient(circle at 50% 50%, rgba(0, 242, 255, 0.06), transparent 70%), #030712; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 16px 0; +} + + + +/* Pop-out discrete action button */ +.eve-popout-btn { + position: absolute; + top: 12px; + right: 12px; + width: 28px; + height: 28px; + border-radius: 6px; + border: 1px solid rgba(226, 240, 255, 0.12); + background-color: rgba(5, 9, 20, 0.5); + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: var(--transition-smooth); + z-index: 10; +} + +.eve-popout-btn:hover { + background-color: rgba(0, 242, 255, 0.08); + border-color: rgba(0, 242, 255, 0.4); + color: #ffffff; +} + + + +/* Status pill under robot */ +.eve-status-pill { + background-color: rgba(5, 9, 20, 0.8); + border: 1px solid rgba(0, 242, 255, 0.2); + border-radius: 20px; + padding: 6px 18px; + font-family: var(--font-body); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.5px; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 8px; + z-index: 5; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.eve-status-pill .status-led { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--accent-cyan); + box-shadow: 0 0 10px var(--accent-cyan); + display: inline-block; + animation: pulse-cyan 1.5s infinite; +} + +@keyframes pulse-cyan { + 0%, 100% { opacity: 0.7; transform: scale(0.9); } + 50% { opacity: 1; transform: scale(1.1); } +} + + + +/* Hermes vertical sliding volet handle in the border */ +.hermes-drawer-handle { + position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%) translateX(-50%); + z-index: 20; + background-color: #0b1220; + border: 1px solid rgba(0, 242, 255, 0.25); + border-radius: 20px 0 0 20px; /* curved out on left side */ + border-right: none; + padding: 16px 4px 16px 6px; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + color: var(--text-secondary); + transition: var(--transition-smooth); +} + +.hermes-drawer-handle:hover { + color: var(--text-neon); + background-color: #0e172a; + border-color: var(--accent-cyan); +} + +.hermes-drawer-handle span { + font-family: var(--font-body); + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + writing-mode: vertical-rl; /* Vertical vertical text orientation */ + letter-spacing: 2px; +} + +/* --- VISUAL CONFORMITY PASS: desktop companion layout --- */ +.app-main { + display: flex; + min-width: 0; +} + +.chat-section { + flex: 1 1 auto; + min-width: 0; +} + +.eve-section { + flex: 0 0 clamp(360px, 35vw, 440px); + width: clamp(360px, 35vw, 440px); + min-width: 360px; + overflow: hidden; + border-left: 1px solid rgba(226, 240, 255, 0.16); + background: + linear-gradient(rgba(0, 216, 255, 0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 216, 255, 0.038) 1px, transparent 1px), + radial-gradient(ellipse at 50% 42%, rgba(0, 216, 255, 0.28), transparent 48%), + radial-gradient(ellipse at 50% 86%, rgba(0, 216, 255, 0.2), transparent 42%), + #030712; + background-size: 46px 46px, 46px 46px, auto, auto, auto; + animation: eve-panel-field 12s linear infinite; +} + +.eve-section::before, +.eve-section::after { + content: ""; + display: block !important; + position: absolute; + pointer-events: none; + z-index: 1; +} + +.eve-section::before { + width: min(72%, 330px); + aspect-ratio: 1; + left: 50%; + top: 20%; + transform: translateX(-50%); + border: 1px solid rgba(0, 216, 255, 0.16); + border-radius: 50%; + box-shadow: + 0 0 92px rgba(0, 216, 255, 0.22), + inset 0 0 70px rgba(0, 216, 255, 0.12); + opacity: 1; + animation: eve-halo-idle 7s ease-in-out infinite; +} + +.eve-section::after { + left: 50%; + bottom: 94px; + width: min(34%, 150px); + height: 28px; + transform: translateX(-50%); + border: 1px solid rgba(0, 216, 255, 0.22); + border-radius: 50%; + background: radial-gradient(ellipse at center, rgba(138, 247, 255, 0.34), rgba(0, 216, 255, 0.12) 54%, transparent 75%); + box-shadow: 0 0 42px rgba(0, 216, 255, 0.2); + filter: blur(0.1px); +} + +.eve-section.eve-active::before { + animation: eve-halo-talk 1.15s ease-in-out infinite; +} + +@keyframes eve-panel-field { + 0% { + background-position: 0 0, 0 0, center, center, center; + } + 100% { + background-position: 46px 92px, -46px 46px, center, center, center; + } +} + +@keyframes eve-halo-idle { + 0%, 100% { + opacity: 0.62; + transform: translateX(-50%) scale(0.98) rotate(0deg); + } + 50% { + opacity: 0.92; + transform: translateX(-50%) scale(1.025) rotate(4deg); + } +} + +@keyframes eve-halo-talk { + 0%, 100% { + opacity: 0.68; + transform: translateX(-50%) scale(0.985); + } + 50% { + opacity: 1; + transform: translateX(-50%) scale(1.035); + } +} + +.eve-section > div[style*="position: relative"] { + z-index: 2; +} + +.eve-status-pill { + position: absolute; + left: 50%; + bottom: 62px; + transform: translateX(-50%); + z-index: 5; + min-width: 126px; + justify-content: center; + color: var(--text-neon); + background: rgba(3, 9, 20, 0.72); + backdrop-filter: blur(8px); +} + +.eve-section.eve-active .eve-status-pill { + box-shadow: 0 0 20px rgba(0, 216, 255, 0.22); +} + +.app-main:has(> .hermes-cockpit-window) .chat-section { + flex: 1 1 0; + min-width: 420px; +} + +.app-main:has(> .hermes-cockpit-window) .eve-section { + flex: 0 0 clamp(360px, 29vw, 410px); + width: clamp(360px, 29vw, 410px); + min-width: 360px; +} + +.app-main:has(> .hermes-cockpit-window) .eve-status-pill { + bottom: 58px; +} + +.app-main > .hermes-cockpit-window { + align-self: stretch; + flex: 0 0 360px; + width: 360px !important; + max-width: 360px; + height: auto !important; +} + +@media (max-width: 1220px) { + .app-main:has(> .hermes-cockpit-window) .chat-section { + min-width: 360px; + } + + .app-main:has(> .hermes-cockpit-window) .eve-section { + flex-basis: 340px; + width: 340px; + min-width: 340px; + } + + .app-main > .hermes-cockpit-window { + flex-basis: 330px; + width: 330px !important; + max-width: 330px; + } +} + +/* --- HERMES STATUS STRIP: match visual reference --- */ +.chat-status-bar { + min-height: 54px; + height: 54px; + padding: 0 16px; + gap: 13px; + background: #030712; + border: 0; + border-bottom: 1px solid rgba(226, 240, 255, 0.12); + border-radius: 0; + font-family: var(--font-body); + font-size: 12px; + letter-spacing: 0; + flex-shrink: 0; +} + +.chat-status-bar .status-item { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + height: 26px; + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.chat-status-bar .status-item svg { + width: 14px; + height: 14px; + flex: 0 0 14px; + stroke-width: 2.25; +} + +.chat-status-bar .status-item.green { + color: #39f5a4; +} + +.chat-status-bar .status-item.green svg { + color: #39f5a4; +} + +.chat-status-bar .status-item.blue { + color: #22d8ff; +} + +.chat-status-bar .status-item.blue svg { + color: #22d8ff; +} + +.chat-status-bar .status-item.red { + color: #ffca55; +} + +.chat-status-bar .status-item.red svg { + color: #ff6b73; + fill: rgba(255, 107, 115, 0.18); +} + +.chat-status-bar .status-separator { + width: 1px; + height: 18px; + overflow: hidden; + color: transparent; + background: rgba(226, 240, 255, 0.16); + flex: 0 0 1px; +} + +.chat-status-bar .cockpit-btn { + margin-left: auto; + height: 26px; + min-width: 88px; + padding: 0 13px; + border: 1px solid rgba(0, 216, 255, 0.82); + border-radius: 999px; + background: rgba(0, 216, 255, 0.035); + color: #00e5ff; + font-size: 12px; + font-weight: 800; + line-height: 1; + letter-spacing: 0; + text-transform: none; + gap: 7px; + box-shadow: none; +} + +.chat-status-bar .cockpit-btn:hover { + background: rgba(0, 216, 255, 0.1); + box-shadow: 0 0 12px rgba(0, 216, 255, 0.18); +} + +.chat-status-bar .cockpit-btn .chevron { + width: 12px; + height: 12px; + stroke-width: 2.5; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index a1e54b8..ca96eaa 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -4,6 +4,7 @@ interface Window { electronAPI?: { windowControl: (action: 'minimize' | 'maximize' | 'close') => void; setWindowMode: (mode: 'floating' | 'normal') => void; + setCockpitMode: (isOpen: boolean) => void; onWindowModeChanged: (callback: (mode: 'floating' | 'normal') => void) => () => void; onHermesPush: (callback: (event: any) => void) => () => void; writeLog: (entry: any) => void;