@chaaskit/client 0.1.0
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/dist/favicon.svg +11 -0
- package/dist/index.html +17 -0
- package/dist/lib/LoadingSkeletons-IcIC2JPq.js +132 -0
- package/dist/lib/LoadingSkeletons-IcIC2JPq.js.map +1 -0
- package/dist/lib/ServerThemeProvider-DNF0LAyk.js +42 -0
- package/dist/lib/ServerThemeProvider-DNF0LAyk.js.map +1 -0
- package/dist/lib/extensions.js +10 -0
- package/dist/lib/extensions.js.map +1 -0
- package/dist/lib/favicon.svg +11 -0
- package/dist/lib/index.js +74126 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logo.svg +12 -0
- package/dist/lib/routes/AcceptInviteRoute.js +19 -0
- package/dist/lib/routes/AcceptInviteRoute.js.map +1 -0
- package/dist/lib/routes/AdminDashboardRoute.js +19 -0
- package/dist/lib/routes/AdminDashboardRoute.js.map +1 -0
- package/dist/lib/routes/AdminTeamRoute.js +19 -0
- package/dist/lib/routes/AdminTeamRoute.js.map +1 -0
- package/dist/lib/routes/AdminTeamsRoute.js +19 -0
- package/dist/lib/routes/AdminTeamsRoute.js.map +1 -0
- package/dist/lib/routes/AdminUsersRoute.js +19 -0
- package/dist/lib/routes/AdminUsersRoute.js.map +1 -0
- package/dist/lib/routes/ApiKeysRoute.js +19 -0
- package/dist/lib/routes/ApiKeysRoute.js.map +1 -0
- package/dist/lib/routes/AutomationsRoute.js +19 -0
- package/dist/lib/routes/AutomationsRoute.js.map +1 -0
- package/dist/lib/routes/ChatRoute.js +19 -0
- package/dist/lib/routes/ChatRoute.js.map +1 -0
- package/dist/lib/routes/DocumentsRoute.js +19 -0
- package/dist/lib/routes/DocumentsRoute.js.map +1 -0
- package/dist/lib/routes/OAuthConsentRoute.js +19 -0
- package/dist/lib/routes/OAuthConsentRoute.js.map +1 -0
- package/dist/lib/routes/PricingRoute.js +19 -0
- package/dist/lib/routes/PricingRoute.js.map +1 -0
- package/dist/lib/routes/PrivacyRoute.js +19 -0
- package/dist/lib/routes/PrivacyRoute.js.map +1 -0
- package/dist/lib/routes/TeamSettingsRoute.js +19 -0
- package/dist/lib/routes/TeamSettingsRoute.js.map +1 -0
- package/dist/lib/routes/TermsRoute.js +19 -0
- package/dist/lib/routes/TermsRoute.js.map +1 -0
- package/dist/lib/routes/VerifyEmailRoute.js +19 -0
- package/dist/lib/routes/VerifyEmailRoute.js.map +1 -0
- package/dist/lib/routes.js +79 -0
- package/dist/lib/routes.js.map +1 -0
- package/dist/lib/ssr-utils.js +29 -0
- package/dist/lib/ssr-utils.js.map +1 -0
- package/dist/lib/ssr.js +60 -0
- package/dist/lib/ssr.js.map +1 -0
- package/dist/lib/styles.css +2410 -0
- package/dist/lib/useExtensions-B5nX_8XD.js +155 -0
- package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -0
- package/dist/logo.svg +12 -0
- package/package.json +84 -0
- package/src/components/AgentSelector.tsx +90 -0
- package/src/components/BranchModal.tsx +129 -0
- package/src/components/ClientOnly.tsx +27 -0
- package/src/components/ExportMenu.tsx +122 -0
- package/src/components/LoadingSkeletons.tsx +110 -0
- package/src/components/MCPCredentialsSection.tsx +309 -0
- package/src/components/MentionChip.tsx +149 -0
- package/src/components/MentionDropdown.tsx +175 -0
- package/src/components/MentionInput.tsx +293 -0
- package/src/components/MessageItem.tsx +300 -0
- package/src/components/MessageList.tsx +159 -0
- package/src/components/OAuthAppsSection.tsx +124 -0
- package/src/components/ProjectFolder.tsx +141 -0
- package/src/components/ProjectModal.tsx +296 -0
- package/src/components/SSRMessageList.tsx +153 -0
- package/src/components/SearchModal.tsx +173 -0
- package/src/components/SettingsModal.tsx +412 -0
- package/src/components/ShareModal.tsx +280 -0
- package/src/components/Sidebar.tsx +491 -0
- package/src/components/TeamSwitcher.tsx +273 -0
- package/src/components/ToolCallDisplay.tsx +473 -0
- package/src/components/ToolConfirmationModal.tsx +130 -0
- package/src/components/UsageChart.tsx +177 -0
- package/src/components/content/CodeBlock.tsx +69 -0
- package/src/components/content/MarkdownRenderer.tsx +64 -0
- package/src/components/content/SSRMarkdownRenderer.tsx +158 -0
- package/src/contexts/AuthContext.tsx +119 -0
- package/src/contexts/ConfigContext.tsx +214 -0
- package/src/contexts/ProjectContext.tsx +167 -0
- package/src/contexts/ServerConfigProvider.tsx +41 -0
- package/src/contexts/ServerThemeProvider.tsx +47 -0
- package/src/contexts/TeamContext.tsx +255 -0
- package/src/contexts/ThemeContext.tsx +113 -0
- package/src/extensions/index.ts +15 -0
- package/src/extensions/registry.ts +187 -0
- package/src/extensions/useExtensions.ts +52 -0
- package/src/hooks/useAppPath.ts +34 -0
- package/src/hooks/useBasePath.ts +13 -0
- package/src/hooks/useKeyboardShortcuts.ts +50 -0
- package/src/hooks/useMentionSearch.ts +106 -0
- package/src/index.tsx +116 -0
- package/src/layouts/MainLayout.tsx +98 -0
- package/src/pages/AcceptInvitePage.tsx +175 -0
- package/src/pages/AdminDashboardPage.tsx +362 -0
- package/src/pages/AdminTeamPage.tsx +304 -0
- package/src/pages/AdminTeamsPage.tsx +242 -0
- package/src/pages/AdminUsersPage.tsx +385 -0
- package/src/pages/ApiKeysPage.tsx +449 -0
- package/src/pages/ChatPage.tsx +310 -0
- package/src/pages/DocumentsPage.tsx +577 -0
- package/src/pages/LoginPage.tsx +232 -0
- package/src/pages/OAuthConsentPage.tsx +234 -0
- package/src/pages/PricingPage.tsx +314 -0
- package/src/pages/PrivacyPage.tsx +65 -0
- package/src/pages/RegisterPage.tsx +153 -0
- package/src/pages/ScheduledPromptsPage.tsx +702 -0
- package/src/pages/SharedThreadPage.tsx +116 -0
- package/src/pages/TeamSettingsPage.tsx +1085 -0
- package/src/pages/TermsPage.tsx +82 -0
- package/src/pages/VerifyEmailPage.tsx +202 -0
- package/src/routes/AcceptInviteRoute.tsx +24 -0
- package/src/routes/AdminDashboardRoute.tsx +24 -0
- package/src/routes/AdminTeamRoute.tsx +24 -0
- package/src/routes/AdminTeamsRoute.tsx +24 -0
- package/src/routes/AdminUsersRoute.tsx +24 -0
- package/src/routes/ApiKeysRoute.tsx +24 -0
- package/src/routes/AutomationsRoute.tsx +24 -0
- package/src/routes/ChatRoute.tsx +28 -0
- package/src/routes/DocumentsRoute.tsx +24 -0
- package/src/routes/OAuthConsentRoute.tsx +24 -0
- package/src/routes/PricingRoute.tsx +24 -0
- package/src/routes/PrivacyRoute.tsx +24 -0
- package/src/routes/TeamSettingsRoute.tsx +24 -0
- package/src/routes/TermsRoute.tsx +24 -0
- package/src/routes/VerifyEmailRoute.tsx +24 -0
- package/src/routes/index.ts +57 -0
- package/src/ssr-utils.tsx +84 -0
- package/src/ssr.ts +123 -0
- package/src/stores/chatStore.ts +670 -0
- package/src/styles/index.css +254 -0
- package/src/utils/api.ts +78 -0
- package/src/vite-env.d.ts +13 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
|
|
2
|
+
import { FileText, Users, Folder, Loader2 } from 'lucide-react';
|
|
3
|
+
import type { MentionableDocument, DocumentScope } from '@chaaskit/shared';
|
|
4
|
+
|
|
5
|
+
interface MentionDropdownProps {
|
|
6
|
+
documents: MentionableDocument[];
|
|
7
|
+
grouped?: {
|
|
8
|
+
my: MentionableDocument[];
|
|
9
|
+
team: MentionableDocument[];
|
|
10
|
+
project: MentionableDocument[];
|
|
11
|
+
};
|
|
12
|
+
isLoading?: boolean;
|
|
13
|
+
selectedIndex: number;
|
|
14
|
+
onSelect: (doc: MentionableDocument) => void;
|
|
15
|
+
position: { top: number; left: number };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MentionDropdownHandle {
|
|
19
|
+
scrollToSelected: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getScopeIcon(scope: DocumentScope) {
|
|
23
|
+
switch (scope) {
|
|
24
|
+
case 'my':
|
|
25
|
+
return <FileText size={14} className="text-text-muted" />;
|
|
26
|
+
case 'team':
|
|
27
|
+
return <Users size={14} className="text-text-muted" />;
|
|
28
|
+
case 'project':
|
|
29
|
+
return <Folder size={14} className="text-text-muted" />;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getScopeLabel(scope: DocumentScope) {
|
|
34
|
+
switch (scope) {
|
|
35
|
+
case 'my':
|
|
36
|
+
return 'My Documents';
|
|
37
|
+
case 'team':
|
|
38
|
+
return 'Team';
|
|
39
|
+
case 'project':
|
|
40
|
+
return 'Project';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatCharCount(count: number): string {
|
|
45
|
+
if (count < 1000) return `${count} chars`;
|
|
46
|
+
return `${(count / 1000).toFixed(1)}k chars`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MentionDropdown = forwardRef<MentionDropdownHandle, MentionDropdownProps>(
|
|
50
|
+
function MentionDropdown(
|
|
51
|
+
{ documents, grouped, isLoading, selectedIndex, onSelect, position },
|
|
52
|
+
ref
|
|
53
|
+
) {
|
|
54
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
55
|
+
const itemRefs = useRef<Map<number, HTMLButtonElement>>(new Map());
|
|
56
|
+
|
|
57
|
+
useImperativeHandle(ref, () => ({
|
|
58
|
+
scrollToSelected: () => {
|
|
59
|
+
const selectedItem = itemRefs.current.get(selectedIndex);
|
|
60
|
+
if (selectedItem) {
|
|
61
|
+
selectedItem.scrollIntoView({ block: 'nearest' });
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const selectedItem = itemRefs.current.get(selectedIndex);
|
|
68
|
+
if (selectedItem) {
|
|
69
|
+
selectedItem.scrollIntoView({ block: 'nearest' });
|
|
70
|
+
}
|
|
71
|
+
}, [selectedIndex]);
|
|
72
|
+
|
|
73
|
+
// Render grouped or flat list
|
|
74
|
+
const renderDocuments = () => {
|
|
75
|
+
if (grouped && (grouped.my.length > 0 || grouped.team.length > 0 || grouped.project.length > 0)) {
|
|
76
|
+
let flatIndex = 0;
|
|
77
|
+
const sections: JSX.Element[] = [];
|
|
78
|
+
|
|
79
|
+
const renderSection = (scope: DocumentScope, docs: MentionableDocument[]) => {
|
|
80
|
+
if (docs.length === 0) return null;
|
|
81
|
+
|
|
82
|
+
const sectionItems = docs.map((doc) => {
|
|
83
|
+
const index = flatIndex++;
|
|
84
|
+
return (
|
|
85
|
+
<button
|
|
86
|
+
key={doc.id}
|
|
87
|
+
ref={(el) => {
|
|
88
|
+
if (el) itemRefs.current.set(index, el);
|
|
89
|
+
else itemRefs.current.delete(index);
|
|
90
|
+
}}
|
|
91
|
+
onClick={() => onSelect(doc)}
|
|
92
|
+
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-background-secondary ${
|
|
93
|
+
selectedIndex === index ? 'bg-background-secondary' : ''
|
|
94
|
+
}`}
|
|
95
|
+
>
|
|
96
|
+
{getScopeIcon(doc.scope)}
|
|
97
|
+
<span className="flex-1 truncate text-text-primary">{doc.name}</span>
|
|
98
|
+
<span className="text-xs text-text-muted">{formatCharCount(doc.charCount)}</span>
|
|
99
|
+
</button>
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div key={scope}>
|
|
105
|
+
<div className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-text-muted">
|
|
106
|
+
{getScopeIcon(scope)}
|
|
107
|
+
{getScopeLabel(scope)}
|
|
108
|
+
{scope !== 'my' && docs[0]?.scopeName && (
|
|
109
|
+
<span className="text-text-secondary">: {docs[0].scopeName}</span>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
{sectionItems}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (grouped.my.length > 0) {
|
|
118
|
+
sections.push(renderSection('my', grouped.my)!);
|
|
119
|
+
}
|
|
120
|
+
if (grouped.team.length > 0) {
|
|
121
|
+
sections.push(renderSection('team', grouped.team)!);
|
|
122
|
+
}
|
|
123
|
+
if (grouped.project.length > 0) {
|
|
124
|
+
sections.push(renderSection('project', grouped.project)!);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return sections;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Flat list fallback
|
|
131
|
+
return documents.map((doc, index) => (
|
|
132
|
+
<button
|
|
133
|
+
key={doc.id}
|
|
134
|
+
ref={(el) => {
|
|
135
|
+
if (el) itemRefs.current.set(index, el);
|
|
136
|
+
else itemRefs.current.delete(index);
|
|
137
|
+
}}
|
|
138
|
+
onClick={() => onSelect(doc)}
|
|
139
|
+
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-background-secondary ${
|
|
140
|
+
selectedIndex === index ? 'bg-background-secondary' : ''
|
|
141
|
+
}`}
|
|
142
|
+
>
|
|
143
|
+
{getScopeIcon(doc.scope)}
|
|
144
|
+
<span className="flex-1 truncate text-text-primary">{doc.name}</span>
|
|
145
|
+
<span className="text-xs text-text-muted">{formatCharCount(doc.charCount)}</span>
|
|
146
|
+
</button>
|
|
147
|
+
));
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div
|
|
152
|
+
ref={containerRef}
|
|
153
|
+
className="fixed z-50 max-h-64 min-w-[240px] max-w-[320px] overflow-y-auto rounded-lg border border-border bg-background shadow-lg"
|
|
154
|
+
style={{
|
|
155
|
+
top: position.top,
|
|
156
|
+
left: position.left,
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
{isLoading ? (
|
|
160
|
+
<div className="flex items-center justify-center py-4">
|
|
161
|
+
<Loader2 size={20} className="animate-spin text-text-muted" />
|
|
162
|
+
</div>
|
|
163
|
+
) : documents.length === 0 ? (
|
|
164
|
+
<div className="px-3 py-4 text-center text-sm text-text-muted">
|
|
165
|
+
No documents found
|
|
166
|
+
</div>
|
|
167
|
+
) : (
|
|
168
|
+
renderDocuments()
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
export default MentionDropdown;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
useCallback,
|
|
6
|
+
forwardRef,
|
|
7
|
+
useImperativeHandle,
|
|
8
|
+
type ChangeEvent,
|
|
9
|
+
type KeyboardEvent,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { createPortal } from 'react-dom';
|
|
12
|
+
import type { MentionableDocument } from '@chaaskit/shared';
|
|
13
|
+
import { useMentionSearch } from '../hooks/useMentionSearch';
|
|
14
|
+
import MentionDropdown, { type MentionDropdownHandle } from './MentionDropdown';
|
|
15
|
+
import { useConfig } from '../contexts/ConfigContext';
|
|
16
|
+
|
|
17
|
+
interface MentionInputProps {
|
|
18
|
+
value: string;
|
|
19
|
+
onChange: (value: string) => void;
|
|
20
|
+
onKeyDown?: (e: KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
rows?: number;
|
|
25
|
+
maxHeight?: number;
|
|
26
|
+
autoGrow?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MentionInputHandle {
|
|
30
|
+
focus: () => void;
|
|
31
|
+
blur: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MentionInput = forwardRef<MentionInputHandle, MentionInputProps>(function MentionInput(
|
|
35
|
+
{
|
|
36
|
+
value,
|
|
37
|
+
onChange,
|
|
38
|
+
onKeyDown,
|
|
39
|
+
placeholder,
|
|
40
|
+
disabled,
|
|
41
|
+
className = '',
|
|
42
|
+
rows = 1,
|
|
43
|
+
maxHeight = 200,
|
|
44
|
+
autoGrow = true,
|
|
45
|
+
},
|
|
46
|
+
ref
|
|
47
|
+
) {
|
|
48
|
+
const config = useConfig();
|
|
49
|
+
const documentsEnabled = config.documents?.enabled ?? false;
|
|
50
|
+
|
|
51
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
52
|
+
const dropdownRef = useRef<MentionDropdownHandle>(null);
|
|
53
|
+
|
|
54
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
55
|
+
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
|
56
|
+
const [mentionQuery, setMentionQuery] = useState('');
|
|
57
|
+
const [mentionStartIndex, setMentionStartIndex] = useState(-1);
|
|
58
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
59
|
+
|
|
60
|
+
const { search, clearResults, results, isSearching } = useMentionSearch();
|
|
61
|
+
|
|
62
|
+
// Expose methods to parent
|
|
63
|
+
useImperativeHandle(ref, () => ({
|
|
64
|
+
focus: () => textareaRef.current?.focus(),
|
|
65
|
+
blur: () => textareaRef.current?.blur(),
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
// Auto-grow textarea
|
|
69
|
+
const adjustHeight = useCallback(() => {
|
|
70
|
+
if (!autoGrow || !textareaRef.current) return;
|
|
71
|
+
const textarea = textareaRef.current;
|
|
72
|
+
textarea.style.height = 'auto';
|
|
73
|
+
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
|
|
74
|
+
}, [autoGrow, maxHeight]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
adjustHeight();
|
|
78
|
+
}, [value, adjustHeight]);
|
|
79
|
+
|
|
80
|
+
// Calculate dropdown position based on cursor
|
|
81
|
+
const updateDropdownPosition = useCallback(() => {
|
|
82
|
+
if (!textareaRef.current) return;
|
|
83
|
+
|
|
84
|
+
const textarea = textareaRef.current;
|
|
85
|
+
const rect = textarea.getBoundingClientRect();
|
|
86
|
+
|
|
87
|
+
// Create a temporary span to measure text position
|
|
88
|
+
const span = document.createElement('span');
|
|
89
|
+
span.style.font = getComputedStyle(textarea).font;
|
|
90
|
+
span.style.visibility = 'hidden';
|
|
91
|
+
span.style.position = 'absolute';
|
|
92
|
+
span.style.whiteSpace = 'pre-wrap';
|
|
93
|
+
span.style.wordWrap = 'break-word';
|
|
94
|
+
span.style.width = `${textarea.clientWidth}px`;
|
|
95
|
+
|
|
96
|
+
// Get text up to cursor
|
|
97
|
+
const textBeforeCursor = value.substring(0, textarea.selectionStart);
|
|
98
|
+
span.textContent = textBeforeCursor;
|
|
99
|
+
document.body.appendChild(span);
|
|
100
|
+
|
|
101
|
+
// Calculate approximate position
|
|
102
|
+
const lines = textBeforeCursor.split('\n');
|
|
103
|
+
const lineHeight = parseInt(getComputedStyle(textarea).lineHeight) || 20;
|
|
104
|
+
const currentLineIndex = lines.length - 1;
|
|
105
|
+
|
|
106
|
+
document.body.removeChild(span);
|
|
107
|
+
|
|
108
|
+
// Position dropdown below the current line
|
|
109
|
+
const top = rect.top + (currentLineIndex + 1) * lineHeight + 4;
|
|
110
|
+
const left = rect.left;
|
|
111
|
+
|
|
112
|
+
// Ensure dropdown doesn't go off-screen
|
|
113
|
+
const viewportHeight = window.innerHeight;
|
|
114
|
+
const dropdownHeight = 256; // max-h-64 = 16rem = 256px
|
|
115
|
+
const adjustedTop = top + dropdownHeight > viewportHeight ? rect.top - dropdownHeight - 4 : top;
|
|
116
|
+
|
|
117
|
+
setDropdownPosition({
|
|
118
|
+
top: adjustedTop,
|
|
119
|
+
left: Math.max(8, Math.min(left, window.innerWidth - 328)), // 320px width + 8px margin
|
|
120
|
+
});
|
|
121
|
+
}, [value]);
|
|
122
|
+
|
|
123
|
+
// Parse for @ mentions as user types
|
|
124
|
+
const handleChange = useCallback(
|
|
125
|
+
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
126
|
+
const newValue = e.target.value;
|
|
127
|
+
const cursorPos = e.target.selectionStart;
|
|
128
|
+
|
|
129
|
+
onChange(newValue);
|
|
130
|
+
|
|
131
|
+
if (!documentsEnabled) return;
|
|
132
|
+
|
|
133
|
+
// Find the @ that might be starting a mention
|
|
134
|
+
const textBeforeCursor = newValue.substring(0, cursorPos);
|
|
135
|
+
const atMatch = textBeforeCursor.match(/@(\S*)$/);
|
|
136
|
+
|
|
137
|
+
if (atMatch) {
|
|
138
|
+
const query = atMatch[1];
|
|
139
|
+
const startIndex = textBeforeCursor.length - atMatch[0].length;
|
|
140
|
+
|
|
141
|
+
setMentionStartIndex(startIndex);
|
|
142
|
+
setMentionQuery(query);
|
|
143
|
+
setSelectedIndex(0);
|
|
144
|
+
setShowDropdown(true);
|
|
145
|
+
updateDropdownPosition();
|
|
146
|
+
|
|
147
|
+
// Search with the query (after the @)
|
|
148
|
+
search(query || '');
|
|
149
|
+
} else {
|
|
150
|
+
// No @ mention in progress
|
|
151
|
+
if (showDropdown) {
|
|
152
|
+
setShowDropdown(false);
|
|
153
|
+
clearResults();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
[onChange, documentsEnabled, showDropdown, search, clearResults, updateDropdownPosition]
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Handle keyboard navigation in dropdown
|
|
161
|
+
const handleKeyDown = useCallback(
|
|
162
|
+
(e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
163
|
+
if (showDropdown && results?.documents.length) {
|
|
164
|
+
const docCount = results.documents.length;
|
|
165
|
+
|
|
166
|
+
if (e.key === 'ArrowDown') {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
setSelectedIndex((prev) => (prev + 1) % docCount);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (e.key === 'ArrowUp') {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
setSelectedIndex((prev) => (prev - 1 + docCount) % docCount);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
const selectedDoc = results.documents[selectedIndex];
|
|
181
|
+
if (selectedDoc) {
|
|
182
|
+
insertMention(selectedDoc);
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (e.key === 'Escape') {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
setShowDropdown(false);
|
|
190
|
+
clearResults();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Pass through to parent handler
|
|
196
|
+
onKeyDown?.(e);
|
|
197
|
+
},
|
|
198
|
+
[showDropdown, results, selectedIndex, onKeyDown, clearResults]
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Insert the selected mention
|
|
202
|
+
const insertMention = useCallback(
|
|
203
|
+
(doc: MentionableDocument) => {
|
|
204
|
+
if (mentionStartIndex < 0) return;
|
|
205
|
+
|
|
206
|
+
const textarea = textareaRef.current;
|
|
207
|
+
if (!textarea) return;
|
|
208
|
+
|
|
209
|
+
const cursorPos = textarea.selectionStart;
|
|
210
|
+
const beforeMention = value.substring(0, mentionStartIndex);
|
|
211
|
+
const afterMention = value.substring(cursorPos);
|
|
212
|
+
|
|
213
|
+
// Insert the full path
|
|
214
|
+
const mentionText = doc.path;
|
|
215
|
+
const newValue = beforeMention + mentionText + ' ' + afterMention;
|
|
216
|
+
|
|
217
|
+
onChange(newValue);
|
|
218
|
+
|
|
219
|
+
// Move cursor after the inserted mention
|
|
220
|
+
const newCursorPos = mentionStartIndex + mentionText.length + 1;
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
textarea.selectionStart = newCursorPos;
|
|
223
|
+
textarea.selectionEnd = newCursorPos;
|
|
224
|
+
textarea.focus();
|
|
225
|
+
}, 0);
|
|
226
|
+
|
|
227
|
+
setShowDropdown(false);
|
|
228
|
+
clearResults();
|
|
229
|
+
},
|
|
230
|
+
[value, mentionStartIndex, onChange, clearResults]
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Handle document selection from dropdown
|
|
234
|
+
const handleSelectDocument = useCallback(
|
|
235
|
+
(doc: MentionableDocument) => {
|
|
236
|
+
insertMention(doc);
|
|
237
|
+
},
|
|
238
|
+
[insertMention]
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Close dropdown when clicking outside
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
function handleClickOutside(e: MouseEvent) {
|
|
244
|
+
if (
|
|
245
|
+
showDropdown &&
|
|
246
|
+
textareaRef.current &&
|
|
247
|
+
!textareaRef.current.contains(e.target as Node)
|
|
248
|
+
) {
|
|
249
|
+
setShowDropdown(false);
|
|
250
|
+
clearResults();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
255
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
256
|
+
}, [showDropdown, clearResults]);
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<>
|
|
260
|
+
<textarea
|
|
261
|
+
ref={textareaRef}
|
|
262
|
+
value={value}
|
|
263
|
+
onChange={handleChange}
|
|
264
|
+
onKeyDown={handleKeyDown}
|
|
265
|
+
placeholder={placeholder}
|
|
266
|
+
disabled={disabled}
|
|
267
|
+
rows={rows}
|
|
268
|
+
className={className}
|
|
269
|
+
style={{
|
|
270
|
+
height: 'auto',
|
|
271
|
+
minHeight: `${rows * 22 + 22}px`,
|
|
272
|
+
}}
|
|
273
|
+
onInput={adjustHeight}
|
|
274
|
+
/>
|
|
275
|
+
|
|
276
|
+
{showDropdown &&
|
|
277
|
+
createPortal(
|
|
278
|
+
<MentionDropdown
|
|
279
|
+
ref={dropdownRef}
|
|
280
|
+
documents={results?.documents ?? []}
|
|
281
|
+
grouped={results?.grouped}
|
|
282
|
+
isLoading={isSearching}
|
|
283
|
+
selectedIndex={selectedIndex}
|
|
284
|
+
onSelect={handleSelectDocument}
|
|
285
|
+
position={dropdownPosition}
|
|
286
|
+
/>,
|
|
287
|
+
document.body
|
|
288
|
+
)}
|
|
289
|
+
</>
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
export default MentionInput;
|