mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-02 04:39:29 -05:00
The New Archon (Beta) - The Operating System for AI Coding Assistants!
This commit is contained in:
60
archon-ui-main/src/hooks/useBugReport.ts
Normal file
60
archon-ui-main/src/hooks/useBugReport.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useState } from 'react';
|
||||
import { bugReportService, BugContext } from '../services/bugReportService';
|
||||
|
||||
export const useBugReport = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [context, setContext] = useState<BugContext | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const openBugReport = async (error?: Error) => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const bugContext = await bugReportService.collectBugContext(error);
|
||||
setContext(bugContext);
|
||||
setIsOpen(true);
|
||||
} catch (contextError) {
|
||||
console.error('Failed to collect bug context:', contextError);
|
||||
// Still open the modal but with minimal context
|
||||
setContext({
|
||||
error: {
|
||||
message: error?.message || 'Manual bug report',
|
||||
stack: error?.stack,
|
||||
name: error?.name || 'UserReportedError'
|
||||
},
|
||||
app: {
|
||||
version: 'unknown',
|
||||
url: window.location.href,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
system: {
|
||||
platform: navigator.platform,
|
||||
userAgent: navigator.userAgent,
|
||||
memory: 'unknown'
|
||||
},
|
||||
services: {
|
||||
server: false,
|
||||
mcp: false,
|
||||
agents: false
|
||||
},
|
||||
logs: ['Failed to collect logs']
|
||||
});
|
||||
setIsOpen(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeBugReport = () => {
|
||||
setIsOpen(false);
|
||||
setContext(null);
|
||||
};
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
context,
|
||||
loading,
|
||||
openBugReport,
|
||||
closeBugReport
|
||||
};
|
||||
};
|
||||
92
archon-ui-main/src/hooks/useCardTilt.ts
Normal file
92
archon-ui-main/src/hooks/useCardTilt.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useState, useRef } from 'react'
|
||||
interface TiltOptions {
|
||||
max: number
|
||||
scale: number
|
||||
speed: number
|
||||
perspective: number
|
||||
easing: string
|
||||
}
|
||||
export const useCardTilt = (options: Partial<TiltOptions> = {}) => {
|
||||
const {
|
||||
max = 15,
|
||||
scale = 1.05,
|
||||
speed = 500,
|
||||
perspective = 1000,
|
||||
easing = 'cubic-bezier(.03,.98,.52,.99)',
|
||||
} = options
|
||||
const [tiltStyles, setTiltStyles] = useState({
|
||||
transform: `perspective(${perspective}px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)`,
|
||||
transition: `transform ${speed}ms ${easing}`,
|
||||
reflectionOpacity: 0,
|
||||
reflectionPosition: '50% 50%',
|
||||
glowIntensity: 0,
|
||||
glowPosition: { x: 50, y: 50 },
|
||||
})
|
||||
const cardRef = useRef<HTMLDivElement>(null)
|
||||
const isHovering = useRef(false)
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return
|
||||
const rect = cardRef.current.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
const centerX = rect.width / 2
|
||||
const centerY = rect.height / 2
|
||||
const percentX = (x - centerX) / centerX
|
||||
const percentY = (y - centerY) / centerY
|
||||
const tiltX = max * -1 * percentY
|
||||
const tiltY = max * percentX
|
||||
// Calculate glow position (0-100%)
|
||||
const glowX = (x / rect.width) * 100
|
||||
const glowY = (y / rect.height) * 100
|
||||
// Calculate reflection position
|
||||
const reflectionX = 50 + percentX * 15
|
||||
const reflectionY = 50 + percentY * 15
|
||||
setTiltStyles({
|
||||
transform: `perspective(${perspective}px) rotateX(${tiltX}deg) rotateY(${tiltY}deg) scale3d(${scale}, ${scale}, ${scale})`,
|
||||
transition: `transform ${speed}ms ${easing}`,
|
||||
reflectionOpacity: 0.15,
|
||||
reflectionPosition: `${reflectionX}% ${reflectionY}%`,
|
||||
glowIntensity: 1,
|
||||
glowPosition: { x: glowX, y: glowY },
|
||||
})
|
||||
}
|
||||
const handleMouseEnter = () => {
|
||||
isHovering.current = true
|
||||
}
|
||||
const handleMouseLeave = () => {
|
||||
isHovering.current = false
|
||||
setTiltStyles({
|
||||
transform: `perspective(${perspective}px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)`,
|
||||
transition: `transform ${speed}ms ${easing}`,
|
||||
reflectionOpacity: 0,
|
||||
reflectionPosition: '50% 50%',
|
||||
glowIntensity: 0,
|
||||
glowPosition: { x: 50, y: 50 },
|
||||
})
|
||||
}
|
||||
const handleClick = () => {
|
||||
// Bounce animation on click
|
||||
if (cardRef.current) {
|
||||
cardRef.current.style.animation = 'card-bounce 0.4s'
|
||||
cardRef.current.addEventListener(
|
||||
'animationend',
|
||||
() => {
|
||||
if (cardRef.current) {
|
||||
cardRef.current.style.animation = ''
|
||||
}
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
}
|
||||
}
|
||||
return {
|
||||
cardRef,
|
||||
tiltStyles,
|
||||
handlers: {
|
||||
onMouseMove: handleMouseMove,
|
||||
onMouseEnter: handleMouseEnter,
|
||||
onMouseLeave: handleMouseLeave,
|
||||
onClick: handleClick,
|
||||
},
|
||||
}
|
||||
}
|
||||
203
archon-ui-main/src/hooks/useNeonGlow.ts
Normal file
203
archon-ui-main/src/hooks/useNeonGlow.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface NeonGlowOptions {
|
||||
opacity?: number;
|
||||
blur?: number;
|
||||
size?: number;
|
||||
color?: string;
|
||||
speed?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface NeonGlowHook {
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
isAnimating: boolean;
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
updateOptions: (options: Partial<NeonGlowOptions>) => void;
|
||||
}
|
||||
|
||||
export const useNeonGlow = (initialOptions: NeonGlowOptions = {}): NeonGlowHook => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const [options, setOptions] = useState<Required<NeonGlowOptions>>({
|
||||
opacity: 0.8,
|
||||
blur: 2,
|
||||
size: 100,
|
||||
color: 'blue',
|
||||
speed: 2000,
|
||||
enabled: true,
|
||||
...initialOptions
|
||||
});
|
||||
|
||||
const animationRef = useRef<number>();
|
||||
const elementsRef = useRef<HTMLDivElement[]>([]);
|
||||
|
||||
// Create optimized heart chakra pattern
|
||||
const createHeartChakra = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// Clear existing elements
|
||||
elementsRef.current.forEach(el => {
|
||||
if (containerRef.current?.contains(el)) {
|
||||
containerRef.current.removeChild(el);
|
||||
}
|
||||
});
|
||||
elementsRef.current = [];
|
||||
|
||||
const container = containerRef.current;
|
||||
const centerX = container.clientWidth / 2;
|
||||
const centerY = container.clientHeight / 2;
|
||||
const radius = options.size;
|
||||
|
||||
// Create heart shape using mathematical equation
|
||||
// Using fewer points for better performance (20 instead of 100)
|
||||
const heartPoints = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const t = (i / 20) * Math.PI * 2;
|
||||
|
||||
// Heart equation: x = 16sin³(t), y = 13cos(t) - 5cos(2t) - 2cos(3t) - cos(4t)
|
||||
const heartX = centerX + Math.pow(Math.sin(t), 3) * radius * 0.8;
|
||||
const heartY = centerY - (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)) * radius * 0.04;
|
||||
|
||||
heartPoints.push({ x: heartX, y: heartY });
|
||||
}
|
||||
|
||||
// Create 12 radiating lines from center (reduced from more for performance)
|
||||
const rayPoints = [];
|
||||
for (let ray = 0; ray < 12; ray++) {
|
||||
const rayAngle = (ray * Math.PI * 2 / 12);
|
||||
const rayRadius = radius * 0.8;
|
||||
rayPoints.push({
|
||||
x: centerX + Math.cos(rayAngle) * rayRadius,
|
||||
y: centerY + Math.sin(rayAngle) * rayRadius
|
||||
});
|
||||
}
|
||||
|
||||
// Create elements using CSS animations instead of JS manipulation
|
||||
[...heartPoints, ...rayPoints].forEach((point, index) => {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'neon-glow-particle';
|
||||
|
||||
// Use CSS custom properties for easy updates
|
||||
element.style.cssText = `
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
left: ${point.x}px;
|
||||
top: ${point.y}px;
|
||||
transform: translate(-50%, -50%);
|
||||
background: transparent;
|
||||
box-shadow:
|
||||
0 0 10px hsla(220, 90%, 60%, var(--neon-opacity)),
|
||||
0 0 20px hsla(260, 80%, 50%, calc(var(--neon-opacity) * 0.7)),
|
||||
0 0 30px hsla(220, 70%, 40%, calc(var(--neon-opacity) * 0.5));
|
||||
filter: blur(var(--neon-blur));
|
||||
animation: neonPulse var(--neon-speed) ease-in-out infinite;
|
||||
animation-delay: ${index * 50}ms;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
container.appendChild(element);
|
||||
elementsRef.current.push(element);
|
||||
});
|
||||
|
||||
// Update CSS custom properties
|
||||
updateCSSProperties();
|
||||
};
|
||||
|
||||
const updateCSSProperties = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
container.style.setProperty('--neon-opacity', options.opacity.toString());
|
||||
container.style.setProperty('--neon-blur', `${options.blur}px`);
|
||||
container.style.setProperty('--neon-speed', `${options.speed}ms`);
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (!options.enabled || isAnimating) return;
|
||||
|
||||
setIsAnimating(true);
|
||||
createHeartChakra();
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
setIsAnimating(false);
|
||||
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
|
||||
// Clean up elements
|
||||
elementsRef.current.forEach(el => {
|
||||
if (containerRef.current?.contains(el)) {
|
||||
containerRef.current.removeChild(el);
|
||||
}
|
||||
});
|
||||
elementsRef.current = [];
|
||||
};
|
||||
|
||||
const updateOptions = (newOptions: Partial<NeonGlowOptions>) => {
|
||||
setOptions(prev => ({ ...prev, ...newOptions }));
|
||||
};
|
||||
|
||||
// Add CSS keyframes when component mounts
|
||||
useEffect(() => {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes neonPulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.neon-glow-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
return () => {
|
||||
if (document.head.contains(style)) {
|
||||
document.head.removeChild(style);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update CSS properties when options change
|
||||
useEffect(() => {
|
||||
if (isAnimating) {
|
||||
updateCSSProperties();
|
||||
}
|
||||
}, [options, isAnimating]);
|
||||
|
||||
// Recreate pattern when size changes
|
||||
useEffect(() => {
|
||||
if (isAnimating && containerRef.current) {
|
||||
createHeartChakra();
|
||||
}
|
||||
}, [options.size]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
stop();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
isAnimating,
|
||||
start,
|
||||
stop,
|
||||
updateOptions
|
||||
};
|
||||
};
|
||||
53
archon-ui-main/src/hooks/useOptimisticUpdates.ts
Normal file
53
archon-ui-main/src/hooks/useOptimisticUpdates.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
|
||||
export interface PendingUpdate<T> {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
data: T;
|
||||
operation: 'create' | 'update' | 'delete' | 'reorder';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for tracking optimistic updates to prevent re-applying server echoes
|
||||
*
|
||||
* @example
|
||||
* const { addPendingUpdate, isPendingUpdate } = useOptimisticUpdates<Task>();
|
||||
*
|
||||
* // When making an optimistic update
|
||||
* addPendingUpdate({
|
||||
* id: task.id,
|
||||
* timestamp: Date.now(),
|
||||
* data: updatedTask,
|
||||
* operation: 'update'
|
||||
* });
|
||||
*
|
||||
* // When receiving server update
|
||||
* if (!isPendingUpdate(task.id, serverTask)) {
|
||||
* // Apply the update
|
||||
* }
|
||||
*/
|
||||
export function useOptimisticUpdates<T extends { id: string }>() {
|
||||
const pendingUpdatesRef = useRef<Map<string, PendingUpdate<T>>>(new Map());
|
||||
|
||||
const addPendingUpdate = useCallback((update: PendingUpdate<T>) => {
|
||||
pendingUpdatesRef.current.set(update.id, update);
|
||||
// Auto-cleanup after 5 seconds
|
||||
setTimeout(() => {
|
||||
pendingUpdatesRef.current.delete(update.id);
|
||||
}, 5000);
|
||||
}, []);
|
||||
|
||||
const isPendingUpdate = useCallback((id: string, data: T): boolean => {
|
||||
const pending = pendingUpdatesRef.current.get(id);
|
||||
if (!pending) return false;
|
||||
|
||||
// Compare relevant fields based on operation type
|
||||
return JSON.stringify(pending.data) === JSON.stringify(data);
|
||||
}, []);
|
||||
|
||||
const removePendingUpdate = useCallback((id: string) => {
|
||||
pendingUpdatesRef.current.delete(id);
|
||||
}, []);
|
||||
|
||||
return { addPendingUpdate, isPendingUpdate, removePendingUpdate };
|
||||
}
|
||||
37
archon-ui-main/src/hooks/useSocketSubscription.ts
Normal file
37
archon-ui-main/src/hooks/useSocketSubscription.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useEffect, useCallback, DependencyList } from 'react';
|
||||
import { WebSocketService, WebSocketMessage } from '../services/socketIOService';
|
||||
|
||||
/**
|
||||
* Hook for managing Socket.IO subscriptions with proper cleanup and memoization
|
||||
*
|
||||
* @example
|
||||
* useSocketSubscription(
|
||||
* taskUpdateSocketIO,
|
||||
* 'task_updated',
|
||||
* (data) => {
|
||||
* console.log('Task updated:', data);
|
||||
* },
|
||||
* [dependency1, dependency2]
|
||||
* );
|
||||
*/
|
||||
export function useSocketSubscription<T = any>(
|
||||
socket: WebSocketService,
|
||||
eventName: string,
|
||||
handler: (data: T) => void,
|
||||
deps: DependencyList = []
|
||||
) {
|
||||
// Memoize the handler
|
||||
const stableHandler = useCallback(handler, deps);
|
||||
|
||||
useEffect(() => {
|
||||
const messageHandler = (message: WebSocketMessage) => {
|
||||
stableHandler(message.data || message);
|
||||
};
|
||||
|
||||
socket.addMessageHandler(eventName, messageHandler);
|
||||
|
||||
return () => {
|
||||
socket.removeMessageHandler(eventName, messageHandler);
|
||||
};
|
||||
}, [socket, eventName, stableHandler]);
|
||||
}
|
||||
74
archon-ui-main/src/hooks/useStaggeredEntrance.ts
Normal file
74
archon-ui-main/src/hooks/useStaggeredEntrance.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
/**
|
||||
* Custom hook for creating staggered entrance animations
|
||||
* @param items Array of items to animate
|
||||
* @param staggerDelay Delay between each item animation (in seconds)
|
||||
* @param forceReanimateCounter Optional counter to force reanimation when it changes
|
||||
* @returns Animation variants and props for Framer Motion
|
||||
*/
|
||||
export const useStaggeredEntrance = <T,>(items: T[], staggerDelay: number = 0.15, forceReanimateCounter?: number) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
useEffect(() => {
|
||||
// Set visible after component mounts for the animation to trigger
|
||||
setIsVisible(true);
|
||||
// Reset visibility briefly to trigger reanimation when counter changes
|
||||
if (forceReanimateCounter !== undefined && forceReanimateCounter > 0) {
|
||||
setIsVisible(false);
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, 50);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [forceReanimateCounter]);
|
||||
// Parent container variants
|
||||
const containerVariants = {
|
||||
hidden: {
|
||||
opacity: 0
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: staggerDelay,
|
||||
delayChildren: 0.1
|
||||
}
|
||||
}
|
||||
};
|
||||
// Child item variants
|
||||
const itemVariants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
scale: 0.98
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: 'easeOut'
|
||||
}
|
||||
}
|
||||
};
|
||||
// Title animation variants
|
||||
const titleVariants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
scale: 0.98
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: 'easeOut'
|
||||
}
|
||||
}
|
||||
};
|
||||
return {
|
||||
isVisible,
|
||||
containerVariants,
|
||||
itemVariants,
|
||||
titleVariants
|
||||
};
|
||||
};
|
||||
142
archon-ui-main/src/hooks/useTaskSocket.ts
Normal file
142
archon-ui-main/src/hooks/useTaskSocket.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Task Socket Hook - Simplified real-time task synchronization
|
||||
*
|
||||
* This hook provides a clean interface to the task socket service,
|
||||
* replacing the complex useOptimisticUpdates pattern with a simpler
|
||||
* approach that avoids conflicts and connection issues.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { taskSocketService, TaskSocketEvents } from '../services/taskSocketService';
|
||||
import { WebSocketState } from '../services/socketIOService';
|
||||
|
||||
export interface UseTaskSocketOptions {
|
||||
projectId: string;
|
||||
onTaskCreated?: (task: any) => void;
|
||||
onTaskUpdated?: (task: any) => void;
|
||||
onTaskDeleted?: (task: any) => void;
|
||||
onTaskArchived?: (task: any) => void;
|
||||
onTasksReordered?: (data: any) => void;
|
||||
onInitialTasks?: (tasks: any[]) => void;
|
||||
onConnectionStateChange?: (state: WebSocketState) => void;
|
||||
}
|
||||
|
||||
export function useTaskSocket(options: UseTaskSocketOptions) {
|
||||
const {
|
||||
projectId,
|
||||
onTaskCreated,
|
||||
onTaskUpdated,
|
||||
onTaskDeleted,
|
||||
onTaskArchived,
|
||||
onTasksReordered,
|
||||
onInitialTasks,
|
||||
onConnectionStateChange
|
||||
} = options;
|
||||
|
||||
const componentIdRef = useRef<string>(`task-socket-${Math.random().toString(36).substring(7)}`);
|
||||
const currentProjectIdRef = useRef<string | null>(null);
|
||||
const isInitializedRef = useRef<boolean>(false);
|
||||
|
||||
// Memoized handlers to prevent unnecessary re-registrations
|
||||
const memoizedHandlers = useCallback((): Partial<TaskSocketEvents> => {
|
||||
return {
|
||||
onTaskCreated,
|
||||
onTaskUpdated,
|
||||
onTaskDeleted,
|
||||
onTaskArchived,
|
||||
onTasksReordered,
|
||||
onInitialTasks,
|
||||
onConnectionStateChange
|
||||
};
|
||||
}, [
|
||||
onTaskCreated,
|
||||
onTaskUpdated,
|
||||
onTaskDeleted,
|
||||
onTaskArchived,
|
||||
onTasksReordered,
|
||||
onInitialTasks,
|
||||
onConnectionStateChange
|
||||
]);
|
||||
|
||||
// Initialize connection once and register handlers
|
||||
useEffect(() => {
|
||||
if (!projectId || isInitializedRef.current) return;
|
||||
|
||||
const initializeConnection = async () => {
|
||||
try {
|
||||
console.log(`[USE_TASK_SOCKET] Initializing connection for project: ${projectId}`);
|
||||
|
||||
// Register handlers first
|
||||
taskSocketService.registerHandlers(componentIdRef.current, memoizedHandlers());
|
||||
|
||||
// Connect to project (singleton service will handle deduplication)
|
||||
await taskSocketService.connectToProject(projectId);
|
||||
|
||||
currentProjectIdRef.current = projectId;
|
||||
isInitializedRef.current = true;
|
||||
console.log(`[USE_TASK_SOCKET] Successfully initialized for project: ${projectId}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[USE_TASK_SOCKET] Failed to initialize for project ${projectId}:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
initializeConnection();
|
||||
|
||||
}, [projectId, memoizedHandlers]);
|
||||
|
||||
// Update handlers when they change (without reconnecting)
|
||||
useEffect(() => {
|
||||
if (isInitializedRef.current && currentProjectIdRef.current === projectId) {
|
||||
console.log(`[USE_TASK_SOCKET] Updating handlers for component: ${componentIdRef.current}`);
|
||||
taskSocketService.registerHandlers(componentIdRef.current, memoizedHandlers());
|
||||
}
|
||||
}, [memoizedHandlers, projectId]);
|
||||
|
||||
// Handle project change (different project)
|
||||
useEffect(() => {
|
||||
if (!projectId) return;
|
||||
|
||||
// If project changed, reconnect
|
||||
if (isInitializedRef.current && currentProjectIdRef.current !== projectId) {
|
||||
console.log(`[USE_TASK_SOCKET] Project changed from ${currentProjectIdRef.current} to ${projectId}`);
|
||||
|
||||
const switchProject = async () => {
|
||||
try {
|
||||
// Update handlers for new project
|
||||
taskSocketService.registerHandlers(componentIdRef.current, memoizedHandlers());
|
||||
|
||||
// Connect to new project (service handles disconnecting from old)
|
||||
await taskSocketService.connectToProject(projectId);
|
||||
|
||||
currentProjectIdRef.current = projectId;
|
||||
console.log(`[USE_TASK_SOCKET] Successfully switched to project: ${projectId}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[USE_TASK_SOCKET] Failed to switch to project ${projectId}:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
switchProject();
|
||||
}
|
||||
}, [projectId, memoizedHandlers]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
const componentId = componentIdRef.current;
|
||||
|
||||
return () => {
|
||||
console.log(`[USE_TASK_SOCKET] Cleaning up component: ${componentId}`);
|
||||
taskSocketService.unregisterHandlers(componentId);
|
||||
isInitializedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Return utility functions
|
||||
return {
|
||||
isConnected: taskSocketService.isConnected(),
|
||||
connectionState: taskSocketService.getConnectionState(),
|
||||
reconnect: taskSocketService.reconnect.bind(taskSocketService),
|
||||
getCurrentProjectId: taskSocketService.getCurrentProjectId.bind(taskSocketService)
|
||||
};
|
||||
}
|
||||
74
archon-ui-main/src/hooks/useTerminalScroll.ts
Normal file
74
archon-ui-main/src/hooks/useTerminalScroll.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Custom hook for automatic terminal scrolling behavior
|
||||
* Automatically scrolls to bottom when dependencies change
|
||||
* BUT stops auto-scrolling when user manually scrolls up
|
||||
*
|
||||
* @param dependencies - Array of dependencies that trigger scroll
|
||||
* @param enabled - Optional flag to enable/disable scrolling (default: true)
|
||||
* @returns ref to attach to the scrollable container
|
||||
*/
|
||||
export const useTerminalScroll = <T = any>(
|
||||
dependencies: T[],
|
||||
enabled: boolean = true
|
||||
) => {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
||||
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Check if user is at bottom of scroll
|
||||
const isAtBottom = () => {
|
||||
if (!scrollContainerRef.current) return true;
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
|
||||
// Allow 50px threshold for "at bottom" detection
|
||||
return scrollHeight - scrollTop - clientHeight < 50;
|
||||
};
|
||||
|
||||
// Handle user scroll events
|
||||
useEffect(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
// Clear any existing timeout
|
||||
if (scrollTimeoutRef.current) {
|
||||
clearTimeout(scrollTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Check if user scrolled away from bottom
|
||||
if (!isAtBottom()) {
|
||||
setIsUserScrolling(true);
|
||||
}
|
||||
|
||||
// Set timeout to re-enable auto-scroll if user returns to bottom
|
||||
scrollTimeoutRef.current = setTimeout(() => {
|
||||
if (isAtBottom()) {
|
||||
setIsUserScrolling(false);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
return () => {
|
||||
container.removeEventListener('scroll', handleScroll);
|
||||
if (scrollTimeoutRef.current) {
|
||||
clearTimeout(scrollTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Auto-scroll effect
|
||||
useEffect(() => {
|
||||
if (scrollContainerRef.current && enabled && !isUserScrolling) {
|
||||
// Use requestAnimationFrame for smooth scrolling
|
||||
requestAnimationFrame(() => {
|
||||
if (scrollContainerRef.current && !isUserScrolling) {
|
||||
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [...dependencies, isUserScrolling]);
|
||||
|
||||
return scrollContainerRef;
|
||||
};
|
||||
Reference in New Issue
Block a user