From 129f70539d3094d2dd7d13d90fd5466b83d995df Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot <“k.kocot0@gmail.com”> Date: Fri, 27 Jun 2025 12:48:14 +0200 Subject: [PATCH 1/7] Improve AI search options --- apps/blog/lib/get-data.tsx | 18 +++++++++++++++++- apps/blog/pages/search.tsx | 33 +++++++++++++++++++++++++++++++-- packages/ui/hooks/useSearch.ts | 20 +++++++++++++++----- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/apps/blog/lib/get-data.tsx b/apps/blog/lib/get-data.tsx index 9d9764141..3f6ba25b7 100644 --- a/apps/blog/lib/get-data.tsx +++ b/apps/blog/lib/get-data.tsx @@ -1,5 +1,5 @@ import env from '@beam-australia/react-env'; -import { Entry } from '@transaction/lib/extended-hive.chain'; +import { Entry } from '@transaction/lib/extended-hive.chain'; import { logger } from '@ui/lib/logger'; const apiDevOrigin = env('AI_DOMAIN') || process.env.AI_DOMAIN; @@ -73,3 +73,19 @@ export const getSuggestions = async ({ throw new Error('Error in getSuggestions'); } }; + +export const getThematicAuthors = async (thematic: string, observer: string): Promise => { + try { + const response = await fetch( + `${apiDevOrigin}/hivesense-api/thematiccontributors?thematic=${encodeURIComponent(thematic)}&authors_limit=10&observer=${observer}` + ); + if (!response.ok) { + throw new Error(`Authors API Error: ${response.status}`); + } + const data = await response.json(); + return data; + } catch (error) { + logger.error('Error in getAuthors', error); + throw new Error('Error in getAuthors'); + } +}; diff --git a/apps/blog/pages/search.tsx b/apps/blog/pages/search.tsx index 74b5c12e7..9979b64f3 100644 --- a/apps/blog/pages/search.tsx +++ b/apps/blog/pages/search.tsx @@ -18,7 +18,11 @@ import { useFollowListQuery } from '../components/hooks/use-follow-list'; import { useTranslation } from 'next-i18next'; import SearchCard from '../components/search-card'; import { toast } from '@ui/components/hooks/use-toast'; -import { getHiveSenseStatus, getSimilarPosts } from '../lib/get-data'; +import { getHiveSenseStatus, getSimilarPosts, getThematicAuthors } from '../lib/get-data'; +import Link from 'next/link'; +import UserAvatar from '../components/user-avatar'; +import { PopoverCardData } from '../components/popover-card-data'; +import { Card, CardContent, CardHeader } from '@ui/components/card'; export const getServerSideProps: GetServerSideProps = getDefaultProps; const PER_PAGE = 20; @@ -31,6 +35,7 @@ export default function SearchPage() { const { t } = useTranslation('common_blog'); const query = router.query.q as string; const sort = router.query.s as string; + const thematic = router.query.t as string; const aiSearch = !!query && !sort; const { data: hiveSense, isLoading: hiveSenseLoading } = useQuery( ['hivesense-api'], @@ -41,7 +46,11 @@ export default function SearchPage() { refetchOnMount: false } ); - + const { data: thematicData, isLoading: thematicIsLoading } = useQuery( + ['thematic-authors', query], + () => getThematicAuthors(thematic, user.username !== '' ? user.username : 'hive.blog'), + { enabled: !!thematic } + ); const { data, isLoading, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteQuery( ['similarPosts', query], async ({ pageParam }: { pageParam?: { author: string; permlink: string } }) => { @@ -175,6 +184,26 @@ export default function SearchPage() {
{isFetching && !isFetchingNextPage ? 'Background Updating...' : null}
+ {!!thematic ? ( + <> +

+ Authors posting about {thematic} +

+ {thematicIsLoading ? ( + + ) : thematicData && thematicData.length > 0 ? ( +
+ {thematicData.map((author) => ( + + + + + + ))} +
+ ) : null} + + ) : null} {!!sort ? ( <> {entriesDataIsError ? null : entriesDataIsLoading ? ( diff --git a/packages/ui/hooks/useSearch.ts b/packages/ui/hooks/useSearch.ts index 6dab08169..a9e207008 100644 --- a/packages/ui/hooks/useSearch.ts +++ b/packages/ui/hooks/useSearch.ts @@ -17,11 +17,21 @@ export function useSearch(aiAvailable: boolean) { }, [aiAvailable, sort]); const handleSearch = (value: string, currentMode: SearchMode) => { - const searchParams = - currentMode === 'search' - ? `q=${encodeURIComponent(value)}&s=${sort ?? 'newest'}` - : `q=${encodeURIComponent(value)}`; - router.push(`/search?${searchParams}`); + if (value.startsWith('/')) { + router.push(`/search?t=${encodeURIComponent(value.trim().slice(1))}`); + return; + } + if (value.startsWith('@')) { + router.push(`@${encodeURIComponent(value.trim().slice(1))}`); + return; + } else { + const searchParams = + currentMode === 'search' + ? `q=${encodeURIComponent(value)}&s=${sort ?? 'newest'}` + : `q=${encodeURIComponent(value)}`; + router.push(`/search?${searchParams}`); + return; + } }; return { -- GitLab From fd55a89d00b0e069a21f8dd503db44e4f2928114 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot <“k.kocot0@gmail.com”> Date: Wed, 2 Jul 2025 07:06:03 +0200 Subject: [PATCH 2/7] Change search rules --- apps/blog/components/popover-card-data.tsx | 2 +- apps/blog/pages/search.tsx | 42 +++++++------------- packages/ui/components/mode-switch-input.tsx | 1 - packages/ui/hooks/useSearch.ts | 4 +- 4 files changed, 19 insertions(+), 30 deletions(-) diff --git a/apps/blog/components/popover-card-data.tsx b/apps/blog/components/popover-card-data.tsx index f5fbc475e..3f5b90ed1 100644 --- a/apps/blog/components/popover-card-data.tsx +++ b/apps/blog/components/popover-card-data.tsx @@ -49,7 +49,7 @@ export function PopoverCardData({ author, blacklist }: { author: string; blackli const legalBlockedUser = userIllegalContent.some((e) => e === account?.name); return ( -
+
{account && !isLoading && follows.data && !follows.isLoading ? ( <>
diff --git a/apps/blog/pages/search.tsx b/apps/blog/pages/search.tsx index 9979b64f3..1524ffe65 100644 --- a/apps/blog/pages/search.tsx +++ b/apps/blog/pages/search.tsx @@ -18,15 +18,15 @@ import { useFollowListQuery } from '../components/hooks/use-follow-list'; import { useTranslation } from 'next-i18next'; import SearchCard from '../components/search-card'; import { toast } from '@ui/components/hooks/use-toast'; -import { getHiveSenseStatus, getSimilarPosts, getThematicAuthors } from '../lib/get-data'; -import Link from 'next/link'; -import UserAvatar from '../components/user-avatar'; +import { getHiveSenseStatus, getSimilarPosts } from '../lib/get-data'; import { PopoverCardData } from '../components/popover-card-data'; -import { Card, CardContent, CardHeader } from '@ui/components/card'; +import { Card } from '@ui/components/card'; export const getServerSideProps: GetServerSideProps = getDefaultProps; + const PER_PAGE = 20; const TAB_TITLE = 'Search - Hive'; + export default function SearchPage() { const router = useRouter(); const { ref, inView } = useInView(); @@ -35,7 +35,8 @@ export default function SearchPage() { const { t } = useTranslation('common_blog'); const query = router.query.q as string; const sort = router.query.s as string; - const thematic = router.query.t as string; + const searchedAuthor = router.query.a as string; + const searchedPost = router.query.p as string; const aiSearch = !!query && !sort; const { data: hiveSense, isLoading: hiveSenseLoading } = useQuery( ['hivesense-api'], @@ -46,11 +47,6 @@ export default function SearchPage() { refetchOnMount: false } ); - const { data: thematicData, isLoading: thematicIsLoading } = useQuery( - ['thematic-authors', query], - () => getThematicAuthors(thematic, user.username !== '' ? user.username : 'hive.blog'), - { enabled: !!thematic } - ); const { data, isLoading, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteQuery( ['similarPosts', query], async ({ pageParam }: { pageParam?: { author: string; permlink: string } }) => { @@ -184,25 +180,17 @@ export default function SearchPage() {
{isFetching && !isFetchingNextPage ? 'Background Updating...' : null}
- {!!thematic ? ( - <> + {searchedAuthor && !!searchedPost ? ( +

- Authors posting about {thematic} + {`Authors posting about ${searchedAuthor} about ${searchedPost}`}

- {thematicIsLoading ? ( - - ) : thematicData && thematicData.length > 0 ? ( -
- {thematicData.map((author) => ( - - - - - - ))} -
- ) : null} - + + + + +
Post List not available yet
+
) : null} {!!sort ? ( <> diff --git a/packages/ui/components/mode-switch-input.tsx b/packages/ui/components/mode-switch-input.tsx index d7fa5d78e..c2c0f6f8d 100644 --- a/packages/ui/components/mode-switch-input.tsx +++ b/packages/ui/components/mode-switch-input.tsx @@ -33,7 +33,6 @@ export function ModeSwitchInput({ className, aiAvailable, isLoading, searchPage />
{ + if (value.startsWith('%')) setInputValue(value.slice(1)); if (value.startsWith('/')) { - router.push(`/search?t=${encodeURIComponent(value.trim().slice(1))}`); + const [first_word, ...all_after] = value.trim().slice(1).split(' '); + router.push(`/search?a=${encodeURIComponent(first_word)}&p=${encodeURIComponent(all_after.join(' '))}`); return; } if (value.startsWith('@')) { -- GitLab From eec011e1576d05322bf7998096ff6906f452421f Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot <“k.kocot0@gmail.com”> Date: Mon, 14 Jul 2025 12:23:50 +0200 Subject: [PATCH 3/7] Use new search bar --- apps/blog/components/site-header.tsx | 7 +- apps/blog/feature/search/autocompleter.tsx | 148 +++++++++++++++++++++ apps/blog/feature/search/command.tsx | 29 ++++ apps/blog/feature/search/search-bar.tsx | 100 ++++++++++++++ apps/blog/feature/search/select.tsx | 108 +++++++++++++++ apps/blog/feature/search/smart-select.tsx | 57 ++++++++ apps/blog/feature/search/utils/lib.ts | 20 +++ apps/blog/pages/search.tsx | 34 ++--- packages/ui/components/dialog.tsx | 51 ++++--- packages/ui/hooks/useSearch.ts | 54 +++++--- 10 files changed, 535 insertions(+), 73 deletions(-) create mode 100644 apps/blog/feature/search/autocompleter.tsx create mode 100644 apps/blog/feature/search/command.tsx create mode 100644 apps/blog/feature/search/search-bar.tsx create mode 100644 apps/blog/feature/search/select.tsx create mode 100644 apps/blog/feature/search/smart-select.tsx create mode 100644 apps/blog/feature/search/utils/lib.ts diff --git a/apps/blog/components/site-header.tsx b/apps/blog/components/site-header.tsx index 8685c168b..ff4c3fc90 100644 --- a/apps/blog/components/site-header.tsx +++ b/apps/blog/components/site-header.tsx @@ -20,13 +20,12 @@ import LangToggle from './lang-toggle'; import { PieChart, Pie } from 'recharts'; import useManabars from './hooks/useManabars'; import { hoursAndMinutes } from '../lib/utils'; - import { getAccount } from '@transaction/lib/hive'; import TooltipContainer from '@ui/components/tooltip-container'; -import { ModeSwitchInput } from '@ui/components/mode-switch-input'; import { useRouter } from 'next/router'; import { cn } from '@ui/lib/utils'; import { getHiveSenseStatus } from '../lib/get-data'; +import SearchBar from '../feature/search/search-bar'; const SiteHeader: FC = () => { const { t } = useTranslation('common_blog'); @@ -90,7 +89,7 @@ const SiteHeader: FC = () => {
-
+
{siteConfig.name} {siteConfig.chainEnv !== 'mainnet' && ( {siteConfig.chainEnv} @@ -123,7 +122,7 @@ const SiteHeader: FC = () => { {router.pathname === '/search' ? ( ) : ( - + )}
diff --git a/apps/blog/feature/search/autocompleter.tsx b/apps/blog/feature/search/autocompleter.tsx new file mode 100644 index 000000000..7e1b2ee98 --- /dev/null +++ b/apps/blog/feature/search/autocompleter.tsx @@ -0,0 +1,148 @@ +import { Command as CommandPrimitive } from 'cmdk'; +import { useState, useCallback, type KeyboardEvent, RefObject } from 'react'; +import { Check } from 'lucide-react'; +import { cn } from '@ui/lib/utils'; +import { Skeleton } from '@ui/components/skeleton'; +import { CommandInput } from './command'; +import { CommandGroup, CommandItem, CommandList } from '@ui/components/command'; + +type AutoCompleteProps = { + onKeyDown: (event: KeyboardEvent) => void; + options: string[]; + emptyMessage: string; + value: string; + onValueChange: (value: string) => void; + isLoading?: boolean; + disabled?: boolean; + className?: string; + inputRef: RefObject; + placeholder?: string; +}; + +export const AutoComplete = ({ + onKeyDown, + options, + placeholder, + emptyMessage, + value, + onValueChange, + disabled, + className, + inputRef, + isLoading = false +}: AutoCompleteProps) => { + const [isOpen, setOpen] = useState(false); + const [selected, setSelected] = useState(value); + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + const input = inputRef.current; + if (!input) { + return; + } + + // Keep the options displayed when the user is typing + if (!isOpen) { + setOpen(true); + } + + // This is not a default behaviour of the field + if (event.key === 'Enter' && input.value !== '') { + const optionToSelect = options.find((option) => option === input.value); + if (optionToSelect) { + setSelected(optionToSelect); + onValueChange(optionToSelect ?? ''); + } + } + + if (event.key === 'Escape') { + input.blur(); + } + }, + [isOpen, options, onValueChange] + ); + + // const handleBlur = useCallback(() => { + // setOpen(false); + // onValueChange(selected); + // }, [selected]); + + const handleSelectOption = useCallback( + (selectedOption: string) => { + onValueChange(selectedOption); + + setSelected(selectedOption); + onValueChange(selectedOption); + + // This is a hack to prevent the input from being focused after the user selects an option + // We can call this hack: "The next tick" + setTimeout(() => { + inputRef?.current?.blur(); + }, 0); + }, + [onValueChange] + ); + + return ( + +
+ setOpen(true)} + placeholder={placeholder} + disabled={disabled} + className="text-base" + containterClassName={className} + /> +
+
+
+ + {isLoading ? ( + +
+ +
+
+ ) : null} + {options.length > 0 && !isLoading ? ( + + {options.map((option) => { + const isSelected = selected === option; + return ( + { + event.preventDefault(); + event.stopPropagation(); + }} + onSelect={() => handleSelectOption(option)} + className="flex w-full items-center gap-2" + > + {option} + {isSelected ? : null} + + ); + })} + + ) : null} + {!isLoading ? ( + + {emptyMessage} + + ) : null} +
+
+
+
+ ); +}; diff --git a/apps/blog/feature/search/command.tsx b/apps/blog/feature/search/command.tsx new file mode 100644 index 000000000..db15187ea --- /dev/null +++ b/apps/blog/feature/search/command.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { Command as CommandPrimitive } from 'cmdk'; +import { cn } from '@ui/lib/utils'; + +interface CommandInputProps extends React.ComponentProps { + containterClassName?: string; +} +function CommandInput({ className, containterClassName, ...props }: CommandInputProps) { + return ( +
+ +
+ ); +} + +export { CommandInput }; diff --git a/apps/blog/feature/search/search-bar.tsx b/apps/blog/feature/search/search-bar.tsx new file mode 100644 index 000000000..6cc3af793 --- /dev/null +++ b/apps/blog/feature/search/search-bar.tsx @@ -0,0 +1,100 @@ +import clsx from 'clsx'; +import { useEffect, useRef, KeyboardEvent } from 'react'; +import { getPlaceholder } from './utils/lib'; +import { useSearch } from '@ui/hooks/useSearch'; +import { AutoComplete } from './autocompleter'; +import SmartSelect from './smart-select'; + +interface ModeInputProps { + className?: string; + aiAvailable: boolean; + isLoading: boolean; + searchPage?: boolean; +} + +const SearchBar = ({ className, aiAvailable, isLoading, searchPage }: ModeInputProps) => { + const { inputValue, setInputValue, mode, setMode, handleSearch, secondInputValue, setSecondInputValue } = + useSearch(aiAvailable); + const inputRef = useRef(null); + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + handleSearch(inputValue, mode); + } + }; + const placeholder = getPlaceholder(mode); + + useEffect(() => { + if (inputValue.startsWith('/')) { + setMode('userTopics'); + setInputValue(inputValue.slice(1)); + } + if (inputValue.startsWith('%')) { + setMode('ai'); + setInputValue(inputValue.slice(1)); + } + if (inputValue.startsWith('$')) { + setMode('classic'); + setInputValue(inputValue.slice(1)); + } + if (inputValue.startsWith('@')) { + setMode('users'); + setInputValue(inputValue.slice(1)); + } + if (inputValue.startsWith('#')) { + setMode('tags'); + setInputValue(inputValue.slice(1)); + } + if (inputValue.startsWith('!')) { + setMode('community'); + setInputValue(inputValue.slice(1)); + } + }, [inputValue]); + return ( +
+ + + {mode === 'userTopics' ? ( + <> + + + + + + ) : null} +
+ ); +}; + +export default SearchBar; diff --git a/apps/blog/feature/search/select.tsx b/apps/blog/feature/search/select.tsx new file mode 100644 index 000000000..3ea00fb7b --- /dev/null +++ b/apps/blog/feature/search/select.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; +import * as SelectPrimitive from '@radix-ui/react-select'; +import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; +import { cn } from '@ui/lib/utils'; + +function SelectTrigger({ + className, + size = 'sm', + children, + ...props +}: React.ComponentProps & { + size?: 'sm' | 'default'; +}) { + return ( + + {children} + + ); +} + +function SelectContent({ + className, + children, + position = 'popper', + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ); +} + +function SelectItem({ className, children, ...props }: React.ComponentProps) { + return ( + + {children} + + ); +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { SelectContent, SelectItem, SelectTrigger }; diff --git a/apps/blog/feature/search/smart-select.tsx b/apps/blog/feature/search/smart-select.tsx new file mode 100644 index 000000000..42a5b9362 --- /dev/null +++ b/apps/blog/feature/search/smart-select.tsx @@ -0,0 +1,57 @@ +import { Bot, Search, AtSign, Hash, PersonStanding } from 'lucide-react'; +import { SearchMode } from '@ui/hooks/useSearch'; +import { SelectContent, SelectItem, SelectTrigger } from './select'; +import { Select, SelectGroup, SelectValue } from '@ui/components'; + +const SmartSelect = ({ + value, + onValueChange, + aiDisabled +}: { + value: SearchMode; + onValueChange: (value: SearchMode) => void; + aiDisabled?: boolean; +}) => { + return ( + + ); +}; +export default SmartSelect; diff --git a/apps/blog/feature/search/utils/lib.ts b/apps/blog/feature/search/utils/lib.ts new file mode 100644 index 000000000..b708e085e --- /dev/null +++ b/apps/blog/feature/search/utils/lib.ts @@ -0,0 +1,20 @@ +import { SearchMode } from '@ui/hooks/useSearch'; + +export const getPlaceholder = (value: SearchMode) => { + switch (value) { + case 'ai': + return 'AI Search...'; + case 'classic': + return 'Search...'; + case 'users': + return 'Search users...'; + case 'userTopics': + return 'Username...'; + case 'tags': + return 'Search tags...'; + case 'community': + return 'Search community...'; + default: + return 'Search something...'; + } +}; diff --git a/apps/blog/pages/search.tsx b/apps/blog/pages/search.tsx index 1524ffe65..3b9da6515 100644 --- a/apps/blog/pages/search.tsx +++ b/apps/blog/pages/search.tsx @@ -8,7 +8,6 @@ import { useInView } from 'react-intersection-observer'; import Head from 'next/head'; import PostList from '../components/post-list'; import { getSearch } from '@transaction/lib/bridge'; -import { ModeSwitchInput } from '@ui/components/mode-switch-input'; import { useLocalStorage } from 'usehooks-ts'; import { useEffect } from 'react'; import { useUser } from '@smart-signer/lib/auth/use-user'; @@ -19,8 +18,7 @@ import { useTranslation } from 'next-i18next'; import SearchCard from '../components/search-card'; import { toast } from '@ui/components/hooks/use-toast'; import { getHiveSenseStatus, getSimilarPosts } from '../lib/get-data'; -import { PopoverCardData } from '../components/popover-card-data'; -import { Card } from '@ui/components/card'; +import SearchBar from '../feature/search/search-bar'; export const getServerSideProps: GetServerSideProps = getDefaultProps; @@ -35,9 +33,7 @@ export default function SearchPage() { const { t } = useTranslation('common_blog'); const query = router.query.q as string; const sort = router.query.s as string; - const searchedAuthor = router.query.a as string; - const searchedPost = router.query.p as string; - const aiSearch = !!query && !sort; + const aiQuery = router.query.ai as string; const { data: hiveSense, isLoading: hiveSenseLoading } = useQuery( ['hivesense-api'], () => getHiveSenseStatus(), @@ -48,10 +44,10 @@ export default function SearchPage() { } ); const { data, isLoading, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteQuery( - ['similarPosts', query], + ['similarPosts', aiQuery], async ({ pageParam }: { pageParam?: { author: string; permlink: string } }) => { return await getSimilarPosts({ - pattern: query, + pattern: aiQuery, observer: user.username !== '' ? user.username : 'hive.blog', start_permlink: pageParam?.permlink ?? '', start_author: pageParam?.author ?? '', @@ -71,7 +67,7 @@ export default function SearchPage() { refetchOnWindowFocus: false, refetchOnReconnect: false, refetchOnMount: false, - enabled: aiSearch + enabled: !!aiQuery } ); const { @@ -87,7 +83,7 @@ export default function SearchPage() { (lastPage) => getSearch(query, lastPage.pageParam, sort), { getNextPageParam: (lastPage) => lastPage.scroll_id, - enabled: Boolean(sort) && Boolean(query) + enabled: !!sort && !!query } ); useEffect(() => { @@ -149,11 +145,11 @@ export default function SearchPage() {
- +
- {!aiSearch || !query ? null : isLoading ? ( + {!aiQuery ? null : isLoading ? ( ) : data ? ( data.pages.map((page, index) => { @@ -180,19 +176,7 @@ export default function SearchPage() {
{isFetching && !isFetchingNextPage ? 'Background Updating...' : null}
- {searchedAuthor && !!searchedPost ? ( -
-

- {`Authors posting about ${searchedAuthor} about ${searchedPost}`} -

- - - - -
Post List not available yet
-
- ) : null} - {!!sort ? ( + {!!sort && !!query ? ( <> {entriesDataIsError ? null : entriesDataIsLoading ? ( diff --git a/packages/ui/components/dialog.tsx b/packages/ui/components/dialog.tsx index 871ad9ae0..e58a72540 100644 --- a/packages/ui/components/dialog.tsx +++ b/packages/ui/components/dialog.tsx @@ -32,28 +32,35 @@ const DialogOverlay = React.forwardRef< )); DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)); +interface DialogContentProps extends React.ComponentPropsWithoutRef { + showCloseButton?: boolean; +} +const DialogContent = React.forwardRef, DialogContentProps>( + ({ className, children, showCloseButton, ...props }, ref) => ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +); DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( diff --git a/packages/ui/hooks/useSearch.ts b/packages/ui/hooks/useSearch.ts index 398fdef23..2e581f2d7 100644 --- a/packages/ui/hooks/useSearch.ts +++ b/packages/ui/hooks/useSearch.ts @@ -1,44 +1,54 @@ import { useRouter } from 'next/router'; import { useState, useEffect } from 'react'; -type SearchMode = 'ai' | 'search'; +export type SearchMode = 'ai' | 'users' | 'userTopics' | 'tags' | 'community' | 'classic'; export function useSearch(aiAvailable: boolean) { const router = useRouter(); - const sort = router.query.s as string; const query = router.query.q as string; - const [inputValue, setInputValue] = useState(query ?? ''); - const [mode, setMode] = useState(!aiAvailable || !!sort ? 'search' : 'ai'); + const secondQuery = router.query.p as string; + const aiQuery = router.query.ai as string; + const [inputValue, setInputValue] = useState(query ?? aiQuery ?? ''); + const [secondInputValue, setSecondInputValue] = useState(secondQuery ?? ''); + const [mode, setMode] = useState(!aiAvailable ? 'classic' : 'ai'); useEffect(() => { - if (aiAvailable && !sort) { + if (aiAvailable) { setMode('ai'); } - }, [aiAvailable, sort]); + }, [aiAvailable]); const handleSearch = (value: string, currentMode: SearchMode) => { - if (value.startsWith('%')) setInputValue(value.slice(1)); - if (value.startsWith('/')) { - const [first_word, ...all_after] = value.trim().slice(1).split(' '); - router.push(`/search?a=${encodeURIComponent(first_word)}&p=${encodeURIComponent(all_after.join(' '))}`); - return; - } - if (value.startsWith('@')) { - router.push(`@${encodeURIComponent(value.trim().slice(1))}`); - return; - } else { - const searchParams = - currentMode === 'search' - ? `q=${encodeURIComponent(value)}&s=${sort ?? 'newest'}` - : `q=${encodeURIComponent(value)}`; - router.push(`/search?${searchParams}`); - return; + switch (currentMode) { + case 'ai': + router.push(`/search?ai=${encodeURIComponent(value)}`); + break; + case 'users': + router.push(`@${encodeURIComponent(value)}`); + break; + case 'userTopics': + router.push(`/search?a=${encodeURIComponent(value)}&p=${encodeURIComponent(secondInputValue)}`); + break; + case 'tags': + router.push(`trending/${encodeURIComponent(value)}`); + break; + case 'community': + router.push(`trending/${encodeURIComponent(value)}`); + break; + case 'classic': + router.push(`/search?q=${encodeURIComponent(value)}&s=trending`); + break; + default: + router.push(`/search?q=${encodeURIComponent(value)}&s=trending`); + break; } }; return { inputValue, setInputValue, + secondInputValue, + setSecondInputValue, mode, setMode, handleSearch -- GitLab From 5ce2a71e1b35e07daa241bff553297f077f1da6a Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot <“k.kocot0@gmail.com”> Date: Mon, 14 Jul 2025 12:47:28 +0200 Subject: [PATCH 4/7] Handle error on ai search results --- apps/blog/pages/search.tsx | 53 ++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/apps/blog/pages/search.tsx b/apps/blog/pages/search.tsx index 3b9da6515..0a3fabfcc 100644 --- a/apps/blog/pages/search.tsx +++ b/apps/blog/pages/search.tsx @@ -43,33 +43,34 @@ export default function SearchPage() { refetchOnMount: false } ); - const { data, isLoading, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteQuery( - ['similarPosts', aiQuery], - async ({ pageParam }: { pageParam?: { author: string; permlink: string } }) => { - return await getSimilarPosts({ - pattern: aiQuery, - observer: user.username !== '' ? user.username : 'hive.blog', - start_permlink: pageParam?.permlink ?? '', - start_author: pageParam?.author ?? '', - limit: PER_PAGE - }); - }, - { - getNextPageParam: (lastPage) => { - if (lastPage && lastPage.length === PER_PAGE) { - return { - author: lastPage[lastPage.length - 1].author, - permlink: lastPage[lastPage.length - 1].permlink - }; - } + const { data, isLoading, isFetching, isError, isFetchingNextPage, fetchNextPage, hasNextPage } = + useInfiniteQuery( + ['similarPosts', aiQuery], + async ({ pageParam }: { pageParam?: { author: string; permlink: string } }) => { + return await getSimilarPosts({ + pattern: aiQuery, + observer: user.username !== '' ? user.username : 'hive.blog', + start_permlink: pageParam?.permlink ?? '', + start_author: pageParam?.author ?? '', + limit: PER_PAGE + }); }, + { + getNextPageParam: (lastPage) => { + if (lastPage && lastPage.length === PER_PAGE) { + return { + author: lastPage[lastPage.length - 1].author, + permlink: lastPage[lastPage.length - 1].permlink + }; + } + }, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - refetchOnMount: false, - enabled: !!aiQuery - } - ); + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + enabled: !!aiQuery + } + ); const { data: entriesData, isLoading: entriesDataIsLoading, @@ -155,6 +156,8 @@ export default function SearchPage() { data.pages.map((page, index) => { return page ? : null; }) + ) : isError ? ( +
Error loading AI search results.
) : null}
diff --git a/apps/blog/feature/search/search-bar.tsx b/apps/blog/feature/search/search-bar.tsx index 6cc3af793..389d7f4b3 100644 --- a/apps/blog/feature/search/search-bar.tsx +++ b/apps/blog/feature/search/search-bar.tsx @@ -5,14 +5,7 @@ import { useSearch } from '@ui/hooks/useSearch'; import { AutoComplete } from './autocompleter'; import SmartSelect from './smart-select'; -interface ModeInputProps { - className?: string; - aiAvailable: boolean; - isLoading: boolean; - searchPage?: boolean; -} - -const SearchBar = ({ className, aiAvailable, isLoading, searchPage }: ModeInputProps) => { +const SearchBar = ({ aiAvailable }: { aiAvailable: boolean }) => { const { inputValue, setInputValue, mode, setMode, handleSearch, secondInputValue, setSecondInputValue } = useSearch(aiAvailable); const inputRef = useRef(null); diff --git a/apps/blog/pages/search.tsx b/apps/blog/pages/search.tsx index 0a3fabfcc..0325dbaef 100644 --- a/apps/blog/pages/search.tsx +++ b/apps/blog/pages/search.tsx @@ -146,7 +146,7 @@ export default function SearchPage() {
- +
-- GitLab From de45ce7dcebf00a3b1f8a4ff3bfdbba194f2a5c3 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot <“k.kocot0@gmail.com”> Date: Mon, 14 Jul 2025 13:13:58 +0200 Subject: [PATCH 6/7] Fix Dialog component --- packages/ui/components/dialog.tsx | 52 ++++++++++++++----------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/packages/ui/components/dialog.tsx b/packages/ui/components/dialog.tsx index e58a72540..d472fb8ea 100644 --- a/packages/ui/components/dialog.tsx +++ b/packages/ui/components/dialog.tsx @@ -32,35 +32,31 @@ const DialogOverlay = React.forwardRef< )); DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; -interface DialogContentProps extends React.ComponentPropsWithoutRef { - showCloseButton?: boolean; -} -const DialogContent = React.forwardRef, DialogContentProps>( - ({ className, children, showCloseButton, ...props }, ref) => ( - - - , + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + - {children} - {showCloseButton && ( - - - Close - - )} - - - ) -); + + Close + + + +)); DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( -- GitLab From 878a1418477303211ac3a6e48eac6e1cd6c32a96 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot <“k.kocot0@gmail.com”> Date: Tue, 15 Jul 2025 07:11:03 +0200 Subject: [PATCH 7/7] Catch error faster --- apps/blog/feature/search/smart-select.tsx | 2 +- apps/blog/pages/search.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/blog/feature/search/smart-select.tsx b/apps/blog/feature/search/smart-select.tsx index 42a5b9362..19abd4229 100644 --- a/apps/blog/feature/search/smart-select.tsx +++ b/apps/blog/feature/search/smart-select.tsx @@ -28,7 +28,7 @@ const SmartSelect = ({ - + {!aiQuery ? null : isLoading ? ( + ) : isError ? ( +
Error loading AI search results.
) : data ? ( data.pages.map((page, index) => { return page ? : null; }) - ) : isError ? ( -
Error loading AI search results.
) : null}