## Error Handling - Add GlobalErrorBoundary with error classification and recovery - Add custom error types (SecurityError, ConnectionError, TimeoutError) - Fix ErrorAlert component syntax errors ## Offline Mode - Add offlineStore for offline state management - Implement message queue with localStorage persistence - Add exponential backoff reconnection (1s→60s) - Add OfflineIndicator component with status display - Queue messages when offline, auto-retry on reconnect ## Security Hardening - Add AES-256-GCM encryption for chat history storage - Add secure API key storage with OS keychain integration - Add security audit logging system - Add XSS prevention and input validation utilities - Add rate limiting and token generation helpers ## CI/CD (Gitea Actions) - Add .gitea/workflows/ci.yml for continuous integration - Add .gitea/workflows/release.yml for release automation - Support Windows Tauri build and release ## UI Components - Add LoadingSpinner, LoadingOverlay, LoadingDots components - Add MessageSkeleton, ConversationListSkeleton skeletons - Add EmptyMessages, EmptyConversations empty states - Integrate loading states in ChatArea and ConversationList ## E2E Tests - Fix WebSocket mock for streaming response tests - Fix approval endpoint route matching - Add store state exposure for testing - All 19 core-features tests now passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
182 lines
5.2 KiB
TypeScript
182 lines
5.2 KiB
TypeScript
import { cn } from '../../lib/utils';
|
|
|
|
interface SkeletonProps {
|
|
className?: string;
|
|
}
|
|
|
|
export function Skeleton({ className }: SkeletonProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'animate-pulse bg-gray-200 dark:bg-gray-700 rounded',
|
|
className
|
|
)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function CardSkeleton() {
|
|
return (
|
|
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
|
|
<Skeleton className="h-4 w-24 mb-3" />
|
|
<Skeleton className="h-3 w-full mb-2" />
|
|
<Skeleton className="h-3 w-3/4" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ListSkeleton({ count = 3 }: { count?: number }) {
|
|
return (
|
|
<div className="space-y-2">
|
|
{Array.from({ length: count }).map((_, i) => (
|
|
<div key={i} className="flex items-center gap-3 p-2">
|
|
<Skeleton className="w-8 h-8 rounded-full" />
|
|
<div className="flex-1">
|
|
<Skeleton className="h-3 w-24 mb-1" />
|
|
<Skeleton className="h-2 w-32" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Skeleton for a single chat message bubble.
|
|
* Supports both user and assistant message styles.
|
|
*/
|
|
export function MessageSkeleton({ isUser = false }: { isUser?: boolean }) {
|
|
return (
|
|
<div className={cn('flex gap-4', isUser && 'justify-end')}>
|
|
<div
|
|
className={cn(
|
|
'w-8 h-8 rounded-lg flex-shrink-0',
|
|
isUser ? 'bg-gray-200 dark:bg-gray-600 order-last' : 'bg-gray-300 dark:bg-gray-600'
|
|
)}
|
|
>
|
|
<Skeleton className="w-full h-full rounded-lg" />
|
|
</div>
|
|
<div className={cn('flex-1', isUser && 'max-w-2xl')}>
|
|
<div
|
|
className={cn(
|
|
'p-4 rounded-2xl',
|
|
isUser
|
|
? 'bg-orange-100 dark:bg-orange-900/30'
|
|
: 'bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700'
|
|
)}
|
|
>
|
|
<Skeleton className="h-4 w-full mb-2" />
|
|
<Skeleton className="h-4 w-3/4 mb-2" />
|
|
<Skeleton className="h-4 w-1/2" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Skeleton for a list of chat messages.
|
|
* Alternates between user and assistant skeletons.
|
|
*/
|
|
export function MessageListSkeleton({ count = 4 }: { count?: number }) {
|
|
return (
|
|
<div className="space-y-6 p-6">
|
|
{Array.from({ length: count }).map((_, i) => (
|
|
<MessageSkeleton key={i} isUser={i % 2 === 0} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Skeleton for a conversation item in the sidebar.
|
|
*/
|
|
export function ConversationItemSkeleton() {
|
|
return (
|
|
<div className="flex items-center gap-3 px-3 py-3 border-b border-gray-50 dark:border-gray-800">
|
|
<Skeleton className="w-7 h-7 rounded-lg flex-shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<Skeleton className="h-3 w-24 mb-1.5" />
|
|
<Skeleton className="h-2 w-32" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Skeleton for the conversation list sidebar.
|
|
*/
|
|
export function ConversationListSkeleton({ count = 5 }: { count?: number }) {
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
{/* Header skeleton */}
|
|
<div className="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-gray-700">
|
|
<Skeleton className="h-3 w-16" />
|
|
<Skeleton className="w-4 h-4 rounded" />
|
|
</div>
|
|
{/* List items */}
|
|
<div className="flex-1 overflow-hidden">
|
|
{Array.from({ length: count }).map((_, i) => (
|
|
<ConversationItemSkeleton key={i} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Skeleton for the chat header.
|
|
*/
|
|
export function ChatHeaderSkeleton() {
|
|
return (
|
|
<div className="h-14 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between px-6 bg-white dark:bg-gray-900">
|
|
<div className="flex items-center gap-2">
|
|
<Skeleton className="h-5 w-24" />
|
|
<Skeleton className="h-3 w-20" />
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Skeleton className="h-8 w-8 rounded-full" />
|
|
<Skeleton className="h-8 w-8 rounded-full" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Skeleton for the chat input area.
|
|
*/
|
|
export function ChatInputSkeleton() {
|
|
return (
|
|
<div className="border-t border-gray-100 dark:border-gray-800 p-4 bg-white dark:bg-gray-900">
|
|
<div className="max-w-4xl mx-auto">
|
|
<div className="flex items-end gap-2 bg-gray-50 dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700 p-2">
|
|
<Skeleton className="w-5 h-5 rounded" />
|
|
<div className="flex-1 py-1">
|
|
<Skeleton className="h-5 w-full" />
|
|
</div>
|
|
<Skeleton className="w-16 h-6 rounded" />
|
|
<Skeleton className="w-8 h-8 rounded-full" />
|
|
</div>
|
|
<div className="text-center mt-2">
|
|
<Skeleton className="h-3 w-40 mx-auto" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Full chat area skeleton including header, messages, and input.
|
|
*/
|
|
export function ChatAreaSkeleton({ messageCount = 4 }: { messageCount?: number }) {
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<ChatHeaderSkeleton />
|
|
<div className="flex-1 overflow-hidden">
|
|
<MessageListSkeleton count={messageCount} />
|
|
</div>
|
|
<ChatInputSkeleton />
|
|
</div>
|
|
);
|
|
}
|