Skip to content

Commit 75031b4

Browse files
authored
feat(toolkit): add basic search funcitonality (#740)
* feat(toolkit): add basic search funcitonality * feat(toolkit): add basic search funcitonality * remove unused code * improve search
1 parent 9887e5a commit 75031b4

File tree

14 files changed

+538
-274
lines changed

14 files changed

+538
-274
lines changed

src/interfaces/assistants_web/src/components/HotKeys/CommandAction.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,35 @@ import { ComboboxOption } from '@headlessui/react';
44
import { useMemo } from 'react';
55
import { useHotkeys } from 'react-hotkeys-hook';
66

7-
import { type QuickAction } from '@/components/HotKeys';
7+
import { QuickAction } from '@/components/HotKeys';
88
import { Text } from '@/components/UI';
99
import { useOS } from '@/hooks';
1010
import { cn } from '@/utils';
1111

12-
interface Props extends QuickAction {
12+
interface Props {
13+
hotkey: QuickAction;
1314
isOpen: boolean;
1415
}
1516

16-
export const CommandAction: React.FC<Props> = ({ label, name, commands, action, isOpen }) => {
17+
export const CommandAction: React.FC<Props> = ({ isOpen, hotkey }) => {
1718
const os = useOS();
1819

1920
useHotkeys(
20-
commands,
21+
hotkey.commands,
2122
(e) => {
2223
if (!isOpen) return;
2324
e.preventDefault();
24-
action?.();
25+
hotkey.action?.();
2526
},
2627
{
2728
enableOnFormTags: true,
2829
},
29-
[isOpen, action]
30+
[isOpen, hotkey.action]
3031
);
3132

3233
const formattedCommands = useMemo(() => {
33-
if (commands.length === 0) return [];
34-
const [command] = commands;
34+
if (hotkey.commands.length === 0) return [];
35+
const [command] = hotkey.commands;
3536
return command.split('+').map((key) => {
3637
if (key === 'meta') return os === 'macOS' ? '⌘' : 'win';
3738
if (key === 'alt') return os === 'macOS' ? '⌥' : 'alt';
@@ -40,13 +41,13 @@ export const CommandAction: React.FC<Props> = ({ label, name, commands, action,
4041
if (key.length === 1) return key.toUpperCase();
4142
return key;
4243
});
43-
}, [commands, os]);
44+
}, [hotkey.commands, os]);
4445

4546
return (
4647
<>
4748
<ComboboxOption
48-
key={name}
49-
value={name}
49+
key={hotkey.name}
50+
value={hotkey}
5051
className={cn(
5152
'flex select-none items-center px-6 py-4',
5253
'data-[focus]:bg-volcanic-900 data-[focus]:dark:bg-volcanic-300'
@@ -60,7 +61,7 @@ export const CommandAction: React.FC<Props> = ({ label, name, commands, action,
6061
'text-volcanic-200 dark:text-marble-1000': focus,
6162
})}
6263
>
63-
{label ?? name}
64+
{hotkey.label ?? hotkey.name}
6465
</Text>
6566
<span className="flex gap-x-1">
6667
{formattedCommands.map((key) => (

src/interfaces/assistants_web/src/components/HotKeys/CommandActionGroup.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export const CommandActionGroup: React.FC<Props> = ({ isOpen, options = [] }) =>
1212
return (
1313
<div className="flex flex-col gap-y-4">
1414
{options.map((action, i) => {
15+
if (action.quickActions.length === 0) {
16+
return null;
17+
}
1518
return (
1619
<section key={i} className="flex flex-col">
1720
{action.group && (
@@ -25,7 +28,7 @@ export const CommandActionGroup: React.FC<Props> = ({ isOpen, options = [] }) =>
2528
{action.quickActions
2629
.filter((quickAction) => quickAction.displayInDialog !== false)
2730
.map((quickAction) => (
28-
<CommandAction key={quickAction.name} isOpen={isOpen} {...quickAction} />
31+
<CommandAction key={quickAction.name} isOpen={isOpen} hotkey={quickAction} />
2932
))}
3033
</section>
3134
);
Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
'use client';
22

33
import { HotKeysProvider } from '@/components/HotKeys';
4-
import {
5-
useAssistantHotKeys,
6-
useConversationHotKeys,
7-
useSettingsHotKeys,
8-
useViewHotKeys,
9-
} from '@/hooks';
4+
import { useAssistantHotKeys } from '@/components/HotKeys/hotkeys/assistants';
5+
import { useConversationHotKeys } from '@/components/HotKeys/hotkeys/conversation';
6+
import { useSearchHotKeys } from '@/components/HotKeys/hotkeys/search';
7+
import { useSettingsHotKeys } from '@/components/HotKeys/hotkeys/settings';
8+
import { useViewHotKeys } from '@/components/HotKeys/hotkeys/view';
109

1110
export const HotKeys: React.FC = () => {
11+
const searchHotKeys = useSearchHotKeys();
1212
const conversationHotKeys = useConversationHotKeys();
1313
const viewHotKeys = useViewHotKeys();
1414
const settingsHotKeys = useSettingsHotKeys();
1515
const assistantHotKeys = useAssistantHotKeys({ displayRecentAgentsInDialog: false });
16-
const hotKeys = [...conversationHotKeys, ...viewHotKeys, ...assistantHotKeys, ...settingsHotKeys];
16+
const hotKeys = [
17+
...searchHotKeys,
18+
...conversationHotKeys,
19+
...viewHotKeys,
20+
...assistantHotKeys,
21+
...settingsHotKeys,
22+
];
1723

1824
return <HotKeysProvider hotKeys={hotKeys} />;
1925
};

src/interfaces/assistants_web/src/components/HotKeys/HotKeysDialog.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Fragment, useEffect, useMemo, useState } from 'react';
1212

1313
import {
1414
CommandActionGroup,
15+
CustomHotKey,
1516
type HotKeyGroupOption,
1617
HotKeysDialogInput,
1718
} from '@/components/HotKeys';
@@ -38,6 +39,10 @@ export const HotKeysDialog: React.FC<Props> = ({ isOpen, close, options = [] })
3839
const filteredCustomActions = useMemo(() => {
3940
if (query === '') return [];
4041
if (query === '?') return options;
42+
if (query === '$') {
43+
setCustomView('Search');
44+
return [];
45+
}
4146

4247
const queryWords = query.toLowerCase().split(' ');
4348
return options
@@ -53,11 +58,7 @@ export const HotKeysDialog: React.FC<Props> = ({ isOpen, close, options = [] })
5358
.filter((action) => action.quickActions.length > 0);
5459
}, [query, options]);
5560

56-
const handleOnChange = (command: string | null) => {
57-
const hotkey = options
58-
.flatMap((option) => option.quickActions)
59-
.find((option) => option.name === command);
60-
61+
const handleOnChange = (hotkey?: CustomHotKey) => {
6162
if (hotkey) {
6263
if (hotkey.closeDialogOnRun) {
6364
close();
@@ -113,7 +114,7 @@ export const HotKeysDialog: React.FC<Props> = ({ isOpen, close, options = [] })
113114
)}
114115
>
115116
{View ? (
116-
<View close={close} onBack={() => setCustomView(null)} />
117+
<View isOpen={isOpen} close={close} onBack={() => setCustomView(null)} />
117118
) : (
118119
<>
119120
<HotKeysDialogInput
@@ -122,7 +123,7 @@ export const HotKeysDialog: React.FC<Props> = ({ isOpen, close, options = [] })
122123
close={close}
123124
placeholder="Find a command"
124125
/>
125-
<ComboboxOptions className="flex flex-col gap-y-6 overflow-y-auto pb-3" static>
126+
<ComboboxOptions className="mb-3 flex flex-col gap-y-6 overflow-y-auto" static>
126127
{filteredCustomActions.length > 0 && (
127128
<CommandActionGroup isOpen={isOpen} options={filteredCustomActions} />
128129
)}

src/interfaces/assistants_web/src/components/HotKeys/HotKeysDialogInput.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,56 @@
11
import { ComboboxInput } from '@headlessui/react';
22

3-
import { Icon, Input } from '@/components/UI';
3+
import { Icon, Input, Text } from '@/components/UI';
44

55
type Props = {
66
value: string;
7-
setValue?: (query: string) => void;
8-
close: VoidFunction;
9-
onBack?: VoidFunction;
107
readOnly?: boolean;
118
placeholder?: string;
9+
isSearch?: boolean;
10+
onBack?: VoidFunction;
11+
setValue?: (query: string) => void;
12+
close: VoidFunction;
1213
};
1314

1415
export const HotKeysDialogInput: React.FC<Props> = ({
1516
value,
1617
setValue,
1718
close,
19+
isSearch,
1820
readOnly,
1921
onBack,
22+
placeholder,
2023
}) => {
2124
return (
22-
<div className="mx-6 mb-3 mt-6 ">
23-
<span className="flex items-center gap-x-2">
24-
{onBack && (
25+
<div className="mx-6 mb-3 mt-6">
26+
<span className="flex items-center gap-x-2 [&>div]:flex-grow">
27+
{onBack && !isSearch && (
2528
<button onClick={onBack}>
2629
<Icon name="arrow-left" />
2730
</button>
2831
)}
32+
{isSearch && (
33+
<Text styleAs="p-sm" className="w-fit rounded-lg bg-volcanic-900 p-2 dark:bg-volcanic-60">
34+
$
35+
</Text>
36+
)}
2937
<ComboboxInput
3038
as={Input}
3139
autoComplete="off"
32-
placeholder="Find a command."
40+
placeholder={placeholder}
3341
value={value}
3442
onChange={(event) => setValue?.(event.target.value)}
3543
onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
3644
if (event.key === 'Escape') {
3745
close();
3846
}
39-
if (event.key === 'Backspace' || event.key === 'Delete') {
47+
if ((event.key === 'Backspace' || event.key === 'Delete') && (!value || readOnly)) {
4048
onBack?.();
4149
}
4250
}}
4351
readOnly={readOnly}
4452
autoFocus
45-
className="border-none bg-transparent py-0 text-p-lg focus:bg-transparent dark:bg-transparent dark:focus:bg-transparent"
53+
className="w-full flex-grow border-none bg-transparent py-0 text-p-lg focus:bg-transparent dark:bg-transparent dark:focus:bg-transparent"
4654
/>
4755
</span>
4856
<hr className="mt-6 border-t border-volcanic-800 dark:border-volcanic-400" />
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
'use client';
2+
3+
import { ComboboxOptions } from '@headlessui/react';
4+
import { useDebouncedEffect } from '@react-hookz/web';
5+
import { useRouter } from 'next/navigation';
6+
import { useState } from 'react';
7+
8+
import { AgentLogo } from '@/components/Agents/AgentLogo';
9+
import { CommandActionGroup, HotKeyGroupOption, HotKeysDialogInput } from '@/components/HotKeys';
10+
import { BASE_AGENT } from '@/constants';
11+
import { useConversations, useListAgents } from '@/hooks';
12+
13+
type Props = {
14+
isOpen: boolean;
15+
close: VoidFunction;
16+
onBack: VoidFunction;
17+
};
18+
19+
export const Search: React.FC<Props> = ({ isOpen, close, onBack }) => {
20+
const [query, setQuery] = useState('');
21+
const [results, setResults] = useState<HotKeyGroupOption[]>([
22+
{
23+
group: 'Conversations',
24+
quickActions: [],
25+
},
26+
27+
{
28+
group: 'Assistants',
29+
quickActions: [],
30+
},
31+
]);
32+
const router = useRouter();
33+
34+
const { data: assistants } = useListAgents();
35+
const { data: conversations } = useConversations({});
36+
37+
useDebouncedEffect(
38+
() => {
39+
if (!query) {
40+
setResults([
41+
{
42+
group: 'Conversations',
43+
quickActions: [],
44+
},
45+
{
46+
group: 'Assistants',
47+
quickActions: [],
48+
},
49+
]);
50+
return;
51+
}
52+
53+
const foundConversations = conversations?.filter((conversation) => {
54+
const title = conversation.title.toLowerCase();
55+
const description = conversation.description?.toLowerCase();
56+
return title.includes(query) || (description && description.includes(query));
57+
});
58+
59+
const foundAssistants = assistants?.filter((assistant) => {
60+
const name = assistant.name.toLowerCase();
61+
const description = assistant.description?.toLowerCase();
62+
const preamble = assistant.preamble?.toLowerCase();
63+
64+
return (
65+
name.includes(query) ||
66+
(description && description.includes(query)) ||
67+
(preamble && preamble.includes(query))
68+
);
69+
});
70+
71+
setResults([
72+
{
73+
group: 'Conversations',
74+
quickActions:
75+
foundConversations?.map((conversation) => ({
76+
name: conversation.title,
77+
label: (
78+
<div className="flex items-center gap-x-2">
79+
<AgentLogo
80+
agent={
81+
assistants?.find((assistant) => assistant.id === conversation.agent_id) ??
82+
BASE_AGENT
83+
}
84+
/>
85+
<span>{conversation.title}</span>
86+
<span className="ml-2 truncate text-p-sm text-volcanic-600">
87+
{conversation.description}
88+
</span>
89+
</div>
90+
),
91+
action: () => {
92+
if (conversation.agent_id) {
93+
router.push(`/a/${conversation.agent_id}/c/${conversation.id}`);
94+
} else {
95+
router.push(`/c/${conversation.id}`);
96+
}
97+
},
98+
closeDialogOnRun: true,
99+
commands: [],
100+
registerGlobal: false,
101+
})) || [],
102+
},
103+
{
104+
group: 'Assistants',
105+
quickActions:
106+
foundAssistants?.map((assistant) => ({
107+
name: assistant.name,
108+
label: (
109+
<div className="flex items-center gap-x-2">
110+
<AgentLogo agent={assistant} />
111+
<span>{assistant.name}</span>
112+
{assistant.description && (
113+
<span className="ml-2 truncate text-p-sm text-volcanic-600">
114+
{assistant.description}
115+
</span>
116+
)}
117+
</div>
118+
),
119+
action: () => router.push(`/a/${assistant.id}`),
120+
closeDialogOnRun: true,
121+
commands: [],
122+
registerGlobal: false,
123+
})) || [],
124+
},
125+
]);
126+
},
127+
[query, assistants, conversations],
128+
500
129+
);
130+
131+
return (
132+
<span className="mb-3">
133+
<HotKeysDialogInput
134+
value={query}
135+
setValue={setQuery}
136+
placeholder="Search conversations and assistants"
137+
close={close}
138+
onBack={onBack}
139+
isSearch
140+
/>
141+
<ComboboxOptions className="mb-3 flex flex-col gap-y-6 overflow-y-auto" static>
142+
<CommandActionGroup isOpen={isOpen} options={results} />
143+
</ComboboxOptions>
144+
</span>
145+
);
146+
};

0 commit comments

Comments
 (0)