diff --git a/src/features/Inference/Inference.tsx b/src/features/Inference/Inference.tsx new file mode 100644 index 000000000..413e83c26 --- /dev/null +++ b/src/features/Inference/Inference.tsx @@ -0,0 +1,67 @@ +import { useEffect, useMemo, useState } from 'react'; +import { Display, MainContainer } from 'src/components'; +import { getSearchQuery } from 'src/utils/search/utils'; +import { useParams } from 'react-router-dom'; +import Loader2 from 'src/components/ui/Loader2'; +import useInference from './hooks/useInference'; +import RowItems from './components/RowItems/RowItems'; +import ActionBarContainer from './components/ActionBar/ActionBar'; +import Filters from './components/Filters/Filters'; +import { SortBy } from './type'; +import sortByKey from './utils/sortByKey'; + +function Inference() { + const { query } = useParams(); + + const [keywordHash, setKeywordHash] = useState(''); + const [sortBy, setSortBy] = useState(SortBy.inference); + + const { data, isFetching, error } = useInference(keywordHash); + + useEffect(() => { + (async () => { + const keywordHash = await getSearchQuery(query || ''); + + setKeywordHash(keywordHash); + })(); + }, [query]); + + const dataSortByKey = useMemo(() => { + if (!data) { + return []; + } + + return sortByKey(data.result, sortBy); + }, [data, sortBy]); + + return ( + <> + + + {isFetching && !data ? ( + + ) : data && data.result.length > 0 ? ( + + ) : error ? ( + +

{error.toString()}

+
+ ) : ( + +

+ there are no answers or questions to this particle
be the + first and create one +

+
+ )} +
+ + + ); +} + +export default Inference; diff --git a/src/features/Inference/components/ActionBar/ActionBar.module.scss b/src/features/Inference/components/ActionBar/ActionBar.module.scss new file mode 100644 index 000000000..7a92828d5 --- /dev/null +++ b/src/features/Inference/components/ActionBar/ActionBar.module.scss @@ -0,0 +1,9 @@ +.inputContainer { + width: 80%; + text-align: center; +} + +.input { + text-align: center !important; + font-size: 20px !important; +} diff --git a/src/features/Inference/components/ActionBar/ActionBar.tsx b/src/features/Inference/components/ActionBar/ActionBar.tsx new file mode 100644 index 000000000..7b9200e03 --- /dev/null +++ b/src/features/Inference/components/ActionBar/ActionBar.tsx @@ -0,0 +1,33 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ActionBar, Color, Input } from 'src/components'; +import { routes } from 'src/routes'; +import { replaceSlash } from 'src/utils/utils'; +import styles from './ActionBar.module.scss'; + +function ActionBarContainer() { + const navigate = useNavigate(); + const [valueInput, setValueInput] = useState(''); + + function submit(event: React.FormEvent) { + event.preventDefault(); + + navigate(routes.inference.getLink(replaceSlash(valueInput))); + } + + return ( + +
+ setValueInput(e.target.value)} + className={styles.input} + /> +
+
+ ); +} + +export default ActionBarContainer; diff --git a/src/features/Inference/components/Filters/Filters.module.scss b/src/features/Inference/components/Filters/Filters.module.scss new file mode 100644 index 000000000..ccf84fd94 --- /dev/null +++ b/src/features/Inference/components/Filters/Filters.module.scss @@ -0,0 +1,25 @@ +@import '../../../../components/containerGradient/saber/index.module.scss'; + +.header { + margin: 10px 0 -5px; + padding: 17px 12px 20px; + + display: flex; + justify-content: space-between; + align-content: center; + + @include saber('blue', top); + + @media (max-width: 680px) { + gap: 40px 0; + flex-wrap: wrap; + } +} + +.total { + color: rgba(221, 255, 255, 0.38); + + span { + color: rgba(255, 255, 255, 0.78); + } +} \ No newline at end of file diff --git a/src/features/Inference/components/Filters/Filters.tsx b/src/features/Inference/components/Filters/Filters.tsx new file mode 100644 index 000000000..e663775b4 --- /dev/null +++ b/src/features/Inference/components/Filters/Filters.tsx @@ -0,0 +1,56 @@ +import ButtonsGroup from 'src/components/buttons/ButtonsGroup/ButtonsGroup'; +import AdviserHoverWrapper from 'src/features/adviser/AdviserHoverWrapper/AdviserHoverWrapper'; +import { SortBy } from '../../type'; +import styles from './Filters.module.scss'; + +const sortConfig = { + [SortBy.rank]: { + label: '⭐', + tooltip: 'sort particles by cyberrank', + }, + [SortBy.inference]: { + label: '🔥', + tooltip: 'sort particles by inference', + }, + [SortBy.balance]: { + label: '⚡️', + tooltip: 'sort particles by balance of volt', + }, +}; + +type Props = { + filter: SortBy; + setFilter: (item: SortBy) => void; + total?: number; +}; + +function Filters({ filter, setFilter, total }: Props) { + return ( +
+ { + return { + label: sortConfig[sortType].label, + name: sortType, + checked: filter === sortType, + tooltip: sortConfig[sortType].tooltip, + }; + })} + onChange={(sortType: SortBy) => { + setFilter(sortType); + }} + /> + + {total && ( + +
+ {total} particles +
+
+ )} +
+ ); +} + +export default Filters; diff --git a/src/features/Inference/components/RowItems/RowItems.module.scss b/src/features/Inference/components/RowItems/RowItems.module.scss new file mode 100644 index 000000000..14f4e3b7c --- /dev/null +++ b/src/features/Inference/components/RowItems/RowItems.module.scss @@ -0,0 +1,11 @@ +.infiniteScroll { + margin-top: 12px; + + display: flex; + flex-direction: column; + align-items: center; + + > * { + width: 100%; + } +} diff --git a/src/features/Inference/components/RowItems/RowItems.tsx b/src/features/Inference/components/RowItems/RowItems.tsx new file mode 100644 index 000000000..4f6731b50 --- /dev/null +++ b/src/features/Inference/components/RowItems/RowItems.tsx @@ -0,0 +1,47 @@ +import { useMemo, useState } from 'react'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import Spark from 'src/components/search/Spark/Spark'; +import { Dots } from 'src/components'; +import styles from './RowItems.module.scss'; +import { InferenceItem } from '../../type'; + +const LOAD_COUNT = 10; + +function RowItems({ dataItem, sortBy }: { dataItem: InferenceItem[]; sortBy: string }) { + const [itemsToShow, setItemsToShow] = useState(20); + + const loadMore = () => { + setTimeout(() => { + setItemsToShow((i) => i + LOAD_COUNT); + }, 2000); + }; + + const displayedPalettes = useMemo( + () => + dataItem.slice(0, itemsToShow).map((item) => { + return ( + + ); + }), + [itemsToShow, dataItem, sortBy] + ); + + return ( + all loaded

} + hasMore={dataItem.length > itemsToShow} + loader={} + className={styles.infiniteScroll} + > + {displayedPalettes} +
+ ); +} + +export default RowItems; diff --git a/src/features/Inference/hooks/useInference.ts b/src/features/Inference/hooks/useInference.ts new file mode 100644 index 000000000..0c9c58ec2 --- /dev/null +++ b/src/features/Inference/hooks/useInference.ts @@ -0,0 +1,35 @@ +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; +import { InferenceItem, SortBy } from '../type'; + +export type InferenceResponse = { + result: InferenceItem[]; + time: number; +}; + +const inferenceFetcher = async (hash: string) => { + return axios({ + method: 'get', + url: `https://st-inference.cybernode.ai/standard_inference?particle=${hash}`, + }) + .then((response) => response.data as InferenceResponse) + .catch((e) => { + throw new Error(`useInference: ${e}`); + }); +}; + +function useInference(hash: string) { + const { data, isFetching, error } = useQuery( + ['useInference', hash], + () => inferenceFetcher(hash), + { enabled: Boolean(hash.length) } + ); + + return { + data, + isFetching, + error, + }; +} + +export default useInference; diff --git a/src/features/Inference/type.ts b/src/features/Inference/type.ts new file mode 100644 index 000000000..50750d715 --- /dev/null +++ b/src/features/Inference/type.ts @@ -0,0 +1,12 @@ +export type InferenceItem = { + particle: string; + balance: number; + rank: number; + inference: number; +}; + +export enum SortBy { + inference = 'inference', + rank = 'rank', + balance = 'balance', +} diff --git a/src/features/Inference/utils/sortByKey.ts b/src/features/Inference/utils/sortByKey.ts new file mode 100644 index 000000000..62a28543a --- /dev/null +++ b/src/features/Inference/utils/sortByKey.ts @@ -0,0 +1,20 @@ +import { InferenceItem, SortBy } from '../type'; + +const sortByKey = (data: InferenceItem[], sortBy: SortBy) => { + if (!data) { + return []; + } + + switch (sortBy) { + case SortBy.balance: + return data.sort((a, b) => b.balance - a.balance); + + case SortBy.rank: + return data.sort((a, b) => b.rank - a.rank); + + default: + return data.sort((a, b) => b.inference - a.inference); + } +}; + +export default sortByKey; diff --git a/src/router.tsx b/src/router.tsx index 8f73a59ae..023c3ea54 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -59,6 +59,7 @@ import Cybernet from './features/cybernet/ui/Cybernet'; import Settings from './pages/Settings/Settings'; import FreestyleIde from './pages/robot/Soul/RuneEditor/FreestyleIde/FreestyleIde'; import Map from './pages/Portal/Map/Map'; +import Inference from './features/Inference/Inference'; type WrappedRouterProps = { children: React.ReactNode; @@ -147,6 +148,8 @@ function AppRouter() { /> } /> + } /> + } /> `/oracle/ask/${search}`, }, + inference: { + path: '/inference', + getLink: (search: string) => `/oracle/inference/${search}`, + }, teleport: { path: '/teleport', send: {