extracted terminal to a widget

This commit is contained in:
2026-02-17 18:46:18 +00:00
parent c6999ea66f
commit 0746844d6f
98 changed files with 487 additions and 3513 deletions
@@ -1,21 +0,0 @@
import { Background } from './Background';
type AuthenticationLayoutProps = {
children?: React.ReactNode;
};
export function AuthenticationLayout({ children }: AuthenticationLayoutProps) {
return (
<div className="relative overflow-hidden h-dvh outline-none inset-0">
<section className="relative h-dvh snap-start overflow-hidden">
<Background />
<div className="absolute inset-0 z-520">
<div className="absolute inset-x-0 bottom-0 z-20 flex justify-center pb-8 md:pb-12">
{children}
</div>
</div>
</section>
</div>
);
};
@@ -1,4 +1,3 @@
import { DuckAvatar } from "./DuckAvatar";
import { PixelGrid } from "@/components/PixelGrid";
import landscapebg from './landscape1.jpg';
@@ -13,7 +12,6 @@ export function Background() {
}}
>
<PixelGrid />
<DuckAvatar showDebug={false} fullControlMode={false} currentSection={0} />
</div>
);
};
@@ -0,0 +1,24 @@
import { Background } from './Background';
import { Header } from './Header';
import { Dock, dockItems } from './Dock';
type DashboardLayoutProps = {
children?: React.ReactNode;
};
export function DashboardLayout({ children }: DashboardLayoutProps) {
return (
<div className="relative overflow-hidden h-dvh outline-none inset-0">
<Header />
<section className="relative h-dvh snap-start overflow-hidden">
<Background />
<Dock items={dockItems} />
<div className="absolute inset-0 z-2 pt-[52px] md:pt-[64px] pb-[80px] overflow-y-auto">
{children}
</div>
</section>
</div>
);
};
@@ -0,0 +1,106 @@
import { useRef, useState } from 'react';
import { Link, useLocation } from 'react-router';
import type { LucideIcon } from 'lucide-react';
export type DockItem = {
label: string;
to: string;
icon: LucideIcon;
color: string;
};
type DockProps = {
items: DockItem[];
className?: string;
};
const ICON_SIZE = 48;
const ICON_GAP = 24;
const DOCK_PADDING = 12;
const MAX_SCALE = 1.5;
const MAX_DISTANCE = 150;
const getScale = (mouseX: number | null, iconCenterX: number) => {
if (mouseX === null) return 1;
const distance = Math.abs(mouseX - iconCenterX);
if (distance > MAX_DISTANCE) return 1;
return 1 + (MAX_SCALE - 1) * Math.cos((distance / MAX_DISTANCE) * (Math.PI / 2));
};
export const Dock = ({ items, className }: DockProps) => {
const [mouseX, setMouseX] = useState<number | null>(null);
const dockRef = useRef<HTMLDivElement | null>(null);
const location = useLocation();
const isActive = (to: string) => location.pathname.startsWith(to);
const handleMouseMove = (ev: React.MouseEvent) => {
const rect = dockRef.current?.getBoundingClientRect();
if (rect) setMouseX(ev.clientX - rect.left);
};
const handleMouseLeave = () => setMouseX(null);
return (
<div
ref={dockRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className={`fixed bottom-4 left-1/2 -translate-x-1/2 z-5 items-end gap-2 md:gap-6 px-2 py-1.5 md:px-3 md:py-2 rounded-2xl border backdrop-blur-xl shadow-lg ${className ?? 'flex'}`}
style={{ backgroundColor: 'var(--dock-bg)', borderColor: 'var(--dock-border)' }}
>
{items.map((item, index) => {
const iconCenter = DOCK_PADDING + index * (ICON_SIZE + ICON_GAP) + ICON_SIZE / 2;
const scale = getScale(mouseX, iconCenter);
const active = isActive(item.to);
return (
<Link
key={item.to}
to={item.to}
className="group relative flex flex-col items-center"
style={{
transform: `scale(${scale})`,
transformOrigin: 'bottom center',
transition: 'transform 150ms ease-out',
}}
>
<span
className="absolute -top-9 px-2 py-1 rounded-md text-white text-xs whitespace-nowrap hidden md:block opacity-0 group-hover:opacity-100 transition-opacity duration-150 pointer-events-none"
style={{ backgroundColor: 'var(--dock-tooltip-bg)' }}
>
{item.label}
</span>
<div
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center transition-all"
style={{
background: active ? `${item.color}55` : `${item.color}30`,
boxShadow: active ? `0 0 12px ${item.color}30` : 'none',
}}
>
<item.icon className="h-5 w-5 md:h-6 md:w-6" style={{ color: active ? item.color : `${item.color}cc` }} />
</div>
{active && (
<div className="absolute -bottom-1.5 w-1.5 h-1.5 rounded-full" style={{ background: item.color }} />
)}
</Link>
);
})}
</div>
);
};
import { Terminal, TerminalSquare, FileText, FolderOpen } from 'lucide-react';
import { Sparkles, ClipboardList, ScrollText, Workflow } from 'lucide-react';
export const dockItems: DockItem[] = [
{ label: 'Chat', to: '/chat', icon: Terminal, color: '#60a5fa' },
{ label: 'Files', to: '/files', icon: FolderOpen, color: '#fbbf24' },
{ label: 'Terminal', to: '/terminal', icon: TerminalSquare, color: '#34d399' },
{ label: 'Plans', to: '/plans', icon: FileText, color: '#f472b6' },
{ label: 'Skills', to: '/skills', icon: Sparkles, color: '#c084fc' },
{ label: 'Tasks', to: '/tasks', icon: ClipboardList, color: '#fb923c' },
{ label: 'Processes', to: '/processes', icon: Workflow, color: '#2dd4bf' },
{ label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' },
];
@@ -1,43 +0,0 @@
type DebugPanelProps = {
fullControlMode: boolean;
cameraInfo: {
cameraPosition: number[];
target: number[];
zoom: number;
};
modelInfo: {
position: number[];
rotation: number[];
scale: number;
};
};
export function DebugPanel({ fullControlMode, cameraInfo, modelInfo }: DebugPanelProps) {
return (
<div className="fixed top-4 left-4 bg-black/80 text-white p-4 rounded text-xs font-mono space-y-2 z-[100]">
<div className="font-bold text-blue-400">Controls:</div>
<div className={fullControlMode ? 'text-green-300' : 'text-yellow-300'}>
Mode: {fullControlMode ? 'FULL CONTROL' : 'ROTATION ONLY'}
</div>
<div>* Left-click/Touch + drag: Rotate</div>
{fullControlMode && (
<>
<div>* Right-click + drag: Pan</div>
<div>* Scroll: Zoom</div>
</>
)}
<div>* Ctrl+Alt+Enter: Toggle full control</div>
<div>* Ctrl+Alt+D: Toggle debug panel</div>
<div className="font-bold text-green-400 pt-2">Camera:</div>
<div>Position: [{cameraInfo.cameraPosition.map((v) => v.toFixed(2)).join(', ')}]</div>
<div>Target: [{cameraInfo.target.map((v) => v.toFixed(2)).join(', ')}]</div>
<div>Zoom: {cameraInfo.zoom.toFixed(2)}</div>
<div className="font-bold text-yellow-400 pt-2">Model:</div>
<div>Position: [{modelInfo.position.map((v) => v.toFixed(2)).join(', ')}]</div>
<div>Rotation: [{modelInfo.rotation.map((v) => v.toFixed(2)).join(', ')}]</div>
<div>Scale: {modelInfo.scale.toFixed(2)}</div>
</div>
);
}
@@ -1,118 +0,0 @@
import { Canvas, useThree } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
import { useEffect, useState, useRef } from 'react';
import { DuckModel } from './DuckModel';
import { DebugPanel } from './DebugPanel';
type CameraInfoProps = {
onUpdate: (info: { cameraPosition: number[]; target: number[]; zoom: number }) => void;
initialTarget: [number, number, number];
fullControlMode: boolean;
initialPosition: [number, number, number];
};
function CameraInfo({ onUpdate, initialTarget, fullControlMode, initialPosition }: CameraInfoProps) {
const { camera } = useThree();
const controlsRef = useRef<any>(null);
useEffect(() => {
if (controlsRef.current) {
controlsRef.current.target.set(...initialTarget);
camera.position.set(...initialPosition);
controlsRef.current.update();
}
}, [initialTarget, initialPosition, camera]);
useEffect(() => {
const interval = setInterval(() => {
if (controlsRef.current) {
const controls = controlsRef.current;
onUpdate({
cameraPosition: camera.position.toArray(),
target: controls.target.toArray(),
zoom: camera.zoom,
});
}
}, 100);
return () => clearInterval(interval);
}, [camera, onUpdate]);
return (
<OrbitControls
ref={controlsRef}
enabled={fullControlMode}
enableRotate={fullControlMode}
enableZoom={fullControlMode}
enablePan={fullControlMode}
target={initialTarget}
/>
);
}
type DuckAvatarProps = {
showDebug: boolean;
fullControlMode: boolean;
currentSection: number;
};
export function DuckAvatar({ showDebug, fullControlMode, currentSection }: DuckAvatarProps) {
const isHeroSection = currentSection === 0;
const [cameraInfo, setCameraInfo] = useState({
cameraPosition: [2.89, 5.24, 7.38],
target: [0.3, 3.29, -0.4],
zoom: 1,
});
const [modelInfo, setModelInfo] = useState({
position: [0, -0.2, 0],
rotation: [-0.1, -0.75, 0],
scale: 4.5,
});
const [isLoaded, setIsLoaded] = useState(false);
const heroClasses = 'fixed top-12 md:top-auto md:bottom-0 left-1/2 -translate-x-1/2 w-[60vh] h-[75vh]';
const miniClasses = 'fixed bottom-4 right-4 w-[20vh] h-[25vh]';
return (
<div
className={`${isHeroSection ? heroClasses : miniClasses} overflow-visible z-1 pointer-events-none`}
style={{
background: 'transparent',
opacity: isLoaded ? 1 : 0,
}}
>
<div className="w-full h-full pointer-events-none overflow-visible" style={{ background: 'transparent' }}>
<Canvas
camera={{
position: [2.89, 5.24, 7.38],
fov: 65,
near: 0.1,
far: 1000,
}}
style={{ background: 'transparent' }}
>
<ambientLight intensity={1.5} />
<directionalLight position={[10, 10, 5]} intensity={2} />
<directionalLight position={[-10, -10, -5]} intensity={1} />
<pointLight position={[0, 5, 0]} intensity={1.5} />
<CameraInfo
onUpdate={setCameraInfo}
initialTarget={[0.3, 3.29, -0.4]}
fullControlMode={fullControlMode}
initialPosition={[2.89, 5.24, 7.38]}
/>
<DuckModel
onUpdate={setModelInfo}
onAssetsLoaded={() => {
setIsLoaded(true);
}}
currentSection={currentSection}
/>
</Canvas>
</div>
{showDebug && <DebugPanel fullControlMode={fullControlMode} cameraInfo={cameraInfo} modelInfo={modelInfo} />}
</div>
);
}
@@ -1,68 +0,0 @@
import { useRef, useEffect } from 'react';
import { useGLTF, useAnimations } from '@react-three/drei';
type DuckModelProps = {
onUpdate: (info: { position: number[]; rotation: number[]; scale: number }) => void;
onAssetsLoaded: () => void;
currentSection: number;
};
export function DuckModel({ onUpdate, onAssetsLoaded, currentSection }: DuckModelProps) {
const character = useGLTF('/static/duck3D/Character_output.glb');
const animations = useGLTF('/static/duck3D/Meshy_Merged_Animations.glb');
const meshRef = useRef<any>(null);
const { actions } = useAnimations(animations.animations, meshRef);
useEffect(() => {
onAssetsLoaded();
}, [character, animations, onAssetsLoaded]);
useEffect(() => {
if (!actions) return;
const isContactSection = currentSection === 6;
if (isContactSection) {
Object.values(actions).forEach((a) => a?.fadeOut(0.4));
return;
}
const action = actions['Walking'];
if (!action) return;
Object.values(actions).forEach((a) => a?.fadeOut(0.4));
action.reset();
action.setLoop(2201, Infinity);
action.clampWhenFinished = false;
action.fadeIn(0.4);
action.play();
}, [actions, currentSection]);
useEffect(() => {
const interval = setInterval(() => {
if (meshRef.current) {
onUpdate({
position: meshRef.current.position.toArray(),
rotation: meshRef.current.rotation.toArray().slice(0, 3),
scale: meshRef.current.scale.x,
});
}
}, 100);
return () => clearInterval(interval);
}, [onUpdate]);
const isContactSection = currentSection === 6;
const rotationY = isContactSection ? 0 : (10 * Math.PI) / 180;
return (
<primitive
ref={meshRef}
object={character.scene}
position={[0, -0.2, 0]}
scale={4.5}
rotation={[-0.1, rotationY, 0]}
/>
);
}
@@ -1 +0,0 @@
export * from './DuckAvatar';
@@ -0,0 +1,25 @@
import { Link } from 'react-router';
import { UserMenu } from './UserMenu';
export function Header() {
return (
<header className="fixed z-10 w-full">
<div
className="shrink-0 border-b backdrop-blur-xl shadow-lg px-3 py-2 md:px-6 md:py-3 flex items-center justify-between"
style={{ backgroundColor: 'rgba(255, 255, 255, 0.25)', borderColor: 'rgba(255, 255, 255, 0.35)' }}
>
<Link to="/">
<img
src="/static/officer-logo.svg"
alt="Officer"
className="h-8 md:h-12 w-auto"
style={{ transform: 'skew(-15deg, -2deg)' }}
/>
</Link>
<UserMenu />
</div>
</header>
);
}
@@ -0,0 +1,59 @@
import { Link } from 'react-router';
import * as Dropdown from '@/components/ui/dropdown-menu';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { User, LogOut, Server, Package, Bot } from 'lucide-react';
import { useAuth } from 'hooks/useAuth';
const { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } = Dropdown;
export function UserMenu() {
const { user, isLoading } = useAuth();
if (isLoading) return null;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-duck-yellow cursor-pointer">
<Avatar className="h-9 w-9 rounded-full">
<AvatarImage src={user?.avatar ?? undefined} />
<AvatarFallback className="bg-duck-teal text-duck-yellow text-sm font-bold rounded-full">
{user?.name?.charAt(0).toUpperCase() ?? '?'}
</AvatarFallback>
</Avatar>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="z-10 w-48">
<DropdownMenuItem asChild className="cursor-pointer">
<Link to="/settings/profile">
<User className="mr-2 h-4 w-4" />
Profile
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link to="/settings/ai">
<Bot className="mr-2 h-4 w-4" />
AI Settings
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link to="/settings/server">
<Server className="mr-2 h-4 w-4" />
Server Settings
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link to="/settings/resources">
<Package className="mr-2 h-4 w-4" />
Resources
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link to="/auth/signout">
<LogOut className="mr-2 h-4 w-4" />
Sign Out
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -0,0 +1 @@
export * from './Header';
@@ -1 +1 @@
export * from './AuthenticationLayout';
export * from './DashboardLayout';