diff --git a/messages/en.json b/messages/en.json index f433c75..c0670fa 100644 --- a/messages/en.json +++ b/messages/en.json @@ -4,6 +4,11 @@ "title": "Try asking..." } }, + "search": { + "followUp": { + "title": "Follow up..." + } + }, "searchbox": { "placeholder": "Search for words and phrases, or ask a question...", "groups": { diff --git a/src/app/[locale]/search/page.tsx b/src/app/[locale]/search/page.tsx index 14724ed..3fd8fd6 100644 --- a/src/app/[locale]/search/page.tsx +++ b/src/app/[locale]/search/page.tsx @@ -1,9 +1,8 @@ import { Hits } from '@/components/hits'; +import { QuestionsResult } from '@/components/questions'; import { SearchProvider } from '@/components/search-provider'; import { Searchbox } from '@/components/searchbox'; import { SearchRequest } from '@/lib/client'; -import { getInfoPocServerClient } from '@/lib/info-poc-client'; -import { SearchParameters } from '@clinia/search-sdk-core'; const parseSearchRequest = (searchParams: { [key: string]: string | string[] | undefined; @@ -27,19 +26,11 @@ export default async function Search({ }) { const req = parseSearchRequest(searchParams); - const client = getInfoPocServerClient(); - - const resp = await client.search(req); - return ( - -
- + +
+ +
diff --git a/src/components/hits.tsx b/src/components/hits.tsx index f5179a9..22a94f9 100644 --- a/src/components/hits.tsx +++ b/src/components/hits.tsx @@ -1,18 +1,97 @@ 'use client'; -import { Article, Hit } from '@/lib/client'; +import { Article, Highlight, Hit } from '@/lib/client'; import { useHits } from '@clinia/search-sdk-react'; export const Hits = () => { const hits = useHits() as Hit
[]; + if (hits.length === 0) { + return null; + } + return ( -
+
{hits.map((hit) => ( -
-

{hit.resource.title}

-
+ ))}
); }; + +const ArticleHit = ({ hit }: { hit: Hit
}) => { + const passageHighlight = hit.highlight.find((h) => h.type === 'passage'); + + return ( +
+

+ {hit.resource.title} +

+ {passageHighlight && ( + + )} +
+ ); +}; + +const PassageHighlight = ({ + highlight, + text, +}: { + highlight: Highlight; + text: string; +}) => { + if (highlight.type !== 'passage') { + return null; + } + + const sentenceHighlight = highlight.highlight; + if (sentenceHighlight?.type === 'sentence') { + return ( + + ); + } + + return

{highlight.match}

; +}; + +const SentenceHighlight = ({ + highlight, + text, + passage, +}: { + highlight: Highlight; + passage: Highlight; + text: string; +}) => { + // We get the start offset of the sentence with respect to the passage + const startOffset = highlight.startOffset - passage.startOffset; + + console.log({ + passageStartOffset: passage.startOffset, + sentenceStartOffset: highlight.startOffset, + startOffset, + }); + + const start = passage.match.slice(passage.startOffset, startOffset); + + // We get the end offset of the sentence with respect to the passage + const endOffset = highlight.endOffset - passage.startOffset; + + const end = passage.match.slice(endOffset); + + return ( +

+ {start} + {highlight.match} + {end} +

+ ); +}; diff --git a/src/components/questions.tsx b/src/components/questions.tsx index d7f8e37..04a30d9 100644 --- a/src/components/questions.tsx +++ b/src/components/questions.tsx @@ -1,7 +1,9 @@ 'use client'; +import { useTranslations } from 'next-intl'; import { useRouter } from 'next/navigation'; import { Button } from '@clinia-ui/react'; +import { useMeta } from './use-meta'; type QuestionsProps = { title: string; @@ -28,3 +30,16 @@ export const Questions = ({ title, questions }: QuestionsProps) => {
); }; + +export const QuestionsResult = () => { + const meta = useMeta(); + const t = useTranslations(); + + if (!meta) { + return null; + } + + return ( + + ); +}; diff --git a/src/components/search-provider.tsx b/src/components/search-provider.tsx index a52cc21..a8a2205 100644 --- a/src/components/search-provider.tsx +++ b/src/components/search-provider.tsx @@ -1,6 +1,7 @@ 'use client'; import { SearchRequest, SearchResponse } from '@/lib/client'; +import { client } from '@/lib/info-poc-client'; import { PropsWithChildren, use, useCallback, useEffect, useMemo } from 'react'; import { SearchParameters, @@ -18,13 +19,16 @@ type SearchProviderProps = PropsWithChildren<{ export const SearchProvider = ({ children, state }: SearchProviderProps) => { const search: SearchSDKOptions['search'] = async (_collection, params) => { - const response = await fetch(`/api/search?q=${params.query}`); - - if (!response.ok) { - throw new Error('Failed to fetch search results'); - } - - return await response.json(); + const resp = await client.search({ query: params.query ?? '' }); + return { + hits: resp.hits, + meta: { + numPages: 1, + page: 1, + perPage: 10, + total: resp.hits.length, + }, + }; }; const searchForFacets: SearchSDKOptions['searchForFacets'] = @@ -72,23 +76,8 @@ export const SearchProvider = ({ children, state }: SearchProviderProps) => { }} > - {children} ); }; - -/** - * This component is used to observe the collection state and trigger side effects - * - **/ -const CollectionObserver = () => { - const col = useCollection(); - - useEffect(() => { - col.search(); - }, [col]); - - return null; -}; diff --git a/src/components/searchbox.tsx b/src/components/searchbox.tsx index c2a0b5b..3473b95 100644 --- a/src/components/searchbox.tsx +++ b/src/components/searchbox.tsx @@ -3,9 +3,10 @@ import { useClickAway } from '@uidotdev/usehooks'; import { Search, Sparkles } from 'lucide-react'; import { twMerge } from 'tailwind-merge'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslations } from 'next-intl'; -import { useRouter } from 'next/router'; +import { usePathname, useRouter } from 'next/navigation'; +import { useQuery } from '@clinia/search-sdk-react'; import { Command, CommandEmpty, @@ -15,13 +16,21 @@ import { CommandList, } from '@clinia-ui/react'; -type SearchBoxProps = React.HTMLAttributes; +type SearchBoxProps = React.HTMLAttributes & { + initialQuery?: string; +}; -export const Searchbox = ({ className, ...props }: SearchBoxProps) => { +export const Searchbox = ({ + className, + initialQuery, + ...props +}: SearchBoxProps) => { + const [query, setQuery] = useQuery(); const t = useTranslations(); - const [value, setValue] = useState(''); + const [value, setValue] = useState(initialQuery ?? ''); const [open, setOpen] = useState(false); const router = useRouter(); + const pathname = usePathname(); const groups = useMemo( () => [ @@ -46,9 +55,23 @@ export const Searchbox = ({ className, ...props }: SearchBoxProps) => { const ref = useClickAway(() => setOpen(false)); const handleSearch = (v: string) => { + if (pathname === '/search') { + setQuery(v); + setValue(v); + return; + } + + // Else we push the new search query to the router router.push(`/search?q=${v}`); }; + useEffect(() => { + if (pathname === '/search') { + setQuery(initialQuery); + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this once + }, []); + return ( { + const meta = useObserver({ + key: 'meta', + onSearchStateChange: (state) => { + if (!state.result) + return { + queryId: '', + queryIntent: '', + query: '', + questions: [], + }; + + return state.result.meta as unknown as SearchResponse['meta']; + }, + }); + + return meta; +}; diff --git a/src/lib/client/types.ts b/src/lib/client/types.ts index 1ca18a1..f991794 100644 --- a/src/lib/client/types.ts +++ b/src/lib/client/types.ts @@ -1,47 +1,45 @@ export type InformationPocClient = { - search: (params: SearchRequest) => Promise>; + search: (params: SearchRequest) => Promise>; }; - export type SearchRequest = { - query: string; -} + query: string; +}; export type SearchResponse = { - hits: Hit[]; - meta: { - queryId: string - queryIntent: 'QUESTION' - questions: string[] - } -} + hits: Hit[]; + meta: { + queryId: string; + queryIntent: 'QUESTION'; + questions: string[]; + }; +}; export type Hit = { - resource: T; - highlight: Highlight[]; - enrichers: Enrichers; -} - + resource: T; + highlight: Highlight[]; + enrichers: Enrichers; +}; export type Resource = { - id: string; - [key: string]: any; -} + id: string; + [key: string]: any; +}; export type Article = Resource & { - title: string; - text: string; -} + title: string; + text: string; +}; export type Highlight = { - match: string; - startOffset: number; - endOffset: number; - type: 'passage' | 'sentence'; - highlight?: Highlight; - score: number; -} + match: string; + startOffset: number; + endOffset: number; + type: 'passage' | 'sentence'; + highlight?: Highlight; + score: number; +}; export type Enrichers = { - questions: string[]; -} \ No newline at end of file + questions: string[]; +}; diff --git a/src/lib/info-poc-client.ts b/src/lib/info-poc-client.ts index 89a6796..b171eba 100644 --- a/src/lib/info-poc-client.ts +++ b/src/lib/info-poc-client.ts @@ -1,7 +1,3 @@ -import {informationPocClient} from '@/lib/client'; +import { informationPocClient } from '@/lib/client'; - -export const getInfoPocServerClient = () => { - const baseUrl = process.env.API_URL || 'http://localhost:7999'; - return informationPocClient({baseUrl}); -} \ No newline at end of file +export const client = informationPocClient({ baseUrl: '/api' });