@hef2024/llmasaservice-ui 0.16.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -0
- package/dist/index.css +3239 -0
- package/dist/index.d.mts +521 -0
- package/dist/index.d.ts +521 -0
- package/dist/index.js +5885 -0
- package/dist/index.mjs +5851 -0
- package/index.ts +28 -0
- package/package.json +70 -0
- package/src/AIAgentPanel.css +1354 -0
- package/src/AIAgentPanel.tsx +1883 -0
- package/src/AIChatPanel.css +1618 -0
- package/src/AIChatPanel.tsx +1725 -0
- package/src/AgentPanel.tsx +323 -0
- package/src/ChatPanel.css +1093 -0
- package/src/ChatPanel.tsx +3583 -0
- package/src/ChatStatus.tsx +40 -0
- package/src/EmailModal.tsx +56 -0
- package/src/ToolInfoModal.tsx +49 -0
- package/src/components/ui/Button.tsx +57 -0
- package/src/components/ui/Dialog.tsx +153 -0
- package/src/components/ui/Input.tsx +33 -0
- package/src/components/ui/ScrollArea.tsx +29 -0
- package/src/components/ui/Select.tsx +156 -0
- package/src/components/ui/Tooltip.tsx +73 -0
- package/src/components/ui/index.ts +20 -0
- package/src/hooks/useAgentRegistry.ts +349 -0
- package/src/hooks/useConversationStore.ts +313 -0
- package/src/mcpClient.ts +107 -0
- package/tsconfig.json +108 -0
- package/types/declarations.d.ts +22 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ChatStatusProps {
|
|
4
|
+
isLoading: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const ChatStatus: React.FC<ChatStatusProps> = ({ isLoading }) => {
|
|
8
|
+
const [dots, setDots] = useState<string[]>(['', '', '']);
|
|
9
|
+
const [dotIndex, setDotIndex] = useState<number>(0);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (isLoading) {
|
|
13
|
+
const interval = setInterval(() => {
|
|
14
|
+
setDots((prevDots) => {
|
|
15
|
+
const newDots = [...prevDots];
|
|
16
|
+
newDots[dotIndex] = '.';
|
|
17
|
+
setDotIndex((prevIndex) => (prevIndex + 1) % 3);
|
|
18
|
+
return newDots;
|
|
19
|
+
});
|
|
20
|
+
}, 500);
|
|
21
|
+
|
|
22
|
+
return () => clearInterval(interval);
|
|
23
|
+
} else {
|
|
24
|
+
setDots(['', '', '']);
|
|
25
|
+
setDotIndex(0);
|
|
26
|
+
}
|
|
27
|
+
}, [isLoading, dotIndex]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="chat-status">
|
|
31
|
+
{isLoading ? (
|
|
32
|
+
<span className="loading-dots">
|
|
33
|
+
{dots.join('')}
|
|
34
|
+
</span>
|
|
35
|
+
) : null}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default ChatStatus;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import "./ChatPanel.css"; // Ensure this file contains the modal styles
|
|
3
|
+
|
|
4
|
+
interface EmailModalProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
onSend: (to: string, from: string) => void;
|
|
8
|
+
defaultEmail?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const EmailModal: React.FC<EmailModalProps> = ({ isOpen, onClose, onSend, defaultEmail }) => {
|
|
12
|
+
const [email, setEmail] = useState("");
|
|
13
|
+
const [emailFrom, setEmailFrom] = useState(defaultEmail || "");
|
|
14
|
+
|
|
15
|
+
const handleSend = () => {
|
|
16
|
+
onSend(email, emailFrom);
|
|
17
|
+
onClose();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (!isOpen) return null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="modal-overlay">
|
|
24
|
+
<div className="modal-content">
|
|
25
|
+
<p className="modal-text">
|
|
26
|
+
Email Addresses
|
|
27
|
+
<br /> (If multiple, comma separate them)
|
|
28
|
+
</p>
|
|
29
|
+
<p>
|
|
30
|
+
<input
|
|
31
|
+
type="email"
|
|
32
|
+
width="100%"
|
|
33
|
+
value={email}
|
|
34
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
35
|
+
placeholder="To email address"
|
|
36
|
+
/>
|
|
37
|
+
</p>
|
|
38
|
+
<p>
|
|
39
|
+
<input
|
|
40
|
+
type="email"
|
|
41
|
+
width="100%"
|
|
42
|
+
value={emailFrom}
|
|
43
|
+
onChange={(e) => setEmailFrom(e.target.value)}
|
|
44
|
+
placeholder="From email address (optional)"
|
|
45
|
+
/>
|
|
46
|
+
</p>
|
|
47
|
+
<div className="modal-buttons">
|
|
48
|
+
<button onClick={onClose}>Cancel</button>
|
|
49
|
+
<button onClick={handleSend}>Send</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default EmailModal;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import "./ChatPanel.css"; // Reuse styles or create specific ones
|
|
3
|
+
|
|
4
|
+
interface ToolInfoModalProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
data: { calls: any[]; responses: any[] } | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ToolInfoModal: React.FC<ToolInfoModalProps> = ({
|
|
11
|
+
isOpen,
|
|
12
|
+
onClose,
|
|
13
|
+
data,
|
|
14
|
+
}) => {
|
|
15
|
+
if (!isOpen || !data) return null;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="modal-overlay" onClick={onClose}>
|
|
19
|
+
<div
|
|
20
|
+
className="modal-content tool-info-modal-content"
|
|
21
|
+
onClick={(e) => e.stopPropagation()}
|
|
22
|
+
>
|
|
23
|
+
<div className="tool-info-container">
|
|
24
|
+
<div className="tool-info-section">
|
|
25
|
+
<b>Tool Calls</b>
|
|
26
|
+
<textarea
|
|
27
|
+
className="tool-info-json"
|
|
28
|
+
readOnly
|
|
29
|
+
value={JSON.stringify(data.calls, null, 2)}
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
<div className="tool-info-section">
|
|
33
|
+
<b>Tool Responses</b>
|
|
34
|
+
<textarea
|
|
35
|
+
className="tool-info-json"
|
|
36
|
+
readOnly
|
|
37
|
+
value={JSON.stringify(data.responses, null, 2)}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div className="modal-buttons">
|
|
42
|
+
<button onClick={onClose}>Close</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default ToolInfoModal;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
4
|
+
variant?: 'default' | 'secondary' | 'ghost' | 'outline' | 'destructive';
|
|
5
|
+
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* shadcn-inspired Button component
|
|
11
|
+
*/
|
|
12
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
13
|
+
({ className = '', variant = 'default', size = 'default', children, ...props }, ref) => {
|
|
14
|
+
const baseStyles = `
|
|
15
|
+
ai-button
|
|
16
|
+
inline-flex items-center justify-center
|
|
17
|
+
font-medium transition-colors
|
|
18
|
+
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
|
|
19
|
+
disabled:pointer-events-none disabled:opacity-50
|
|
20
|
+
`.trim().replace(/\s+/g, ' ');
|
|
21
|
+
|
|
22
|
+
const variantStyles: Record<string, string> = {
|
|
23
|
+
default: 'ai-button--primary',
|
|
24
|
+
secondary: 'ai-button--secondary',
|
|
25
|
+
ghost: 'ai-button--ghost',
|
|
26
|
+
outline: 'ai-button--outline',
|
|
27
|
+
destructive: 'ai-button--destructive',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const sizeStyles: Record<string, string> = {
|
|
31
|
+
default: 'ai-button--default',
|
|
32
|
+
sm: 'ai-button--sm',
|
|
33
|
+
lg: 'ai-button--lg',
|
|
34
|
+
icon: 'ai-button--icon',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const classes = [
|
|
38
|
+
baseStyles,
|
|
39
|
+
variantStyles[variant] || variantStyles.default,
|
|
40
|
+
sizeStyles[size] || sizeStyles.default,
|
|
41
|
+
className,
|
|
42
|
+
].filter(Boolean).join(' ');
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<button ref={ref} className={classes} {...props}>
|
|
46
|
+
{children}
|
|
47
|
+
</button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
Button.displayName = 'Button';
|
|
53
|
+
|
|
54
|
+
export default Button;
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface DialogProps {
|
|
4
|
+
isOpen: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* shadcn-inspired Dialog component
|
|
14
|
+
*/
|
|
15
|
+
export const Dialog: React.FC<DialogProps> = ({
|
|
16
|
+
isOpen,
|
|
17
|
+
onClose,
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
children,
|
|
21
|
+
className = '',
|
|
22
|
+
}) => {
|
|
23
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
|
|
25
|
+
// Close on escape key
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
28
|
+
if (event.key === 'Escape' && isOpen) {
|
|
29
|
+
onClose();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
34
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
35
|
+
}, [isOpen, onClose]);
|
|
36
|
+
|
|
37
|
+
// Trap focus within dialog
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!isOpen) return;
|
|
40
|
+
|
|
41
|
+
const dialog = dialogRef.current;
|
|
42
|
+
if (!dialog) return;
|
|
43
|
+
|
|
44
|
+
const focusableElements = dialog.querySelectorAll(
|
|
45
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
46
|
+
);
|
|
47
|
+
const firstFocusable = focusableElements[0] as HTMLElement;
|
|
48
|
+
const lastFocusable = focusableElements[focusableElements.length - 1] as HTMLElement;
|
|
49
|
+
|
|
50
|
+
const handleTabKey = (event: KeyboardEvent) => {
|
|
51
|
+
if (event.key !== 'Tab') return;
|
|
52
|
+
|
|
53
|
+
if (event.shiftKey) {
|
|
54
|
+
if (document.activeElement === firstFocusable) {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
lastFocusable?.focus();
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
if (document.activeElement === lastFocusable) {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
firstFocusable?.focus();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
document.addEventListener('keydown', handleTabKey);
|
|
67
|
+
firstFocusable?.focus();
|
|
68
|
+
|
|
69
|
+
return () => document.removeEventListener('keydown', handleTabKey);
|
|
70
|
+
}, [isOpen]);
|
|
71
|
+
|
|
72
|
+
// Prevent body scroll when open
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (isOpen) {
|
|
75
|
+
document.body.style.overflow = 'hidden';
|
|
76
|
+
} else {
|
|
77
|
+
document.body.style.overflow = '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
document.body.style.overflow = '';
|
|
82
|
+
};
|
|
83
|
+
}, [isOpen]);
|
|
84
|
+
|
|
85
|
+
if (!isOpen) return null;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="ai-dialog-overlay" onClick={onClose}>
|
|
89
|
+
<div
|
|
90
|
+
ref={dialogRef}
|
|
91
|
+
className={`ai-dialog ${className}`}
|
|
92
|
+
role="dialog"
|
|
93
|
+
aria-modal="true"
|
|
94
|
+
aria-labelledby={title ? 'dialog-title' : undefined}
|
|
95
|
+
aria-describedby={description ? 'dialog-description' : undefined}
|
|
96
|
+
onClick={(e) => e.stopPropagation()}
|
|
97
|
+
>
|
|
98
|
+
{title && (
|
|
99
|
+
<div className="ai-dialog-header">
|
|
100
|
+
<h2 id="dialog-title" className="ai-dialog-title">
|
|
101
|
+
{title}
|
|
102
|
+
</h2>
|
|
103
|
+
{description && (
|
|
104
|
+
<p id="dialog-description" className="ai-dialog-description">
|
|
105
|
+
{description}
|
|
106
|
+
</p>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
<div className="ai-dialog-content">{children}</div>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
className="ai-dialog-close"
|
|
114
|
+
onClick={onClose}
|
|
115
|
+
aria-label="Close dialog"
|
|
116
|
+
>
|
|
117
|
+
<svg
|
|
118
|
+
width="16"
|
|
119
|
+
height="16"
|
|
120
|
+
viewBox="0 0 16 16"
|
|
121
|
+
fill="none"
|
|
122
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
123
|
+
>
|
|
124
|
+
<path
|
|
125
|
+
d="M12 4L4 12M4 4L12 12"
|
|
126
|
+
stroke="currentColor"
|
|
127
|
+
strokeWidth="1.5"
|
|
128
|
+
strokeLinecap="round"
|
|
129
|
+
strokeLinejoin="round"
|
|
130
|
+
/>
|
|
131
|
+
</svg>
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export interface DialogFooterProps {
|
|
139
|
+
children: React.ReactNode;
|
|
140
|
+
className?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const DialogFooter: React.FC<DialogFooterProps> = ({
|
|
144
|
+
children,
|
|
145
|
+
className = '',
|
|
146
|
+
}) => {
|
|
147
|
+
return <div className={`ai-dialog-footer ${className}`}>{children}</div>;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export default Dialog;
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
4
|
+
icon?: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* shadcn-inspired Input component
|
|
9
|
+
*/
|
|
10
|
+
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
11
|
+
({ className = '', icon, ...props }, ref) => {
|
|
12
|
+
const baseStyles = 'ai-input';
|
|
13
|
+
const classes = [baseStyles, className].filter(Boolean).join(' ');
|
|
14
|
+
|
|
15
|
+
if (icon) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="ai-input-wrapper">
|
|
18
|
+
<span className="ai-input-icon">{icon}</span>
|
|
19
|
+
<input ref={ref} className={classes} {...props} />
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return <input ref={ref} className={classes} {...props} />;
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
Input.displayName = 'Input';
|
|
29
|
+
|
|
30
|
+
export default Input;
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ScrollAreaProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
maxHeight?: string | number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* shadcn-inspired ScrollArea component with ref forwarding
|
|
11
|
+
*/
|
|
12
|
+
export const ScrollArea = forwardRef<HTMLDivElement, ScrollAreaProps>(
|
|
13
|
+
({ children, className = '', maxHeight }, ref) => {
|
|
14
|
+
const style: React.CSSProperties = maxHeight
|
|
15
|
+
? { maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight }
|
|
16
|
+
: {};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className={`ai-scroll-area ${className}`} style={style} ref={ref}>
|
|
20
|
+
<div className="ai-scroll-area-viewport">{children}</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
ScrollArea.displayName = 'ScrollArea';
|
|
27
|
+
|
|
28
|
+
export default ScrollArea;
|
|
29
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SelectOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SelectProps {
|
|
11
|
+
value: string;
|
|
12
|
+
onChange: (value: string) => void;
|
|
13
|
+
options: SelectOption[];
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* shadcn-inspired Select component
|
|
21
|
+
*/
|
|
22
|
+
export const Select: React.FC<SelectProps> = ({
|
|
23
|
+
value,
|
|
24
|
+
onChange,
|
|
25
|
+
options,
|
|
26
|
+
placeholder = 'Select...',
|
|
27
|
+
disabled = false,
|
|
28
|
+
className = '',
|
|
29
|
+
}) => {
|
|
30
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
31
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
|
|
33
|
+
// Close on outside click
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
36
|
+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
37
|
+
setIsOpen(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
42
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
// Close on escape
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
48
|
+
if (event.key === 'Escape') {
|
|
49
|
+
setIsOpen(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
54
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const selectedOption = options.find((opt) => opt.value === value);
|
|
58
|
+
|
|
59
|
+
const handleSelect = (optionValue: string) => {
|
|
60
|
+
onChange(optionValue);
|
|
61
|
+
setIsOpen(false);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
ref={containerRef}
|
|
67
|
+
className={`ai-select ${disabled ? 'ai-select--disabled' : ''} ${className}`}
|
|
68
|
+
>
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
className="ai-select-trigger"
|
|
72
|
+
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
73
|
+
disabled={disabled}
|
|
74
|
+
aria-haspopup="listbox"
|
|
75
|
+
aria-expanded={isOpen}
|
|
76
|
+
>
|
|
77
|
+
<span className="ai-select-value">
|
|
78
|
+
{selectedOption ? (
|
|
79
|
+
<>
|
|
80
|
+
{selectedOption.icon && (
|
|
81
|
+
<span className="ai-select-icon">{selectedOption.icon}</span>
|
|
82
|
+
)}
|
|
83
|
+
{selectedOption.label}
|
|
84
|
+
</>
|
|
85
|
+
) : (
|
|
86
|
+
<span className="ai-select-placeholder">{placeholder}</span>
|
|
87
|
+
)}
|
|
88
|
+
</span>
|
|
89
|
+
<span className="ai-select-chevron">
|
|
90
|
+
<svg
|
|
91
|
+
width="12"
|
|
92
|
+
height="12"
|
|
93
|
+
viewBox="0 0 12 12"
|
|
94
|
+
fill="none"
|
|
95
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
96
|
+
>
|
|
97
|
+
<path
|
|
98
|
+
d="M2.5 4.5L6 8L9.5 4.5"
|
|
99
|
+
stroke="currentColor"
|
|
100
|
+
strokeWidth="1.5"
|
|
101
|
+
strokeLinecap="round"
|
|
102
|
+
strokeLinejoin="round"
|
|
103
|
+
/>
|
|
104
|
+
</svg>
|
|
105
|
+
</span>
|
|
106
|
+
</button>
|
|
107
|
+
|
|
108
|
+
{isOpen && (
|
|
109
|
+
<div className="ai-select-content" role="listbox">
|
|
110
|
+
{options.map((option) => (
|
|
111
|
+
<button
|
|
112
|
+
key={option.value}
|
|
113
|
+
type="button"
|
|
114
|
+
className={`ai-select-item ${option.value === value ? 'ai-select-item--selected' : ''}`}
|
|
115
|
+
onClick={() => handleSelect(option.value)}
|
|
116
|
+
role="option"
|
|
117
|
+
aria-selected={option.value === value}
|
|
118
|
+
>
|
|
119
|
+
{option.icon && <span className="ai-select-item-icon">{option.icon}</span>}
|
|
120
|
+
<div className="ai-select-item-content">
|
|
121
|
+
<span className="ai-select-item-label">{option.label}</span>
|
|
122
|
+
{option.description && (
|
|
123
|
+
<span className="ai-select-item-description">{option.description}</span>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
{option.value === value && (
|
|
127
|
+
<span className="ai-select-item-check">
|
|
128
|
+
<svg
|
|
129
|
+
width="12"
|
|
130
|
+
height="12"
|
|
131
|
+
viewBox="0 0 12 12"
|
|
132
|
+
fill="none"
|
|
133
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
134
|
+
>
|
|
135
|
+
<path
|
|
136
|
+
d="M2.5 6L5 8.5L9.5 3.5"
|
|
137
|
+
stroke="currentColor"
|
|
138
|
+
strokeWidth="1.5"
|
|
139
|
+
strokeLinecap="round"
|
|
140
|
+
strokeLinejoin="round"
|
|
141
|
+
/>
|
|
142
|
+
</svg>
|
|
143
|
+
</span>
|
|
144
|
+
)}
|
|
145
|
+
</button>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export default Select;
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface TooltipProps {
|
|
4
|
+
content: React.ReactNode;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
7
|
+
delay?: number;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* shadcn-inspired Tooltip component
|
|
13
|
+
*/
|
|
14
|
+
export const Tooltip: React.FC<TooltipProps> = ({
|
|
15
|
+
content,
|
|
16
|
+
children,
|
|
17
|
+
side = 'right',
|
|
18
|
+
delay = 300,
|
|
19
|
+
className = '',
|
|
20
|
+
}) => {
|
|
21
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
22
|
+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
23
|
+
const triggerRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
|
|
25
|
+
const showTooltip = () => {
|
|
26
|
+
timeoutRef.current = setTimeout(() => {
|
|
27
|
+
setIsVisible(true);
|
|
28
|
+
}, delay);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const hideTooltip = () => {
|
|
32
|
+
if (timeoutRef.current) {
|
|
33
|
+
clearTimeout(timeoutRef.current);
|
|
34
|
+
timeoutRef.current = null;
|
|
35
|
+
}
|
|
36
|
+
setIsVisible(false);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
return () => {
|
|
41
|
+
if (timeoutRef.current) {
|
|
42
|
+
clearTimeout(timeoutRef.current);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
ref={triggerRef}
|
|
50
|
+
className={`ai-tooltip-trigger ${className}`}
|
|
51
|
+
onMouseEnter={showTooltip}
|
|
52
|
+
onMouseLeave={hideTooltip}
|
|
53
|
+
onFocus={showTooltip}
|
|
54
|
+
onBlur={hideTooltip}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
{isVisible && (
|
|
58
|
+
<div
|
|
59
|
+
className={`ai-tooltip ai-tooltip--${side}`}
|
|
60
|
+
role="tooltip"
|
|
61
|
+
>
|
|
62
|
+
<div className="ai-tooltip-content">{content}</div>
|
|
63
|
+
<div className="ai-tooltip-arrow" />
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default Tooltip;
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { Button } from './Button';
|
|
2
|
+
export type { ButtonProps } from './Button';
|
|
3
|
+
|
|
4
|
+
export { Input } from './Input';
|
|
5
|
+
export type { InputProps } from './Input';
|
|
6
|
+
|
|
7
|
+
export { Select } from './Select';
|
|
8
|
+
export type { SelectProps, SelectOption } from './Select';
|
|
9
|
+
|
|
10
|
+
export { ScrollArea } from './ScrollArea';
|
|
11
|
+
export type { ScrollAreaProps } from './ScrollArea';
|
|
12
|
+
|
|
13
|
+
export { Tooltip } from './Tooltip';
|
|
14
|
+
export type { TooltipProps } from './Tooltip';
|
|
15
|
+
|
|
16
|
+
export { Dialog, DialogFooter } from './Dialog';
|
|
17
|
+
export type { DialogProps, DialogFooterProps } from './Dialog';
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|