|
1 | | -import type { ChatContext } from '@/stores/panel' |
2 | | - |
3 | | -/** |
4 | | - * Mention folder types |
5 | | - */ |
6 | | -export type MentionFolderId = |
7 | | - | 'chats' |
8 | | - | 'workflows' |
9 | | - | 'knowledge' |
10 | | - | 'blocks' |
11 | | - | 'workflow-blocks' |
12 | | - | 'logs' |
13 | | - | 'integrations' |
14 | | - |
15 | | -/** |
16 | | - * Menu item category types for mention menu (includes folders + docs item) |
17 | | - */ |
18 | | -export type MentionCategory = MentionFolderId | 'docs' |
19 | | - |
20 | | -/** |
21 | | - * Configuration interface for folder types |
22 | | - */ |
23 | | -export interface FolderConfig<TItem = any> { |
24 | | - /** Display title in menu */ |
25 | | - title: string |
26 | | - /** Data source key in useMentionData return */ |
27 | | - dataKey: string |
28 | | - /** Loading state key in useMentionData return */ |
29 | | - loadingKey: string |
30 | | - /** Ensure loaded function key in useMentionData return (optional - some folders auto-load) */ |
31 | | - ensureLoadedKey?: string |
32 | | - /** Extract label from an item */ |
33 | | - getLabel: (item: TItem) => string |
34 | | - /** Extract unique ID from an item */ |
35 | | - getId: (item: TItem) => string |
36 | | - /** Empty state message */ |
37 | | - emptyMessage: string |
38 | | - /** No match message (when filtering) */ |
39 | | - noMatchMessage: string |
40 | | - /** Filter function for matching query */ |
41 | | - filterFn: (item: TItem, query: string) => boolean |
42 | | - /** Build the ChatContext object from an item */ |
43 | | - buildContext: (item: TItem, workflowId?: string | null) => ChatContext |
44 | | - /** Whether to use insertAtCursor fallback when replaceActiveMentionWith fails */ |
45 | | - useInsertFallback?: boolean |
46 | | -} |
47 | | - |
48 | | -/** |
49 | | - * Configuration for all folder types in the mention menu |
50 | | - */ |
51 | | -export const FOLDER_CONFIGS: Record<MentionFolderId, FolderConfig> = { |
52 | | - chats: { |
53 | | - title: 'Chats', |
54 | | - dataKey: 'pastChats', |
55 | | - loadingKey: 'isLoadingPastChats', |
56 | | - ensureLoadedKey: 'ensurePastChatsLoaded', |
57 | | - getLabel: (item) => item.title || 'New Chat', |
58 | | - getId: (item) => item.id, |
59 | | - emptyMessage: 'No past chats', |
60 | | - noMatchMessage: 'No matching chats', |
61 | | - filterFn: (item, q) => (item.title || 'New Chat').toLowerCase().includes(q), |
62 | | - buildContext: (item) => ({ |
63 | | - kind: 'past_chat', |
64 | | - chatId: item.id, |
65 | | - label: item.title || 'New Chat', |
66 | | - }), |
67 | | - useInsertFallback: false, |
68 | | - }, |
69 | | - workflows: { |
70 | | - title: 'All workflows', |
71 | | - dataKey: 'workflows', |
72 | | - loadingKey: 'isLoadingWorkflows', |
73 | | - getLabel: (item) => item.name || 'Untitled Workflow', |
74 | | - getId: (item) => item.id, |
75 | | - emptyMessage: 'No workflows', |
76 | | - noMatchMessage: 'No matching workflows', |
77 | | - filterFn: (item, q) => (item.name || 'Untitled Workflow').toLowerCase().includes(q), |
78 | | - buildContext: (item) => ({ |
79 | | - kind: 'workflow', |
80 | | - workflowId: item.id, |
81 | | - label: item.name || 'Untitled Workflow', |
82 | | - }), |
83 | | - useInsertFallback: true, |
84 | | - }, |
85 | | - knowledge: { |
86 | | - title: 'Knowledge Bases', |
87 | | - dataKey: 'knowledgeBases', |
88 | | - loadingKey: 'isLoadingKnowledge', |
89 | | - ensureLoadedKey: 'ensureKnowledgeLoaded', |
90 | | - getLabel: (item) => item.name || 'Untitled', |
91 | | - getId: (item) => item.id, |
92 | | - emptyMessage: 'No knowledge bases', |
93 | | - noMatchMessage: 'No matching knowledge bases', |
94 | | - filterFn: (item, q) => (item.name || 'Untitled').toLowerCase().includes(q), |
95 | | - buildContext: (item) => ({ |
96 | | - kind: 'knowledge', |
97 | | - knowledgeId: item.id, |
98 | | - label: item.name || 'Untitled', |
99 | | - }), |
100 | | - useInsertFallback: false, |
101 | | - }, |
102 | | - blocks: { |
103 | | - title: 'Blocks', |
104 | | - dataKey: 'blocksList', |
105 | | - loadingKey: 'isLoadingBlocks', |
106 | | - ensureLoadedKey: 'ensureBlocksLoaded', |
107 | | - getLabel: (item) => item.name || item.id, |
108 | | - getId: (item) => item.id, |
109 | | - emptyMessage: 'No blocks found', |
110 | | - noMatchMessage: 'No matching blocks', |
111 | | - filterFn: (item, q) => (item.name || item.id).toLowerCase().includes(q), |
112 | | - buildContext: (item) => ({ |
113 | | - kind: 'blocks', |
114 | | - blockIds: [item.id], |
115 | | - label: item.name || item.id, |
116 | | - }), |
117 | | - useInsertFallback: false, |
118 | | - }, |
119 | | - 'workflow-blocks': { |
120 | | - title: 'Workflow Blocks', |
121 | | - dataKey: 'workflowBlocks', |
122 | | - loadingKey: 'isLoadingWorkflowBlocks', |
123 | | - // No ensureLoadedKey - workflow blocks auto-sync from store |
124 | | - getLabel: (item) => item.name || item.id, |
125 | | - getId: (item) => item.id, |
126 | | - emptyMessage: 'No blocks in this workflow', |
127 | | - noMatchMessage: 'No matching blocks', |
128 | | - filterFn: (item, q) => (item.name || item.id).toLowerCase().includes(q), |
129 | | - buildContext: (item, workflowId) => ({ |
130 | | - kind: 'workflow_block', |
131 | | - workflowId: workflowId || '', |
132 | | - blockId: item.id, |
133 | | - label: item.name || item.id, |
134 | | - }), |
135 | | - useInsertFallback: true, |
136 | | - }, |
137 | | - logs: { |
138 | | - title: 'Logs', |
139 | | - dataKey: 'logsList', |
140 | | - loadingKey: 'isLoadingLogs', |
141 | | - ensureLoadedKey: 'ensureLogsLoaded', |
142 | | - getLabel: (item) => item.workflowName, |
143 | | - getId: (item) => item.id, |
144 | | - emptyMessage: 'No executions found', |
145 | | - noMatchMessage: 'No matching executions', |
146 | | - filterFn: (item, q) => |
147 | | - [item.workflowName, item.trigger || ''].join(' ').toLowerCase().includes(q), |
148 | | - buildContext: (item) => ({ |
149 | | - kind: 'logs', |
150 | | - executionId: item.executionId || item.id, |
151 | | - label: item.workflowName, |
152 | | - }), |
153 | | - useInsertFallback: false, |
154 | | - }, |
155 | | - integrations: { |
156 | | - title: 'Integrations', |
157 | | - dataKey: 'integrations', |
158 | | - loadingKey: 'isLoadingIntegrations', |
159 | | - getLabel: (item) => item.name, |
160 | | - getId: (item) => item.blockType, |
161 | | - emptyMessage: 'No integrations', |
162 | | - noMatchMessage: 'No matching integrations', |
163 | | - filterFn: (item, q) => item.name.toLowerCase().includes(q), |
164 | | - buildContext: (item) => ({ |
165 | | - kind: 'integration', |
166 | | - blockType: item.blockType, |
167 | | - label: item.name, |
168 | | - }), |
169 | | - useInsertFallback: true, |
170 | | - }, |
171 | | -} |
172 | | - |
173 | | -/** |
174 | | - * Order of folders in the mention menu |
175 | | - */ |
176 | | -export const FOLDER_ORDER: MentionFolderId[] = [ |
177 | | - 'chats', |
178 | | - 'workflows', |
179 | | - 'knowledge', |
180 | | - 'blocks', |
181 | | - 'workflow-blocks', |
182 | | - 'integrations', |
183 | | - 'logs', |
184 | | -] |
185 | | - |
186 | | -/** |
187 | | - * Docs item configuration (special case - not a folder) |
188 | | - */ |
189 | | -export const DOCS_CONFIG = { |
190 | | - getLabel: () => 'Docs', |
191 | | - buildContext: (): ChatContext => ({ kind: 'docs', label: 'Docs' }), |
192 | | -} as const |
193 | | - |
194 | | -/** |
195 | | - * Total number of items in root menu (folders + docs) |
196 | | - */ |
197 | | -export const ROOT_MENU_ITEM_COUNT = FOLDER_ORDER.length + 1 |
198 | | - |
199 | | -/** |
200 | | - * Slash command configuration |
201 | | - */ |
202 | | -export interface SlashCommand { |
203 | | - id: string |
204 | | - label: string |
205 | | -} |
206 | | - |
207 | | -export const TOP_LEVEL_COMMANDS: readonly SlashCommand[] = [ |
208 | | - { id: 'fast', label: 'Fast' }, |
209 | | - { id: 'research', label: 'Research' }, |
210 | | - { id: 'actions', label: 'Actions' }, |
211 | | -] as const |
212 | | - |
213 | | -/** |
214 | | - * Maps UI command IDs to API command IDs. |
215 | | - * Some commands have different IDs for display vs API (e.g., "actions" -> "superagent") |
216 | | - */ |
217 | | -export function getApiCommandId(uiCommandId: string): string { |
218 | | - const commandMapping: Record<string, string> = { |
219 | | - actions: 'superagent', |
220 | | - } |
221 | | - return commandMapping[uiCommandId] || uiCommandId |
222 | | -} |
223 | | - |
224 | | -export const WEB_COMMANDS: readonly SlashCommand[] = [ |
225 | | - { id: 'search', label: 'Search' }, |
226 | | - { id: 'read', label: 'Read' }, |
227 | | - { id: 'scrape', label: 'Scrape' }, |
228 | | - { id: 'crawl', label: 'Crawl' }, |
229 | | -] as const |
230 | | - |
231 | | -export const ALL_SLASH_COMMANDS: readonly SlashCommand[] = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS] |
232 | | - |
233 | | -export const ALL_COMMAND_IDS = ALL_SLASH_COMMANDS.map((cmd) => cmd.id) |
234 | | - |
235 | | -/** |
236 | | - * Get display label for a command ID |
237 | | - */ |
238 | | -export function getCommandDisplayLabel(commandId: string): string { |
239 | | - const command = ALL_SLASH_COMMANDS.find((cmd) => cmd.id === commandId) |
240 | | - return command?.label || commandId.charAt(0).toUpperCase() + commandId.slice(1) |
241 | | -} |
242 | | - |
243 | | -/** |
244 | | - * Threshold for considering input "near top" of viewport (in pixels) |
245 | | - */ |
246 | | -export const NEAR_TOP_THRESHOLD = 300 |
247 | | - |
248 | 1 | /** |
249 | 2 | * Scroll tolerance for mention menu positioning (in pixels) |
250 | 3 | */ |
251 | 4 | export const SCROLL_TOLERANCE = 8 |
252 | | - |
253 | | -/** |
254 | | - * Shared CSS classes for menu state text (loading, empty states) |
255 | | - */ |
256 | | -export const MENU_STATE_TEXT_CLASSES = 'px-2 py-2 text-caption text-[var(--text-muted)]' |
257 | | - |
258 | | -/** |
259 | | - * Calculates the next index for circular navigation (wraps around at bounds) |
260 | | - */ |
261 | | -export function getNextIndex(current: number, direction: 'up' | 'down', maxIndex: number): number { |
262 | | - if (direction === 'down') { |
263 | | - return current >= maxIndex ? 0 : current + 1 |
264 | | - } |
265 | | - return current <= 0 ? maxIndex : current - 1 |
266 | | -} |
0 commit comments