diff --git a/.env b/.env new file mode 100644 index 000000000..895f1007c --- /dev/null +++ b/.env @@ -0,0 +1 @@ +# CHAIN_ID = space-pussy \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index cc53da22d..a62234fdf 100644 --- a/.eslintrc +++ b/.eslintrc @@ -37,6 +37,7 @@ "FileReader": true, "cyblog": "readonly" }, + "rules": { "valid-jsdoc": "off", "no-shadow": "off", diff --git a/.gitignore b/.gitignore index 4a0fc2c63..d0443a7ff 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,7 @@ yarn-error.log* src/.DS_Store storybook-static -.env +.env.*.local # Local Netlify folder .netlify diff --git a/.storybook/main.ts b/.storybook/main.ts index 3e36d82c5..d105743ae 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -56,6 +56,10 @@ const config: StorybookConfig = { test: /\.cozo$/, use: 'raw-loader', }, + { + test: /\.rn$/, + type: 'asset/source', + }, ], }, }; diff --git a/.vscode/settings.json b/.vscode/settings.json index 9224529ed..2ef084356 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "cybercongress", "cyberlink", "cyberlinks", + "cybernet", "denoms", "helia", "investmint", @@ -13,6 +14,8 @@ "negentropy", "stylelint", "superintelligence", + "websockets", + "cyberver", "websockets" ], "eslint.enable": true, @@ -23,7 +26,7 @@ "typescriptreact" ], "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "css.validate": false, "less.validate": false, diff --git a/docs/backend.md b/docs/backend.md index 7f36deeb9..69cc83b36 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -1,51 +1,194 @@ -# Backend Architecture +# CYB local backend(in-browser) + +Cyb plays singinficat role in cyber infrastructure. The app reconstruct self-sufficient backend+frontend pattern inside the browser. +In big view app consist from 3 parts: ```mermaid graph TD; + App["frontend\n(main thread)"]-.proxy.->Backend["backend\n(shared worker)"]; + App-.proxy.->Db["graph db\n(shared worker)"]; + Backend-.proxy.->Db; + App<-.message\nchannels.->Backend; +``` - subgraph frontend["frontend(main thread)"] - App["Frontend"]-->Hook["useBackend()"]; - Hook-->methods("startSync()\nloadIpfs()\n...\nisReady\nipfsError\n..."); - Hook-.broadcast channel\n(any worker).->reducer["redux(state)"] - Hook-.save history from app.->defferedDbApiFront[/"DefferedDbApi(proxy)"/] - Hook--osenseApi["senseApi"]; - Hook--oipfsApiFront[/"ipfsApi(proxy)"/]; - senseApi--odbApi[/"dbApi(proxy)"/]; - end +To reduce overload of main thread we have created 2 separate shared workers, where all the stuff is hosted. Bi-interraction between all layers occurs using proxy(comlink abstraction) or directly using broadcast channels. - dbApi<-.message channel.->dbWorker["dbApi"]; - subgraph dbWorkerGraph["cyb~db(worker)"] - dbWorker<-.bindings(webApi).->cozodb{{"CozoDb(wasm)"}} - end +## Db layer + +Db worker is pretty simple it it's host only local relational-graph-vector database - [[cozo]]. It's represented with DbApi in frontend and backend layers. +Cozo provide bazing fast access to brain and ipfs data in relational form and also in vector format, processing by [ml]embedder. - defferedDbApiFront-.->defferedDbApi; - ipfsApiFront<-.->ipfsApi; - subgraph backgroundWorker["cyb~backend(worker)"] - subgraph sync["sync service"] - ipfsNode["ipfs node"]; - links; - transactions; +```mermaid +graph TD; + dbApi["dbApi"]--odb_meta_orm; + subgraph rune["cozo db"] + db_meta_orm[["meta orm"]]-.->db; end - sync--oparticleResolver[["Particle resolver"]] - particleResolver--oqueue; - particleResolver--odbProxyWorker; - sync--oipfsApi; - sync--odbProxyWorker[/"dbApi(proxy)"/]; - defferedDbApi[["defferedDbApi"]]-->dbProxyWorker; - queue-->defferedDbApi; - ipfsApi--oqueue[["queue"]]; - ipfsApi--onode["node"]; - queue--balancer-->node; - node--embedded-->helia; - node--rpc-->kubo; - node--embedded-->js-ipfs; +``` + +### Db entities + +- brain: + - particles + - embeddings + - links + - transactions + - community +- sense: + + - sync items + update status + +- system: + - config + - queue messages + +## Backend layer + +Backend worker is more complicated it contains significant elements of cyb architecture: + +```mermaid +graph TD; + subgraph Backend["backend(shared worker)"] + subgraph ipfs["ipfs implementations"] helia; kubo; js-ipfs; end - dbProxyWorker<-.message channel.->dbWorker + subgraph queues["message brokers"] + ipfs_queue["ipfs load balancer"]; + queue["data processing queue aka bus"]; + end + + subgraph rune["rune"] + vm["virtual machine"]--ovm_bingen{{"cyb bindings"}}; + end + + subgraph sense["sense"] + link_sync["link sync"]; + msg_sync["message sync"]; + swarm_sync["swarm sync"]; + end + + subgraph ml["ML transformers"] + feature_extractor["embedder"]; + end + + end +``` + +### Ipfs module + +Represented with IpfsApi at frontend layer, but also have direct access for some edge cases + +- Uses module that encapsulate different Ipfs implementations(kubo, helia, js-ipfs(obsolete)) + - cache content(local storage & cozo) + - preserve redundancy +- Ipfs queue, process all requests to ipfs, prioritize, cancel non-actual requests and organize content pipeline + - responsible for: + - ipfs load balancing(limit of requests) + - request prioritizing(actual requests first) + - fault processing(switch fetch policy) + - post processing(**inline rune vm** into pipeline) + +```mermaid +graph LR +user(ipfsApi\nenqueue particle) --> q[["queue\n(balancer)"]] --> node[/"ipfs"/] -- found --> rune[rune vm] -- mutation | content --> cache["cache"] --> app(app\ncontent) +node -. not found\n(retry | error) .-> q +``` + +## Bus + +Represented with some helpers and used for cases when blaancer is needed, some services not initialized yet(deffered actions), or long calculations is requered(ml inference, ipfs requests): + +- particle, request ipfs, save; calc embedding +- link, deffered save +- message persistence is protected by db store + +```mermaid +graph TD; + sender{{"enqueue(...)"}} -.message bus.-> bus + subgraph task["task manager"] + bus[["queue listener"]]; + + bus-.task.->db("store\ndata")--odb1["dbApi"]; + bus-.task.->ml("calculate\nembedding")--oml1["mlApi"]; + bus-.task.->ipfs("request ipfs\nlow-priority")--oi["ipfsApi"] + end +``` + +## Sense + +Represented by SenseApi + subscription to broadcast channel at fronted layer. Provide continious update of cyberlinks related to my brain and my swarm, recieving on chain messages etc.: + +- Particles service (pooling) +- Transactions service (pooling + websocket) +- My friends service (pooling) +- Ipfs service(pooling) + +All data and update status is stored into db, when some new data is recieved that triggers notification for frontendю + +```mermaid +graph TD; + db[["dbApi"]]; + bus[["particle queue"]]; + + subgraph sense["sync service"] + notification("notification service") + + particles[["particle service"]]--onotification; + transactions[["transaction service"]]--onotification; + myfriend[["my friends service"]]--onotification; + + particles -.loop.-> particles; + transactions -.loop.-> transactions; + myfriend -.loop.-> myfriend; + end + + + subgraph blockchain["blockchain"] + lcd[["lcd"]] + websockets("websockets") + indexer[["indexer"]] + end + + subgraph app["frontend"] + redux["redux"] + sender{{"senseApi"}}; + end + + notification -.message.-> redux; + sender -.proxy.-> db; + sense -.proxy.-> db; + sense -.message.-> bus; + bus -.proxy.-> db; + + sense <-.request\nsubscriptin.->blockchain; + +``` + +## Rune + +Rune VM execution is pipelined thru special abstraction called entrypoints. VM have bindings to all app parts: DB, transformers, signer, blockchain api, ipfs and also includes context of the entrypoint.(see. [[scripting]] for detailed description). + +## ML transformers + +Represented my mlApi. Uses inference from local ML models hosted inside browser. + +- future extractor. BERT-like model to trnsform text-to-embeddings. + +```mermaid +graph TD; + subgraph ml["transformers"] + embedder["embedder"]; + end + + subgraph dbApi["dbApi"] + db[["DB"]]; end + mlApi["mlApi"]; + mlApi--odb; + mlApi--oembedder; ``` diff --git a/docs/scripting.md b/docs/scripting.md new file mode 100644 index 000000000..0b0c78715 --- /dev/null +++ b/docs/scripting.md @@ -0,0 +1,407 @@ +# soul: scripting guide + +[rune Language]: https://rune-rs.github.io +[cyb]: https://cyb.ai + +[cyb] uses [rune Language] for embeddable scripting aka [[cybscript]]. + +rune is virtual machine that runs inside cyb and process all commands and rendering results + +## why we choose new language? + +rune is dynamic, compact, portable, async and fast scripting language which is specially targeted to rust developers. + +to our dismay we was not able to find any other way to provide dynamic execution in existing browsers which are compatable with future wasm based browsers. + +rune is wasm module written in rust. + +we hope you will enjoy it. + +Using cybscript any cyber citizen can + +- tune-up his [[soul]] +- extend and modify robot behaivior and functionality + +soul is stored in [[ipfs]] and is linked directly to avatars passport. + +## CYB module + +cyb module provide bindings that connect cyber-scripting with app and extend [Rune language] functionality. + +#### Distributed computing + +Allows to evaluate code from external IPFS scripts in verifiable way, execute remote computations + +``` +// Evaluate function from IPFS +cyb::eval_script_from_ipfs(cid,'func_name', #{'name': 'john-the-baptist', 'evangelist': true, 'age': 33}) + +// Evaluate remote function(in developement) +cyb::eval_remote_script(peer_id, func_name, params) +``` + +#### Passport + +Get info about Citizenship + +``` +// Get passport data by nickname +cyb::get_passport_by_nickname(nickname: string) -> json; +``` + +#### Cyber links + +Work with Knowelege Graph + +``` +cyb::get_cyberlinks_from_cid(cid: string) -> json; +cyb::get_cyberlinks_to_cid(cid: string) -> json; + +// Search links by text or cid +cyb::cyber_search(query: string) -> json; + +// Create cyber link +// (this action requires trigger signer) +cyb::cyber_link(from_cid: string, to_cid: string); +``` + +#### IPFS + +Work with IPFS + +``` +cyb::get_text_from_ipfs(cid: string) -> string; +cyb::add_content_to_ipfs(text: string); +``` + +#### Local relational-graph-vector database + +Access to [[cozo]] and your [[brain]] data represented in text and vector-based formats. + +``` +// return N closest particles based on embeddings of each +cyb::search_by_embedding(text:string, count: int) -> string[]; +``` + +#### Experemental + +OpenAI promts(beta) + +- api key should be added using cyb-keys +- this is wrapper around [openai api](https://platform.openai.com/docs/api-reference/chat/create) + +``` +// Apply prompt OpenAI and get result +cyb::open_ai_completions(messages: object[]; apiKey: string; params: json) -> string | AsyncIterable; +``` + +#### Debug + +Logging and debug methods + +``` +// Add debug info to script output +dbg(`personal_processor ${cid} ${content_type} ${content}`); + +// console.log +cyb:log("blah"); +``` + +## Entrypoints + +Entrypoint is important concept of cyber scripting, literally that is the place where cyber-script is inlined into app pipeline. +At the moment all entrypoint types is related to some particle in cyber: + +- Moon Domain Resolver +- Personal Processor +- Ask Companion + +### Entrypoint principles + +Each entrypoint is function that accept some arguments as **input**. + +``` +pub async fn personal_processor(cid, content_type, content) { + // ... // +} +``` + +_`personal_processor` is entrypoint for each-single particle(see. furter)_ + +Second convention that each entrypoint should return **output** object - with one required property named _action_ and some optional props depending on action for ex. `#{action: 'pass'}`. + +Cyber-scripting has helpers to construct object-like responses, with mandatory "action" field and some optional fields: + +``` +pass() // pass untouched = #{action: 'pass'} + +hide() // hide particle = #{ "action": "hide" } + +cid_result(cid) // change particle's cid and parse = #{ "action": "cid_result", "cid": cid } + +content_result(content) // modify particle content = #{ "action": "content_result", "content": content } + +error(message) // error ^_^ = #{ "action": "error", "message": message } +``` + +So minimal entrypoint looks like this: + +``` +pub async fn personal_processor(cid, content_type, content) { + return pass() // keep data stream untouched +} +``` + +### Entrypoint types + +#### Particle processor + +Every single particle goes thru the pipeline and **personal_processor** function is applied to it content: + +```mermaid +graph LR +A[particle] -- cid --> B[IPFS] -- content --> C(("personal
processor")) -- content mutation --> D(app) +``` + +``` +// params: +// cid: CID of the content +// content_type: text, image, link, pdf, video, directory, html etc... +// content: (text only supported at the moment) +pub async fn personal_processor(cid, content_type, content) { + /* SOME CODE */ +} +``` + +User can do any transformation of the particle in pipeline + +``` +// Update content +return content_result("Transformed content") + +// Replace CID -> re-apply new particle +return cid_result("Qm.....") + +// Hide item from UI +return hide() + +// Keep it as is +return pass() +``` + +#### .moon domain resolver + +Every user can write his own .moon domain resolver: _[username].moon_. When any other user meep particle with exactly such text, entrypoint will be executed. + +```mermaid +graph LR +B(username.moon) -- username --> C["cyber
passport
particle"] -- cid --> D(IPFS) -- script code --> E(( particle
resolver)) -- render content --> F(result) +``` + +Minimal resolver can looks like this: \* _no input params but context is used(user that look at your domain)_ + +``` +pub async fn moon_domain_resolver() { + + // particle consumer from context + let name = cyb::context.nickname; + + // respond + // as string + return content_result(`Hello ${name}!`) + + // or CID(can be any text/image or even app hosted inside IPFS) + // return cid_result("QmcqikiVZJLmum6QRDH7kmLSUuvoPvNiDnCKY4A5nuRw17") +} +``` + +And there is minimal personal processor that process domain and run resolver from remote user script. + +``` +pub async fn personal_processor(cid, content_type, content) { + // check if text is passed here and it's looks like .moon domain + if content_type == "text" and content.ends_with(".moon") { + let items = content.split(".").collect::(); + let username = items[0]; + let ext = items[1]; + if username.len() <= 14 && ext == "moon" { + + // get citizenship data by username + let passport = cyb::get_passport_by_nickname(username).await; + + // get personal particle + let particle_cid = passport["extension"]["particle"]; + + // log to browser console + cyb::log(`Resolve ${username} domain from passport particle '${particle_cid}'`); + + // execute user 'moon_domain_resolver' function from 'soul' script with zero params + return cyb::eval_script_from_ipfs(particle_cid, "moon_domain_resolver", []).await; + } + } +} +``` + +#### Ask Companion + +User can extend UI of the particle with custom controls. User can pass meta items as script result and cyb will render as UI extension. +At the moment we have 2 meta UI items: + +- text: `meta_text("title")` +- link: `meta_link("/@master", "link to user named @master")` + +```mermaid +graph LR +E(user) -- search input --> C(("ask
Companion")) -- companion payload --> D(meta UI) +``` + +``` +pub async fn ask_companion(cid, content_type, content) { + // plain text item + let links = [meta_text("similar:")]; + + // search closest 5 particles using local data from the brain(embedding-search) + let similar_results = cyb::search_by_embedding(content, 5).await; + + + for v in similar_results { + // link item + links.push(meta_link(`/oracle/ask/${v.cid}`, v.text)); + } + + return content_result(links) +} +``` + +### Context + +One of important thing, that can be used inside scripting is the context. +Context point to place and obstacles where entrypoint was triggered. Context is stored in `cyb::context` and contains such values: + +- params(url params) + - path / query / search +- user(user that executes entrypoint) + - address / nickname / passport +- secrets(key-value list from the cyber app) + - key/value storage + +``` + +// nick of user that see this particle(in case of domain resolver) +let name = cyb::context.user.nickname; + +// user particle that contains soul, that can be interracted directly from your soul +let particle = cyb::context.particle; + +// Get list of url parameters (in progress) +let path = cyb::context.params.path; + +//get some secret (in progress) +let open_ai_api_key = cyb::context.secrets.open_ai_api_key; + +``` + +## Advanced examples + +#### Personal processor + +``` +// your content for .moon domain +pub async fn moon_domain_resolver() { + // get nickname of domain resolver at the momemnt + let nickname = cyb::context.user.nickname; + + let rng = rand::WyRand::new(); + let rand_int = rng.int_range(0, 999999); + + return content_result(`Hello, ${nickname}, your lucky number is ${rand_int} 🎉`); + + // substitute with some CID (ipfs hosted app in this case) + // return cid_result("QmcqikiVZJLmum6QRDH7kmLSUuvoPvNiDnCKY4A5nuRw17") +} + +// Extend particle page with custom UI elements +pub async fn ask_companion(cid, content_type, content) { + // plain text item + let links = [meta_text("similar:")]; + + // search closest 5 particles using local data from the brain + let similar_results = cyb::search_by_embedding(content, 5).await; + + + for v in similar_results { + // link item + links.push(meta_link(`/oracle/ask/${v.cid}`, v.text)); + } + + return content_result(links) +} + +// Transform content of the particle +pub async fn personal_processor(cid, content_type, content) { + + // skip any non-text content + if content_type != "text" { + return pass() + } + + // .moon domain resolver + if content.ends_with(".moon") { + let items = content.split(".").collect::(); + + let username = items[0]; + let ext = items[1]; + + if username.len() <= 14 && ext == "moon" { + + // get passport data by username + let passport = cyb::get_passport_by_nickname(username).await; + + // particle - CID of soul script + let particle_cid = passport["extension"]["particle"]; + + cyb::log(`Resolve ${username} domain from passport particle '${particle_cid}'`); + + // resolve content(script) by cid + // evaluate 'moon_domain_resolver' from that + let result = cyb::eval_script_from_ipfs(particle_cid, "moon_domain_resolver", []).await; + + return result + } + } + + // example of content exclusion from the search results + let buzz_word = "пиздопроебанное хуеплетство"; + + if content.contains(buzz_word) { + cyb::log(`Hide ${cid} item because of '${buzz_word}' in the content`); + return hide() + } + + + // example of content modification + // replaces cyber with cyber❤ + let highlight_text = "cyber"; + let highlight_with = "❤"; + + if content.contains(highlight_text) { + cyb::log(`Update ${cid} content, highlight ${highlight_text}${highlight_with}`); + return content_result(content.replace(highlight_text, `${highlight_text}${highlight_with}`)) + } + + // replace @NOW (ex. bitcoin@NOW) with actual price in usdt + // using external api call + if content.contains("@NOW") { + let left_part = content.split("@NOW").next().unwrap(); + let token_name = left_part.split(" ").rev().next().unwrap(); + let vs_currency = "usd"; + + // external url call + let json = http::get(`https://api.coingecko.com/api/v3/simple/price?ids=${token_name}&vs_currencies=${vs_currency}`).await?.json().await?; + return content_result(content.replace(`${token_name}@NOW`, `Current ${token_name} price is ${json[token_name][vs_currency]} ${vs_currency}`)) + } + + // anything else - pass as is + pass() +} +``` diff --git a/package.json b/package.json index 265370f24..97e7f7bb2 100644 --- a/package.json +++ b/package.json @@ -179,6 +179,7 @@ "@types/react-dom": "^18.0.11", "@types/react-router-dom": "^5.3.3", "@uniswap/sdk": "^3.0.3", + "@xenova/transformers": "^2.17.0", "apollo-boost": "^0.4.7", "bech32": "^1.1.3", "big.js": "^5.2.2", @@ -187,10 +188,12 @@ "blockstore-idb": "^1.1.4", "cjs-to-es6": "^2.0.1", "classnames": "^2.3.1", + "codemirror": "5.57.0", "comlink": "^4.4.1", "core-js": "^3.30.0", "crypto": "^1.0.1", "cyb-cozo-lib-wasm": "^0.7.145", + "cyb-rune-wasm": "^0.0.843", "datastore-core": "^9.2.3", "datastore-idb": "^2.1.4", "dateformat": "^3.0.3", @@ -228,6 +231,7 @@ "raw-loader": "^4.0.2", "rc-slider": "^9.7.2", "react": "^18.0.0", + "react-codemirror2": "^7.2.1", "react-dom": "^18.0.0", "react-force-graph": "^1.39.5", "react-helmet": "^6.1.0", @@ -241,6 +245,7 @@ "react-transition-group": "^4.4.2", "read-chunk": "^4.0.3", "readable-stream": "^4.3.0", + "redux-observable": "^3.0.0-rc.2", "redux-thunk": "^2.4.2", "regenerator-runtime": "^0.13.7", "remark-breaks": "^3.0.3", diff --git a/public/images/cyb-map.png b/public/images/cyb-map.png new file mode 100644 index 000000000..da9b0f21b Binary files /dev/null and b/public/images/cyb-map.png differ diff --git a/src/components/CIDResolver/CIDResolver.tsx b/src/components/CIDResolver/CIDResolver.tsx new file mode 100644 index 000000000..4b928c81a --- /dev/null +++ b/src/components/CIDResolver/CIDResolver.tsx @@ -0,0 +1,30 @@ +import { trimString } from 'src/utils/utils'; +import { Cid } from '../link/link'; +import useParticleDetails from 'src/features/particle/useParticleDetails'; +import { Dots } from '../ui/Dots'; + +type Props = { + cid: string; +}; + +function CIDResolver({ cid }: Props) { + const { data, loading } = useParticleDetails(cid); + + if (!data?.content) { + return ( + + {loading ? ( + <> + loading + + ) : ( + trimString(cid, 3, 3) + )} + + ); + } + + return {data.content}; +} + +export default CIDResolver; diff --git a/src/components/ContentItem/contentItem.tsx b/src/components/ContentItem/contentItem.tsx index 0d7c837cb..66fc8bea7 100644 --- a/src/components/ContentItem/contentItem.tsx +++ b/src/components/ContentItem/contentItem.tsx @@ -1,15 +1,11 @@ // TODO: refactor needed -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { Link } from 'react-router-dom'; import { LinksType } from 'src/containers/Search/types'; -import useQueueIpfsContent from 'src/hooks/useQueueIpfsContent'; -import type { - IPFSContentDetails, - IpfsContentType, -} from 'src/services/ipfs/types'; +import useParticle from 'src/hooks/useParticle'; +import type { IpfsContentType } from 'src/services/ipfs/types'; import { $TsFixMe } from 'src/types/tsfix'; -import { parseArrayLikeToDetails } from 'src/services/ipfs/utils/content'; import SearchItem from '../SearchItem/searchItem'; @@ -17,12 +13,12 @@ import { getRankGrade } from '../../utils/search/utils'; import ContentIpfs from '../contentIpfs/contentIpfs'; type ContentItemProps = { - item: $TsFixMe; + item?: $TsFixMe; cid: string; grade?: $TsFixMe; className?: string; parent?: string; - linkType: LinksType; + linkType?: LinksType; setType?: (type: IpfsContentType) => void; }; @@ -35,24 +31,15 @@ function ContentItem({ setType, className, }: ContentItemProps): JSX.Element { - const [details, setDetails] = useState(undefined); - const { status, content, fetchParticle } = useQueueIpfsContent(parentId); + const { details, status, hidden, content } = useParticle(cid, parentId); useEffect(() => { - fetchParticle && (async () => fetchParticle(cid, item?.rank))(); - }, [cid, item?.rank, fetchParticle]); + details?.type && setType && setType(details?.type); + }, [details]); // TODO: REFACT - setType rise infinite loop - useEffect(() => { - (async () => { - const details = await parseArrayLikeToDetails( - content, - cid - // (progress: number) => console.log(`${cid} progress: ${progress}`) - ); - setDetails(details); - details?.type && setType && setType(details?.type); - })(); - }, [content, cid]); //TODO: REFACT - setType rise infinite loop + if (hidden) { + return
; + } return ( diff --git a/src/components/CreatedAt/CreatedAt.tsx b/src/components/CreatedAt/CreatedAt.tsx index c63e6b3c0..f1c80d16d 100644 --- a/src/components/CreatedAt/CreatedAt.tsx +++ b/src/components/CreatedAt/CreatedAt.tsx @@ -1,4 +1,5 @@ -import { getNowUtcTime, timeSince } from 'src/utils/utils'; +import { timeSince } from 'src/utils/utils'; +import { dateToUtcNumber, getNowUtcNumber } from 'src/utils/date'; import styles from './CreatedAt.module.scss'; export type Props = { @@ -8,7 +9,9 @@ export type Props = { function CreatedAt({ timeAt }: Props) { let timeAgoInMS = 0; - const time = getNowUtcTime() - new Date(timeAt).getTime(); + const timeUtc = typeof timeAt === 'string' ? dateToUtcNumber(timeAt) : timeAt; + + const time = getNowUtcNumber() - timeUtc; if (time && time > 0) { timeAgoInMS = time; } @@ -16,9 +19,9 @@ function CreatedAt({ timeAt }: Props) { const timeSinceValue = timeSince(timeAgoInMS); return ( -
+ {timeSinceValue === 'now' ? timeSinceValue : `${timeSinceValue} ago`} -
+ ); } diff --git a/src/components/DebugContentInfo/DebugContentInfo.tsx b/src/components/DebugContentInfo/DebugContentInfo.tsx index da5fe1ffa..c61a9c1a2 100644 --- a/src/components/DebugContentInfo/DebugContentInfo.tsx +++ b/src/components/DebugContentInfo/DebugContentInfo.tsx @@ -1,5 +1,6 @@ -import { IPFSContentMaybe, IpfsContentSource } from 'src/utils/ipfs/ipfs'; +import { IpfsContent, IpfsContentSource } from 'src/utils/ipfs/ipfs'; import styles from './DebugContentInfo.module.scss'; +import { Option } from 'src/types'; function DebugContentInfo({ cid, @@ -8,9 +9,9 @@ function DebugContentInfo({ status, }: { cid: string; - source: IpfsContentSource | undefined; - content: IPFSContentMaybe; - status: string | undefined; + source: Option; + content: Option; + status: Option; }) { const meta = content ? content.meta : undefined; const measurementInfo = diff --git a/src/components/Dropdown/Dropdown.module.scss b/src/components/Dropdown/Dropdown.module.scss index c88a7e2a0..2d03422c3 100644 --- a/src/components/Dropdown/Dropdown.module.scss +++ b/src/components/Dropdown/Dropdown.module.scss @@ -3,6 +3,7 @@ padding: 0 15px; > button { + // REFACTOR: use Triangle component color: var(--blue-light); display: flex; diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index d1e70537b..3e1787278 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -17,6 +17,7 @@ function Dropdown({ options = [], value, onChange }: Props) { return (
+ {/* REFACTOR: use Triangle component */} {isOpen && ( -
+
{/* {placeholder && ( diff --git a/src/components/Table/Table.module.scss b/src/components/Table/Table.module.scss index 4b5840ba4..264701007 100644 --- a/src/components/Table/Table.module.scss +++ b/src/components/Table/Table.module.scss @@ -3,11 +3,24 @@ .table { border-collapse: collapse; width: 100%; + padding: 0 8px; - td, - th { - padding: 18px; - text-align: center; + tr { + th, + td { + padding: 8px; + text-align: center; + height: 64px; + + $padding: 16px; + + &:first-of-type { + padding-left: $padding; + } + &:last-of-type { + padding-right: $padding; + } + } } thead { @@ -15,6 +28,22 @@ th { font-weight: 400; + position: relative; + + &.sortable { + cursor: pointer; + white-space: nowrap; + + > span { + margin-left: 3px; + } + } + + &:hover { + .resizer { + opacity: 1; + } + } } } @@ -27,7 +56,7 @@ } &.rowSelected { - box-shadow: inset 0px 0px 0px 1px #36d6ae; + @include tableHover; } &:hover { @@ -35,4 +64,26 @@ } } } + + &.selectable tbody tr { + cursor: pointer; + } } + +// .resizer { +// position: absolute; +// top: 0; +// height: 100%; +// right: 0; +// width: 5px; +// background: red; +// cursor: col-resize; +// user-select: none; +// touch-action: none; +// opacity: 0; +// } + +// .isResizing { +// background: blue; +// opacity: 1; +// } diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 8136f070d..5e11cfdb9 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -4,20 +4,55 @@ import { ColumnDef, getCoreRowModel, flexRender, + getSortedRowModel, + InitialTableState, + SortingState, + TableOptions, + SortingOptions, } from '@tanstack/react-table'; import styles from './Table.module.scss'; import Loader2 from '../ui/Loader2'; import NoItems from '../ui/noItems'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import cx from 'classnames'; +import Triangle from '../atoms/Triangle/Triangle'; +import { sessionStorageKeys } from 'src/constants/sessionStorageKeys'; +import { tableIDs } from './tableIDs'; + +const storage = sessionStorage; +const SS_KEY = sessionStorageKeys.tableSorting; + +type TableIDs = typeof tableIDs; + +function getDataFromStorage(): Record { + return JSON.parse(storage.getItem(SS_KEY) || '{}'); +} + +function saveDataToStorage(id: string, sorting: SortingState) { + storage.setItem( + SS_KEY, + JSON.stringify({ + ...getDataFromStorage(), + [id]: sorting, + }) + ); +} export type Props = { columns: ColumnDef[]; data: T[]; isLoading?: boolean; onSelect?: (id: string | null) => void; + + // prefer not use style?: any; + id?: keyof TableIDs; + + initialState?: InitialTableState; + + // maybe temporary + enableSorting?: boolean; }; function Table({ @@ -26,37 +61,102 @@ function Table({ isLoading, style, onSelect, + initialState, + id, + enableSorting = true, }: Props) { const [selected, setSelected] = useState(null); + const savedSorting = id && getDataFromStorage()[id]; + const [sorting, setSorting] = useState( + savedSorting || initialState?.sorting || [] + ); + + useEffect(() => { + if (!id) { + return; + } + + saveDataToStorage(id, sorting); + }, [sorting, id]); + + const handleSortingChange = useCallback( + (sorting: TableOptions['onSortingChange']) => { + setSorting(sorting); + }, + [] + ); + const table = useReactTable({ + // debugTable: true, columns, data, state: { rowSelection: {}, + sorting, }, - // debugTable: true, + initialState, + enableSorting, // enableRowSelection: true, // onRowSelectionChange: (state) => { // console.log(state); // debugger; // }, + + // enableColumnResizing: true, + // columnResizeMode: 'onChange', + onSortingChange: handleSortingChange, getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), }); return ( <> - +
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( - ); })} @@ -78,6 +178,14 @@ function Table({ return; } + if ( + ['a', 'button', 'input'].includes( + (e.target as any).tagName.toLowerCase() + ) + ) { + return; + } + const id = e.currentTarget.getAttribute('data-id'); if (id === selected) { @@ -92,8 +200,12 @@ function Table({ > {row.getVisibleCells().map((cell) => { return ( - // eslint-disable-next-line react/jsx-key -
+ {flexRender( header.column.columnDef.header, header.getContext() )} +   + {header.column.getCanSort() && ( + + )} + {/*
header.column.resetSize(), + onMouseDown: header.getResizeHandler(), + onTouchStart: header.getResizeHandler(), + className: `${styles.resizer} ${ + header.column.getIsResizing() ? styles.isResizing : '' + }`, + }} + /> */}
+ {flexRender( cell.column.columnDef.cell, cell.getContext() diff --git a/src/components/Table/tableIDs.ts b/src/components/Table/tableIDs.ts new file mode 100644 index 000000000..ed7322457 --- /dev/null +++ b/src/components/Table/tableIDs.ts @@ -0,0 +1,7 @@ +export const tableIDs = { + cyberver: { + subnets: 'subnets', + subnetNeurons: 'subnetNeurons', + delegates: 'delegates', + }, +}; diff --git a/src/components/Tabs/TabItem/TabItem.tsx b/src/components/Tabs/TabItem/TabItem.tsx index 89241b7f8..2c85ad942 100644 --- a/src/components/Tabs/TabItem/TabItem.tsx +++ b/src/components/Tabs/TabItem/TabItem.tsx @@ -1,5 +1,6 @@ import cx from 'classnames'; import { Link } from 'react-router-dom'; +import { ReactNode } from 'react'; import styles from './TabItem.module.scss'; export const enum Position { @@ -9,7 +10,7 @@ export const enum Position { export type Props = { type?: Position; - text: string | JSX.Element; + text: string | ReactNode; step?: number; isSelected: boolean; to?: string; @@ -39,6 +40,13 @@ function TabItem({ }; } + if (disable && to) { + componentProps = { + ...componentProps, + onClick: (e) => e.preventDefault(), + }; + } + return ( void; key: string; - text?: string; + text?: ReactNode; }; type Props = { diff --git a/src/components/TextMarkdown/styles.module.scss b/src/components/TextMarkdown/styles.module.scss index 5fd4da081..faa67083e 100644 --- a/src/components/TextMarkdown/styles.module.scss +++ b/src/components/TextMarkdown/styles.module.scss @@ -28,10 +28,8 @@ .markdownContainer { margin: 0 auto; - width: clamp(320px, 100%, 580px); min-height: 200px; - display: flex; justify-content: center; flex-direction: column; @@ -51,7 +49,7 @@ margin-left: 20px; } - @media (max-width: 680px) { + @media (width <= 680px) { width: unset; } } @@ -71,7 +69,6 @@ .markdownContainer p { margin-bottom: 20px; - line-height: 23px; &:last-child { margin-bottom: 0; @@ -99,7 +96,7 @@ border: 0; th { - border-bottom: 1px solid rgba(255, 255, 255, 0.5); + border-bottom: 1px solid rgb(255 255 255 / 50%); border-collapse: separate; border-spacing: 5px 5px; } @@ -120,7 +117,7 @@ } p:last-child { - margin-bottom: 0px; + margin-bottom: 0; font-size: 16px; } diff --git a/src/components/VideoPlayer/VideoPlayerGatewayOnly.tsx b/src/components/VideoPlayer/VideoPlayerGatewayOnly.tsx index 82bb49348..cb3f2323b 100644 --- a/src/components/VideoPlayer/VideoPlayerGatewayOnly.tsx +++ b/src/components/VideoPlayer/VideoPlayerGatewayOnly.tsx @@ -1,11 +1,11 @@ /* eslint-disable no-restricted-syntax */ import { useEffect, useState } from 'react'; -import { IPFSContentDetails, IPFSContentMaybe } from 'src/services/ipfs/types'; +import { IPFSContent, IPFSContentDetails } from 'src/services/ipfs/types'; import { useBackend } from 'src/contexts/backend/backend'; import { CYBER_GATEWAY_URL } from 'src/services/ipfs/config'; interface VideoPlayerProps { - content: IPFSContentMaybe; + content: IPFSContent; details: IPFSContentDetails; } diff --git a/src/components/account/account.module.scss b/src/components/account/account.module.scss index 4e67923aa..be5a67013 100644 --- a/src/components/account/account.module.scss +++ b/src/components/account/account.module.scss @@ -15,4 +15,9 @@ color: var(--primary-color); padding: 0; white-space: nowrap; + + width: 100%; + overflow-x: hidden; + text-overflow: ellipsis; + text-align: center; } \ No newline at end of file diff --git a/src/components/account/account.tsx b/src/components/account/account.tsx index 985c9e78d..d6214d903 100644 --- a/src/components/account/account.tsx +++ b/src/components/account/account.tsx @@ -9,6 +9,8 @@ import { BECH32_PREFIX_VALOPER } from 'src/constants/config'; import { trimString } from '../../utils/utils'; import { AvataImgIpfs } from '../../containers/portal/components/avataIpfs'; import styles from './account.module.scss'; +import useCurrentAddress from 'src/hooks/useCurrentAddress'; +import Tooltip from '../tooltip/tooltip'; function useGetValidatorInfo(address: string) { const queryClient = useQueryClient(); @@ -16,11 +18,7 @@ function useGetValidatorInfo(address: string) { const { data } = useQuery( ['validatorInfo', address], async () => { - if (!queryClient) { - return null; - } - - const response = await queryClient.validator(address); + const response = await queryClient!.validator(address); return response; }, { @@ -47,6 +45,8 @@ type Props = { containerClassName?: string; avatarClassName?: string; monikerClassName?: string; + link?: string; + markCurrentAddress?: boolean; }; function Account({ @@ -56,12 +56,14 @@ function Account({ onlyAvatar, avatar, margin, + link, sizeAvatar, styleUser, trimAddressParam = [9, 3], disabled, containerClassName, avatarClassName, + markCurrentAddress, monikerClassName, }: Props) { const { data: dataValidInfo } = useGetValidatorInfo(address); @@ -74,7 +76,13 @@ function Account({ return trimString(address, trimAddressParam[0], trimAddressParam[1]); }, [address, trimAddressParam]); + const currentAddress = useCurrentAddress(); + const linkAddress = useMemo(() => { + if (link) { + return link; + } + if (address?.includes(BECH32_PREFIX_VALOPER)) { return `/network/bostrom/hero/${address}`; } @@ -84,7 +92,7 @@ function Account({ } return `/network/bostrom/contract/${address}`; - }, [address, moniker]); + }, [address, moniker, link]); const cidAvatar = useMemo(() => { if (dataPassport !== undefined && dataPassport !== null) { @@ -126,6 +134,10 @@ function Account({ {!moniker ? trimAddress : moniker} )} + + {markCurrentAddress && currentAddress === address && ( + 🔑 + )} {children} ); diff --git a/src/components/actionBar/index.tsx b/src/components/actionBar/index.tsx index 874ff9f7d..a54a09431 100644 --- a/src/components/actionBar/index.tsx +++ b/src/components/actionBar/index.tsx @@ -7,12 +7,12 @@ import { Networks } from 'src/types/networks'; import usePassportByAddress from 'src/features/passport/hooks/usePassportByAddress'; import { selectCurrentAddress } from 'src/redux/features/pocket'; import { useAppSelector } from 'src/redux/hooks'; -import ButtonIcon from '../buttons/ButtonIcon'; -import styles from './styles.module.scss'; -import Button from '../btnGrd'; import { useSigningClient } from 'src/contexts/signerClient'; import { trimString } from 'src/utils/utils'; import { CHAIN_ID } from 'src/constants/config'; +import ButtonIcon from '../buttons/ButtonIcon'; +import styles from './styles.module.scss'; +import Button from '../btnGrd'; const back = require('../../image/arrow-left-img.svg'); @@ -41,6 +41,7 @@ type Props = { onClick?: () => void; link?: string; disabled?: boolean; + pending?: boolean; }; }; @@ -120,7 +121,7 @@ function ActionBar({ children, text, onClickBack, button }: Props) { const content = text || children; - return ( + const contentPortal = ( {/* */} @@ -139,6 +140,7 @@ function ActionBar({ children, text, onClickBack, button }: Props) { {button?.text && ( + {getMenuItems().map((item, index) => { + const isExternal = item.to.startsWith('http'); + return ( + !isActiveItem(item) && ( + + {`${item.name} + {isExternal && } + + ) + ); + })} + + + ); +}; + +export default MobileMenu; diff --git a/src/components/appMenu/SubMenu/SubMenu.module.scss b/src/components/appMenu/SubMenu/SubMenu.module.scss new file mode 100644 index 000000000..8b88ae632 --- /dev/null +++ b/src/components/appMenu/SubMenu/SubMenu.module.scss @@ -0,0 +1,54 @@ +$icon-size: 16px; + +.subMenu { + // position: absolute; + display: flex; + flex-direction: column; + gap: 5px; +} + +@mixin active-el() { + content: ''; + left: 0; + height: 100%; + width: 3px; + background-color: #4ed6ae; + filter: blur(0.7px); + box-shadow: 3px 0px 20px 1.5px #4ed6ae; + position: absolute; +} + +.navLinkItem { + display: grid; + grid-template-columns: 25px 1fr; + gap: 7px; + align-items: center; + padding-left: 20px; + font-size: 16px; + // >div { + // align-items: center; + // flex-shrink: 0; + // } + + position: relative; + + color: #777777; + + .icon { + width: $icon-size; + height: $icon-size; + object-fit: contain; + } + + .nameApp { + white-space: nowrap; + } + + &.active { + color: #fff; + + &::before { + @include active-el(); + } + } +} diff --git a/src/components/appMenu/SubMenu/SubMenu.tsx b/src/components/appMenu/SubMenu/SubMenu.tsx new file mode 100644 index 000000000..c2f2778c9 --- /dev/null +++ b/src/components/appMenu/SubMenu/SubMenu.tsx @@ -0,0 +1,52 @@ +import { NavLink } from 'react-router-dom'; +import { MenuItem } from 'src/types/menu'; +import cx from 'classnames'; +import { useMemo } from 'react'; +import styles from './SubMenu.module.scss'; + +interface Props { + selectedApp: MenuItem; + closeMenu: () => void; +} + +function SubMenu({ selectedApp, closeMenu }: Props) { + const renderData = useMemo( + () => + selectedApp.subItems.length + ? [ + { + name: 'main', + to: selectedApp.to, + icon: selectedApp.icon, + }, + ...selectedApp.subItems, + ] + : [], + [selectedApp] + ); + + return ( +
+ {renderData.map((item) => ( + + cx(styles.navLinkItem, { + [styles.active]: isActive, + }) + } + onClick={closeMenu} + > + {item.icon && ( + icon + )} + {item.name} + + ))} +
+ ); +} + +export default SubMenu; diff --git a/src/components/atoms/Triangle/Triangle.module.scss b/src/components/atoms/Triangle/Triangle.module.scss new file mode 100644 index 000000000..d27a29ff2 --- /dev/null +++ b/src/components/atoms/Triangle/Triangle.module.scss @@ -0,0 +1,30 @@ +.triangle { + color: var(--blue-light); + width: 0; + + // &::before { + // content: ''; + // filter: blur(10px); + // position: absolute; + // top: 0; + // left: 0; + // width: 100%; + // height: 100%; + // z-index: -1; + // } + transform: rotate(180deg); + display: inline-block; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 9px solid var(--blue-light); + + &_up { + transform: rotate(0deg); + } + + &.disabled { + color: rgba(255, 255, 255, 0.5); + border-bottom-color: rgba(255, 255, 255, 0.5); + } +} diff --git a/src/components/atoms/Triangle/Triangle.stories.tsx b/src/components/atoms/Triangle/Triangle.stories.tsx new file mode 100644 index 000000000..dea75ec7e --- /dev/null +++ b/src/components/atoms/Triangle/Triangle.stories.tsx @@ -0,0 +1,32 @@ +/* eslint-disable import/no-unused-modules */ + +import { Meta, StoryObj } from '@storybook/react'; + +import Triangle from './Triangle'; + +const meta: Meta = { + component: Triangle, + title: 'atoms/Triangle', + parameters: { + design: { + type: 'figma', + url: '', + }, + }, +}; +export default meta; + +type Story = StoryObj; +type Props = React.ComponentProps; + +const defaultArgs: Props = {}; + +export const Main: Story = { + args: defaultArgs, +}; + +export const Disabled: Story = { + args: { + disabled: true, + }, +}; diff --git a/src/components/atoms/Triangle/Triangle.tsx b/src/components/atoms/Triangle/Triangle.tsx new file mode 100644 index 000000000..ae79d8166 --- /dev/null +++ b/src/components/atoms/Triangle/Triangle.tsx @@ -0,0 +1,20 @@ +import styles from './Triangle.module.scss'; +import cx from 'classnames'; + +type Props = { + direction?: 'up' | 'down'; + disabled?: boolean; +}; + +function Triangle({ direction = 'down', disabled }: Props) { + return ( + + ); +} + +export default Triangle; diff --git a/src/components/atoms/glass/mixins.scss b/src/components/atoms/glass/mixins.scss new file mode 100644 index 000000000..b145f1fd6 --- /dev/null +++ b/src/components/atoms/glass/mixins.scss @@ -0,0 +1,23 @@ +// https://www.sketch.com/s/b13841a7-cfd2-47e8-a114-efb8e29285af/a/JAOznjr + +@mixin glass { + backdrop-filter: blur(15px); + box-shadow: 0px 0px 10px rgba(75, 0, 130, 0.09); + background-color: rgba(75, 0, 130, 0.09); +} + +@mixin glassBackground { + position: relative; + + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; + + @include glass; + } +} diff --git a/src/components/btnGrd/index.tsx b/src/components/btnGrd/index.tsx index 6b3328d67..bbefd9314 100644 --- a/src/components/btnGrd/index.tsx +++ b/src/components/btnGrd/index.tsx @@ -1,8 +1,9 @@ import cx from 'classnames'; import { $TsFixMe } from 'src/types/tsfix'; +import React, { ComponentProps } from 'react'; +import { Link } from 'react-router-dom'; import { Dots } from '../ui/Dots'; import styles from './Button.module.scss'; -import { Link, LinkProps } from 'react-router-dom'; const audioBtn = require('../../sounds/main-button.mp3'); // const audioBtnHover = require('../../sounds/main-button-hover.mp3'); @@ -41,7 +42,6 @@ export type Props = { link?: string; onClick?: () => void; small?: boolean; - reloadDocument?: LinkProps['reloadDocument']; }; function Button({ @@ -54,7 +54,6 @@ function Button({ onClick, link, className, - reloadDocument, small, ...props }: Props) { @@ -92,21 +91,26 @@ function Button({ type: 'button', }; - // if http: will need to add tag - // TODO: http is supported by Link // link can't be disabled, it is button - if (link && !link.includes('http:') && !disabled) { + if (link && !disabled) { Component = Link; - componentProps = { + + const linkProps: ComponentProps = { to: link, - reloadDocument, }; + + if (link.startsWith('http')) { + linkProps.target = '_blank'; + linkProps.rel = 'noreferrer noopener'; + } + + componentProps = linkProps; } return ( ( + } + > + + + + {text100} + + ), +}; diff --git a/src/components/containerGradient/Display/Display.tsx b/src/components/containerGradient/Display/Display.tsx index 743effed2..5540ac44a 100644 --- a/src/components/containerGradient/Display/Display.tsx +++ b/src/components/containerGradient/Display/Display.tsx @@ -13,6 +13,8 @@ type Props = { status?: ColorLamp; noPaddingX?: boolean; + noPaddingY?: boolean; + noPadding?: boolean; sideSaber?: 'left' | 'right'; @@ -26,6 +28,8 @@ function Display({ isVertical, title, noPaddingX, + noPaddingY, + noPadding, sideSaber, color = Colors.GREEN, status, @@ -36,17 +40,16 @@ function Display({
{title && ( -
- {React.cloneElement(title, { inDisplay: true })} -
+
{React.cloneElement(title, { inDisplay: true })}
)} - {children} +
{children}
); } diff --git a/src/components/containerGradient/DisplayTitle/DisplayTitle.module.scss b/src/components/containerGradient/DisplayTitle/DisplayTitle.module.scss index 925872d3c..eea817bc4 100644 --- a/src/components/containerGradient/DisplayTitle/DisplayTitle.module.scss +++ b/src/components/containerGradient/DisplayTitle/DisplayTitle.module.scss @@ -3,14 +3,13 @@ @import '../Display/Display.module.scss'; .noPaddingWrapper { - $offset: -$display-padding-x - $saber-border-width; position: relative; + $offset: -$saber-border-width; left: $offset; - margin-right: $offset; + // margin-right: $offset; } .displayTitle { - min-height: 70px; padding: 10px 0; font-size: 20px; display: flex; @@ -18,6 +17,7 @@ position: relative; padding-left: $display-padding-x; + padding-right: $display-padding-x; @each $color in $valid-colors { &.#{$color} { @@ -27,8 +27,8 @@ &Content { display: flex; + min-height: 36px; align-items: center; - min-height: 32px; gap: 0 10px; line-height: 25px; width: 100%; diff --git a/src/components/containerGradient/DisplayTitle/DisplayTitle.tsx b/src/components/containerGradient/DisplayTitle/DisplayTitle.tsx index 16376acbc..36d49b9c8 100644 --- a/src/components/containerGradient/DisplayTitle/DisplayTitle.tsx +++ b/src/components/containerGradient/DisplayTitle/DisplayTitle.tsx @@ -48,7 +48,7 @@ function DisplayTitle({ {title} -
{children}
+ {children &&
{children}
} ); diff --git a/src/components/containerGradient/saber/index.module.scss b/src/components/containerGradient/saber/index.module.scss index 866550364..eea593d38 100644 --- a/src/components/containerGradient/saber/index.module.scss +++ b/src/components/containerGradient/saber/index.module.scss @@ -18,7 +18,7 @@ background: linear-gradient( /* vise versa */ to #{$position}, - rgb(0, 0, 0) 0%, + rgba(0, 0, 0, 0) 0%, rgba(var(--color-r), var(--color-g), var(--color-b), 0.11) 90%, rgba(var(--color-r), var(--color-g), var(--color-b), 0.15) 95%, rgba(var(--color-r), var(--color-g), var(--color-b), 0.1) 100% diff --git a/src/components/contentIpfs/contentIpfs.tsx b/src/components/contentIpfs/contentIpfs.tsx index 469f1ef7e..7b9d0e90d 100644 --- a/src/components/contentIpfs/contentIpfs.tsx +++ b/src/components/contentIpfs/contentIpfs.tsx @@ -1,6 +1,7 @@ import { CYBER_GATEWAY } from 'src/constants/config'; import { CYBER_GATEWAY_URL } from 'src/services/ipfs/config'; -import { IPFSContentDetails, IPFSContentMaybe } from 'src/services/ipfs/types'; +import { IPFSContent, IPFSContentDetails } from 'src/services/ipfs/types'; +import { Option } from 'src/types'; import EPubView from '../EPubView/EPubView'; import Pdf from '../PDF'; import TextMarkdown from '../TextMarkdown'; @@ -27,6 +28,10 @@ function OtherItem({ return ; } +function HtmlItem({ cid }: { cid: string }) { + return ; +} + function DownloadableItem({ cid, search }: { cid: string; search?: boolean }) { if (search) { return
{`${cid} (gateway)`}
; @@ -36,7 +41,7 @@ function DownloadableItem({ cid, search }: { cid: string; search?: boolean }) { type ContentTabProps = { details: IPFSContentDetails; - content?: IPFSContentMaybe; + content?: Option; cid: string; search?: boolean; }; @@ -79,13 +84,14 @@ function ContentIpfs({ details, content, cid, search }: ContentTabProps) { {contentType === 'link' && ( )} + {contentType === 'html' && } {contentType === 'epub' && ( )} - {contentType === 'other' && ( + {['other', 'cid'].some((i) => i === contentType) && ( )} diff --git a/src/components/index.js b/src/components/index.js index 4368ea0b8..ee2d459b3 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,22 +1,13 @@ import { - JsonTransaction, TransactionSubmitted, Confirmed, - ConnectLadger, - Cyberlink, StartStageSearchActionBar, Delegate, ActionBarSend, - GovernanceStartStageActionBar, - CommunityPool, - TextProposal, RewardsDelegators, ReDelegate, TransactionError, ActionBarContentText, - CheckAddressInfo, - GovernanceChangeParam, - GovernanceSoftwareUpgrade, ConnectAddress, } from './ledger/stageActionBar'; import Account from './account/account'; @@ -54,6 +45,7 @@ import ButtonSwap from './ButtonSwap'; import Slider from './Slider/Slider'; import CreatedAt from './CreatedAt/CreatedAt'; import Tabs from './Tabs/Tabs'; +import Time from './time/time'; import Row, { RowsContainer } from './Row/Row'; import Display from './containerGradient/Display/Display'; import DisplayTitle from './containerGradient/DisplayTitle/DisplayTitle'; @@ -110,6 +102,7 @@ export { Slider, CreatedAt, Tabs, + Time, Row, RowsContainer, Display, diff --git a/src/components/ledger/stageActionBar.jsx b/src/components/ledger/stageActionBar.jsx index da4995def..c02175441 100644 --- a/src/components/ledger/stageActionBar.jsx +++ b/src/components/ledger/stageActionBar.jsx @@ -1,10 +1,10 @@ import LocalizedStrings from 'react-localization'; import { Link } from 'react-router-dom'; -import { ActionBar, Pane, Text } from '@cybercongress/gravity'; - +import { Pane, Text } from '@cybercongress/gravity'; import { BondStatus } from 'cosmjs-types/cosmos/staking/v1beta1/staking'; import { useBackend } from 'src/contexts/backend/backend'; import { CHAIN_ID, BASE_DENOM } from 'src/constants/config'; +import { KEY_TYPE } from 'src/pages/Keys/types'; import { ContainetLedger } from './container'; import { Dots } from '../ui/Dots'; import Account from '../account/account'; @@ -15,22 +15,17 @@ import { i18n } from '../../i18n/en'; import Button from '../btnGrd'; import { InputNumber, Input } from '../Input'; -import ActionBarContainer from '../actionBar'; +import ActionBar from '../actionBar'; import ButtonIcon from '../buttons/ButtonIcon'; import { Color } from '../LinearGradientContainer/LinearGradientContainer'; import AddFileButton from '../buttons/AddFile/AddFile'; const imgKeplr = require('../../image/keplr-icon.svg'); const imgRead = require('../../image/duplicate-outline.svg'); +const imgSecrets = require('../../image/secrets_icon.png'); const T = new LocalizedStrings(i18n); -// const toPascalCase = (str) => -// str -// .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[a-zA-Z0-9]+/g) -// .map((cht) => cht.charAt(0).toUpperCase() + cht.substr(1).toLowerCase()) -// .join(''); - export function ActionBarContentText({ children, ...props }) { return ( +// str +// .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[a-zA-Z0-9]+/g) +// .map((cht) => cht.charAt(0).toUpperCase() + cht.substr(1).toLowerCase()) +// .join(''); + export function TransactionSubmitted() { return ( - - Please wait while we confirm the transaction on the blockchain{' '} - - + Please wait while we confirm the transaction on the blockchain{' '} + ); } export function Confirmed({ txHash, txHeight, cosmos, onClickBtnClose }) { return ( - - - Transaction{' '} + + + Transaction {cosmos ? ( {trimString(txHash, 6, 6)} @@ -71,30 +70,18 @@ export function Confirmed({ txHash, txHeight, cosmos, onClickBtnClose }) { {trimString(txHash, 6, 6)} - )}{' '} - {txHeight && ( - - was included in the block
at height{' '} - {formatNumber(parseFloat(txHeight))} -
)} -
- +
); } export function TransactionError({ onClickBtn, errorMessage }) { return ( - - - Message Error: {errorMessage} - - + + Message Error: {errorMessage} ); } @@ -115,7 +102,7 @@ export function StartStageSearchActionBar({ const { isIpfsInitialized } = useBackend(); return ( // use NodeIsLoadingButton component - - + ); } @@ -223,7 +210,7 @@ export function Delegate({ available, }) { return ( - {BASE_DENOM.toUpperCase()} - + ); } @@ -267,7 +254,7 @@ export function ReDelegate({ onClickBack, }) { return ( - ))} - + ); } @@ -326,7 +313,7 @@ export function ActionBarSend({ onClickBack, }) { return ( - - + ); } @@ -405,24 +392,24 @@ export function ConnectAddress({ selectMethodFunc, selectMethod, selectNetwork, - connctAddress, + connectAddress, keplr, onClickBack, }) { return ( - {keplr ? ( selectMethodFunc('keplr')} - active={selectMethod === 'keplr'} + onClick={() => selectMethodFunc(KEY_TYPE.keplr)} + active={selectMethod === KEY_TYPE.keplr} img={imgKeplr} text="keplr" /> @@ -439,11 +426,17 @@ export function ConnectAddress({ )} selectMethodFunc('read-only')} - active={selectMethod === 'read-only'} + onClick={() => selectMethodFunc(KEY_TYPE.readOnly)} + active={selectMethod === KEY_TYPE.readOnly} img={imgRead} text="read-only" /> + selectMethodFunc(KEY_TYPE.secrets)} + active={selectMethod === KEY_TYPE.secrets} + img={imgSecrets} + text="secrets" + /> in @@ -453,6 +446,6 @@ export function ConnectAddress({ text={CHAIN_ID} /> - + ); } diff --git a/src/components/link/link.jsx b/src/components/link/link.tsx similarity index 72% rename from src/components/link/link.jsx rename to src/components/link/link.tsx index 9a38f5fca..ca2a00b75 100644 --- a/src/components/link/link.jsx +++ b/src/components/link/link.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import styles from './cid.modules.scss'; +import React from 'react'; export function LinkWindow({ to, children, ...props }) { return ( @@ -9,7 +10,12 @@ export function LinkWindow({ to, children, ...props }) { ); } -export function Cid({ cid, children }) { +type Props = { + cid: string; + children?: React.ReactNode; +}; + +export function Cid({ cid, children }: Props) { return ( {children || cid} diff --git a/src/components/loader/loader.js b/src/components/loader/loader.js index 3dbf40cee..a73978b53 100644 --- a/src/components/loader/loader.js +++ b/src/components/loader/loader.js @@ -220,7 +220,10 @@ function bootstrap() { console.log('service worker registered: ', registration); }) .catch((registrationError) => { - console.log('service worker registration failed: ', registrationError); + console.log( + 'service worker registration failed: ', + registrationError + ); }); }); } else { @@ -247,8 +250,9 @@ function bootstrap() { progressData.innerHTML = `Loading: ${Math.round( progress * 100 - )}%.
Network speed: ${Math.round(e.networkSpeed * 100) / 100 - } kbps`; + )}%.
Network speed: ${ + Math.round(e.networkSpeed * 100) / 100 + } kbps`; // console.log(e.loaded, e.loaded / e.totalSize); // @TODO }) diff --git a/src/components/search/Spark/Spark.tsx b/src/components/search/Spark/Spark.tsx index a00ab0949..74e82ed3f 100644 --- a/src/components/search/Spark/Spark.tsx +++ b/src/components/search/Spark/Spark.tsx @@ -50,9 +50,11 @@ function Spark({ {/* TODO: refact. meta should be moved inside contentItem and exclude fetchParticle from that */} -
- -
+ {!selfLinks && ( +
+ +
+ )} )} diff --git a/src/components/sideButtonLink/SideButtonLink.module.scss b/src/components/sideButtonLink/SideButtonLink.module.scss new file mode 100644 index 000000000..fcf81b8a9 --- /dev/null +++ b/src/components/sideButtonLink/SideButtonLink.module.scss @@ -0,0 +1,63 @@ +.main { + display: flex; + position: fixed; + z-index: 3; + top: 45%; + padding: 40px 10px; + backdrop-filter: blur(15px); + border-radius: 50px; + + > span { + font-size: 14px; + margin-top: 5px; + color: white; + + &:last-of-type { + margin-top: 4px; + font-size: 16px; + } + } + + &:hover { + transition: all 0.3s; + background-color: var(--green-light); + + span { + color: var(--green-light); + } + } +} + +.sense { + flex-direction: column; + padding-left: 3px; + background: linear-gradient( + to right, + black 90%, + rgba(255, 255, 255, 0.5) 100% + ); + + &:hover { + padding-right: 30px; + } +} + +.hydrogen { + right: 0; + align-items: center; + background: linear-gradient( + to left, + black 90%, + rgba(255, 255, 255, 0.5) 100% + ); + + &:hover { + padding-left: 30px; + } +} + +.hydrogen div { + display: flex; + flex-direction: column; + margin-left: 5px; +} diff --git a/src/components/sideButtonLink/SideButtonLink.tsx b/src/components/sideButtonLink/SideButtonLink.tsx new file mode 100644 index 000000000..4d0dac1c9 --- /dev/null +++ b/src/components/sideButtonLink/SideButtonLink.tsx @@ -0,0 +1,19 @@ +import { Link } from 'react-router-dom'; +import cx from 'classnames'; +import styles from './SideButtonLink.module.scss'; + +type ContainerLinkProps = { + to: string; + buttonType: 'sense' | 'hydrogen'; + children: React.ReactNode; +}; + +function SideButtonLink({ to, buttonType, children }: ContainerLinkProps) { + return ( + + {children} + + ); +} + +export default SideButtonLink; diff --git a/src/components/time/time.module.scss b/src/components/time/time.module.scss new file mode 100644 index 000000000..fc462b8c0 --- /dev/null +++ b/src/components/time/time.module.scss @@ -0,0 +1,9 @@ +.wrapper { + display: flex; + color: var(--blue-light); + gap: 5px; + + .prefix { + color: var(--grayscale-dark); + } +} \ No newline at end of file diff --git a/src/components/time/time.tsx b/src/components/time/time.tsx new file mode 100644 index 000000000..d72172a47 --- /dev/null +++ b/src/components/time/time.tsx @@ -0,0 +1,16 @@ +import { formatNumber } from 'src/utils/utils'; +import { convertTimestampToString } from 'src/utils/date'; +import styles from './time.module.scss'; + +function Time({ msTime }: { msTime: number }) { + const [valueTime, prefixTime] = convertTimestampToString(msTime).split(' '); + + return ( + + {formatNumber(valueTime)} + {prefixTime} + + ); +} + +export default Time; diff --git a/src/components/time/utils.ts b/src/components/time/utils.ts new file mode 100644 index 000000000..1ec3dc255 --- /dev/null +++ b/src/components/time/utils.ts @@ -0,0 +1,19 @@ +const unixTimestamp = (secondsTime: number) => { + const years = Math.floor(secondsTime / 31536000); + const months = Math.floor(secondsTime / 2592000); + const days = Math.floor(secondsTime / 86400); + const hours = Math.floor(((secondsTime % 31536000) % 86400) / 3600); + const minutes = Math.floor((((secondsTime % 31536000) % 86400) % 3600) / 60); + const seconds = Math.floor((((secondsTime % 31536000) % 86400) % 3600) % 60); + + return { + years, + months, + days, + hours, + minutes, + seconds, + }; +}; + +export default unixTimestamp; diff --git a/src/components/tooltip/tooltip.tsx b/src/components/tooltip/tooltip.tsx index 7746b0a6a..c0394a9a5 100644 --- a/src/components/tooltip/tooltip.tsx +++ b/src/components/tooltip/tooltip.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import Popper, { usePopperTooltip } from 'react-popper-tooltip'; import 'react-popper-tooltip/dist/styles.css'; import cx from 'classnames'; import { PositioningStrategy } from '@popperjs/core'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; import styles from './Tooltip.module.scss'; export type TooltipProps = { @@ -20,6 +21,42 @@ export type TooltipProps = { strategy?: PositioningStrategy; }; +function AdviserTooltipWrapper({ + children, + tooltip, +}: { + children: React.ReactNode; + tooltip: React.ReactNode; +}) { + const { setAdviser } = useAdviserTexts(); + const ref = useRef(null); + + function onMouseEnter() { + setAdviser(tooltip); + } + + function onMouseLeave() { + setAdviser(null); + } + useEffect(() => { + return () => { + setAdviser(null); + }; + }, [setAdviser]); + + return ( +
+ {children} +
+ ); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars function Tooltip({ children, trigger = 'hover', @@ -76,4 +113,4 @@ function Tooltip({ ); } -export default Tooltip; +export default AdviserTooltipWrapper; diff --git a/src/components/ui/Loader2.tsx b/src/components/ui/Loader2.tsx index dd1e8ceb1..15035f799 100644 --- a/src/components/ui/Loader2.tsx +++ b/src/components/ui/Loader2.tsx @@ -2,17 +2,10 @@ import { Dots } from './Dots'; import styles from './Loading.module.scss'; // temp -function Loader2() { +function Loader2({ text = 'loading' }: { text?: string }) { return (
- - loading - +

{text}

); } diff --git a/src/components/valueImg/images/lp.png b/src/components/valueImg/images/lp.png new file mode 100644 index 000000000..af8ea9118 Binary files /dev/null and b/src/components/valueImg/images/lp.png differ diff --git a/src/components/valueImg/imgDenom.tsx b/src/components/valueImg/imgDenom.tsx index cc121d3de..d13db7a3a 100644 --- a/src/components/valueImg/imgDenom.tsx +++ b/src/components/valueImg/imgDenom.tsx @@ -8,12 +8,12 @@ import amperImg from 'images/light.png'; import hydrogen from 'images/hydrogen.svg'; import tocyb from 'images/boot.png'; import boot from 'images/large-green.png'; -import pussy from 'images/large-purple-circle.png'; import defaultImg from 'images/large-orange-circle.png'; import Tooltip from '../tooltip/tooltip'; import { trimString } from '../../utils/utils'; import useQueueIpfsContent from 'src/hooks/useQueueIpfsContent'; import styles from './TextDenom.module.scss'; +import lp from './images/lp.png'; // maybe reuse enum from DenomArr const nativeImageMap = { @@ -23,10 +23,10 @@ const nativeImageMap = { a: amperImg, hydrogen, h: hydrogen, - liquidpussy: hydrogen, - lp: hydrogen, + liquidpussy: lp, + lp, boot, - pussy, + pussy: '🟣', tocyb, eth, }; @@ -105,7 +105,12 @@ function ImgDenom({ } }, [coinDenom, infoDenom, fetchWithDetails, getImgFromIpfsByCid]); - const img = ( + // refactor + const isEmoji = imgDenom && imgDenom?.length < 3; + + const img = isEmoji ? ( + imgDenom + ) : ( * { + min-width: 380px; + } +} diff --git a/src/containers/Search/Filters/Filters.tsx b/src/containers/Search/Filters/Filters.tsx index 4f10d9ff2..3d1392ca1 100644 --- a/src/containers/Search/Filters/Filters.tsx +++ b/src/containers/Search/Filters/Filters.tsx @@ -1,10 +1,20 @@ -import React from 'react'; +import React, { useState } from 'react'; import styles from './Filters.module.scss'; import ButtonsGroup from 'src/components/buttons/ButtonsGroup/ButtonsGroup'; import { LinksTypeFilter, SortBy } from '../types'; import { initialContentTypeFilterState } from '../constants'; import Links from 'src/components/search/Spark/Meta/Links/Links'; -import { Tooltip } from 'src/components'; +import { Account, Tooltip } from 'src/components'; +import { AccountInput } from 'src/pages/teleport/components/Inputs'; +import useCurrentAddress from 'src/hooks/useCurrentAddress'; +import { AvataImgIpfs } from 'src/containers/portal/components/avataIpfs'; +import useCurrentPassport from 'src/features/passport/hooks/useCurrentPassport'; + +enum NeuronFilterType { + me = 'me', + all = 'all', + neuron = 'neuron', +} // TODO: move to ipfs config, global export const contentTypeConfig = { @@ -43,18 +53,23 @@ const sortConfig = { label: '📅', tooltip: 'sort particles by date of creation', }, - [SortBy.popular]: { - label: '🔥', - tooltip: '', - }, - [SortBy.mine]: { - label: '👤', - tooltip: '', - }, + // [SortBy.popular]: { + // label: '🔥', + // tooltip: '', + // }, + // [SortBy.mine]: { + // label: '👤', + // tooltip: '', + // }, }; type Props = { linksFilter: LinksTypeFilter; + + neuronFilter: { + value: string; + setValue: (address: string | null) => void; + }; }; function Filters({ @@ -66,94 +81,183 @@ function Filters({ setLinksFilter, total, contentType, + neuronFilter, }: Props) { + const [isNeuronChooserOpened, setNeuronChooserOpened] = useState(false); + + const currentAddress = useCurrentAddress(); + const currentPassport = useCurrentPassport(); + const { value: neuron, setValue: setNeuron } = neuronFilter; + return ( -
-
- { - if (filter === 'all') { - setFilters(initialContentTypeFilterState); - return; - } + <> +
+
+ { + if (filter === 'all') { + setFilters(initialContentTypeFilterState); + return; + } + + setFilters((filters) => ({ + ...initialContentTypeFilterState, + [filter]: !filters[filter], + })); + }} + items={[ + { + label: 'all', + checked: !Object.values(filters).some((filter) => filter), + }, + ].concat( + Object.keys(filters) + .map((filter) => { + if (!Object.values(contentType).includes(filter)) { + return null; + } - setFilters((filters) => ({ - ...initialContentTypeFilterState, - [filter]: !filters[filter], - })); + return { + label: contentTypeConfig[filter].label, + name: filter, + checked: filters[filter], + tooltip: contentTypeConfig[filter].tooltip, + }; + }) + .filter((item) => !!item) + )} + /> +
+ + { + return { + label: sortConfig[sortType].label, + disabled: + // sortType === SortBy.mine || + // sortType === SortBy.popular || + sortType === SortBy.rank && neuron, + name: sortType, + checked: filter2 === sortType, + tooltip: sortConfig[sortType].tooltip, + }; + })} + onChange={(sortType: SortBy) => { + setFilter2(sortType); + localStorage.setItem('search-sort', sortType); }} + /> + + filter), + label: ( + + ), + name: NeuronFilterType.me, + checked: neuron === currentAddress, + tooltip: 'show only particles from my neuron', }, - ].concat( - Object.keys(filters) - .map((filter) => { - if (!Object.values(contentType).includes(filter)) { - return null; - } + { + label: '🌏', + name: NeuronFilterType.all, + checked: !neuron, + tooltip: 'show all particles', + }, + { + label: '👤', + name: NeuronFilterType.neuron, + checked: + (!!neuron && neuron !== currentAddress) || + isNeuronChooserOpened, + tooltip: 'show only particles from this neuron', + }, + ]} + onChange={(name) => { + let value; + let value2: typeof isNeuronChooserOpened; + switch (name) { + case NeuronFilterType.all: + value = null; + break; + + case NeuronFilterType.me: + value = neuron === currentAddress ? null : currentAddress; + break; + + case NeuronFilterType.neuron: + value = null; + value2 = !isNeuronChooserOpened; + break; + + default: + break; + } + + if (name !== NeuronFilterType.neuron && isNeuronChooserOpened) { + value2 = false; + } - return { - label: contentTypeConfig[filter].label, - name: filter, - checked: filters[filter], - tooltip: contentTypeConfig[filter].tooltip, - }; - }) - .filter((item) => !!item) - )} + setNeuron(value || null); + setNeuronChooserOpened(value2 || false); + }} /> -
- - { - return { - label: sortConfig[sortType].label, - disabled: sortType === SortBy.mine || sortType === SortBy.popular, - name: sortType, - checked: filter2 === sortType, - tooltip: sortConfig[sortType].tooltip, - }; - })} - onChange={(sortType: SortBy) => { - setFilter2(sortType); - localStorage.setItem('search-sort', sortType); - }} - /> - - { - setLinksFilter(val); - }} - /> - - -
- - {(() => { - switch (linksFilter) { - case LinksTypeFilter.all: - return total.to + total.from; - - case LinksTypeFilter.to: - return total.to; - - case LinksTypeFilter.from: - default: - return total.from; + + { + setLinksFilter(val); + }} + /> + + +
+ + {(() => { + switch (linksFilter) { + case LinksTypeFilter.all: + return total.to + total.from; + + case LinksTypeFilter.to: + return total.to; + + case LinksTypeFilter.from: + default: + return total.from; + } + })()} + {' '} + particles +
+
+
+ + {isNeuronChooserOpened && ( +
+ { + if (address) { + setNeuronChooserOpened(false); } - })()} - {' '} - particles + + setNeuron(address || null); + }} + />
- - + )} + ); } diff --git a/src/containers/Search/SearchResults.tsx b/src/containers/Search/SearchResults.tsx index 6dfa91716..b13df778a 100644 --- a/src/containers/Search/SearchResults.tsx +++ b/src/containers/Search/SearchResults.tsx @@ -1,25 +1,32 @@ +import { + matchPath, + useLocation, + useNavigate, + useParams, + useSearchParams, +} from 'react-router-dom'; import { useEffect, useState } from 'react'; import InfiniteScroll from 'react-infinite-scroll-component'; -import { useParams } from 'react-router-dom'; + import Display from 'src/components/containerGradient/Display/Display'; import Spark from 'src/components/search/Spark/Spark'; import Loader2 from 'src/components/ui/Loader2'; -import { PATTERN_IPFS_HASH } from 'src/constants/patterns'; import { useDevice } from 'src/contexts/device'; import { IpfsContentType } from 'src/services/ipfs/types'; -import { getIpfsHash } from 'src/utils/ipfs/helpers'; import useIsOnline from 'src/hooks/useIsOnline'; -import { encodeSlash } from '../../utils/utils'; import ActionBarContainer from './ActionBarContainer'; import Filters from './Filters/Filters'; import styles from './SearchResults.module.scss'; import FirstItems from './_FirstItems.refactor'; import { initialContentTypeFilterState } from './constants'; +import { getSearchQuery } from 'src/utils/search/utils'; import useSearchData from './hooks/useSearchData'; import { LinksTypeFilter, SortBy } from './types'; +import { routes } from 'src/routes'; const sortByLSKey = 'search-sort'; +const NEURON_SEARCH_KEY = 'neuron'; type Props = { query?: string; @@ -33,6 +40,12 @@ function SearchResults({ actionBarTextBtn, }: Props) { const { query: q, cid } = useParams(); + + const [searchParams, setSearchParams] = useSearchParams(); + const [neuron, setNeuron] = useState(searchParams.get(NEURON_SEARCH_KEY)); + + const location = useLocation(); + const query = propQuery || q || cid || ''; const isOnline = useIsOnline(); @@ -49,15 +62,19 @@ function SearchResults({ initialContentTypeFilterState ); const [sortBy, setSortBy] = useState( - localStorage.getItem(sortByLSKey) || SortBy.rank + neuron + ? SortBy.date + : (localStorage.getItem(sortByLSKey) as SortBy | null) || SortBy.rank ); + const [linksTypeFilter, setLinksTypeFilter] = useState(LinksTypeFilter.all); const noResultsText = isOnline ? noCommentText || ( <> - there are no answers or questions to this particle
be the first - and create one + there are no answers or questions to this particle{' '} + {neuron && 'for this neuron'} +
be the first and create one ) : "ther's nothing to show, wait until you're online"; @@ -70,7 +87,7 @@ function SearchResults({ isInitialLoading, refetch, fetchNextPage: next, - } = useSearchData(keywordHash, { + } = useSearchData(keywordHash, neuron, { sortBy, linksType: linksTypeFilter, }); @@ -89,15 +106,9 @@ function SearchResults({ setContentType({}); (async () => { - let keywordHashTemp = ''; - - if (query.match(PATTERN_IPFS_HASH)) { - keywordHashTemp = query; - } else { - keywordHashTemp = await getIpfsHash(encodeSlash(query)); - } + const keywordHash = await getSearchQuery(query); - setKeywordHash(keywordHashTemp); + setKeywordHash(keywordHash); })(); }, [query]); @@ -155,6 +166,26 @@ function SearchResults({ total={total} total2={items.length} contentType={contentType} + neuronFilter={{ + value: neuron, + setValue: (address) => { + setNeuron(address); + setSortBy(SortBy.date); + + // TODO: need to check on senate page + if (matchPath(routes.oracle.ask.path, location.pathname)) { + setSearchParams((prevParams) => { + if (address) { + prevParams.set(NEURON_SEARCH_KEY, address); + } else { + prevParams.delete(NEURON_SEARCH_KEY); + } + + return prevParams; + }); + } + }, + }} />
diff --git a/src/containers/Search/hooks/useLinksByDate.tsx b/src/containers/Search/hooks/useLinksByDate.tsx index dd94f2ab8..ad85895b7 100644 --- a/src/containers/Search/hooks/useLinksByDate.tsx +++ b/src/containers/Search/hooks/useLinksByDate.tsx @@ -5,17 +5,18 @@ import { merge } from './shared'; function useLinksByDate( hash: string, type: LinksTypeFilter, + neuron: string | null, { skip = false } = {} ) { const data = useGetDiscussion( - { hash, type: LinksTypeFilter.from }, + { hash, type: LinksTypeFilter.from, neuron }, { skip: skip || type === LinksTypeFilter.to, } ); const dataBacklinks = useGetDiscussion( - { hash, type: LinksTypeFilter.to }, + { hash, type: LinksTypeFilter.to, neuron }, { skip: skip || type === LinksTypeFilter.from, } diff --git a/src/containers/Search/hooks/useRankLinks.tsx b/src/containers/Search/hooks/useRankLinks.tsx index 09bcd81e0..37993e0ee 100644 --- a/src/containers/Search/hooks/useRankLinks.tsx +++ b/src/containers/Search/hooks/useRankLinks.tsx @@ -4,16 +4,15 @@ import { useQueryClient } from 'src/contexts/queryClient'; import { getRankGrade, searchByHash } from 'src/utils/search/utils'; import { mapLinkToLinkDto } from 'src/services/CozoDb/mapping'; import { coinDecimals } from 'src/utils/utils'; -import { useBackend } from 'src/contexts/backend/backend'; import { LinksTypeFilter } from '../types'; import { merge } from './shared'; +import { enqueueLinksSave } from 'src/services/backend/channels/BackendQueueChannel/backendQueueSenders'; const PER_PAGE_LIMIT = 10; const useSearch = (hash: string, { skip = false } = {}) => { const cid = hash; - const { defferedDbApi } = useBackend(); const queryClient = useQueryClient(); @@ -29,17 +28,14 @@ const useSearch = (hash: string, { skip = false } = {}) => { ['useSearch', cid], async ({ pageParam = 0 }: { pageParam?: number }) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const response = await searchByHash( - queryClient, - cid, - pageParam, - PER_PAGE_LIMIT - ); - const result = response?.result || []; - result && - defferedDbApi?.importCyberlinks( - result.map((l) => mapLinkToLinkDto(hash, l.particle)) + const response = await searchByHash(queryClient, cid, pageParam); + + if (response?.result) { + enqueueLinksSave( + response.result.map((l) => mapLinkToLinkDto(hash, l.particle)) ); + } + return { data: response, page: pageParam }; }, { diff --git a/src/containers/Search/hooks/useSearchData.tsx b/src/containers/Search/hooks/useSearchData.tsx index 63ddc7324..4137a43ef 100644 --- a/src/containers/Search/hooks/useSearchData.tsx +++ b/src/containers/Search/hooks/useSearchData.tsx @@ -7,6 +7,7 @@ import useLinksByDate from './useLinksByDate'; const useSearchData = ( hash: string, + neuron: string | null, { sortBy = SortBy.rank, linksType = LinksTypeFilter.all, @@ -15,7 +16,7 @@ const useSearchData = ( linksType?: LinksTypeFilter; } ) => { - const linksByDate = useLinksByDate(hash, linksType, { + const linksByDate = useLinksByDate(hash, linksType, neuron, { skip: sortBy !== SortBy.date, }); diff --git a/src/containers/Search/types.ts b/src/containers/Search/types.ts index d9b9c20c0..a47fc7bdd 100644 --- a/src/containers/Search/types.ts +++ b/src/containers/Search/types.ts @@ -18,6 +18,6 @@ export enum SortBy { rank = 'rank', date = 'date', // not ready - popular = 'popular', - mine = 'mine', + // popular = 'popular', + // mine = 'mine', } diff --git a/src/containers/Validators/Validators.jsx b/src/containers/Validators/Validators.jsx index f484bf3c3..d50562cf3 100644 --- a/src/containers/Validators/Validators.jsx +++ b/src/containers/Validators/Validators.jsx @@ -6,7 +6,7 @@ import { useQueryClient } from 'src/contexts/queryClient'; import { useAdviser } from 'src/features/adviser/context'; import { getDelegatorDelegations } from 'src/utils/search/utils'; import { BondStatus } from 'cosmjs-types/cosmos/staking/v1beta1/staking'; -import { DenomArr } from 'src/components'; +import { DenomArr, MainContainer } from 'src/components'; import { fromBech32, formatNumber, asyncForEach } from '../../utils/utils'; import { Loading } from '../../components'; import ActionBarContainer from './ActionBarContainer'; @@ -15,6 +15,8 @@ import getHeroes from './getHeroesHook'; import { useGetBalance } from '../../pages/robot/_refactor/account/hooks'; import useSetActiveAddress from '../../hooks/useSetActiveAddress'; import styles from './Validators.module.scss'; +import { BASE_DENOM, DENOM_LIQUID } from 'src/constants/config'; +import useStakingParams from 'src/features/staking/useStakingParams'; function Validators({ defaultAccount }) { const { isMobile: mobile } = useDevice(); @@ -40,16 +42,26 @@ function Validators({ defaultAccount }) { const { setAdviser } = useAdviser(); + const { data: stakingParamsData } = useStakingParams(); + const unbondingDays = + stakingParamsData && + stakingParamsData.params.unbondingTime.seconds / 60 / 60 / 24; + useEffect(() => { setAdviser(
- the current undelegation period is 42 days -
- you need to burn 1 to unstake - 1 + {unbondingDays && ( + <> + the current undelegation period is{' '} + {unbondingDays} days +
+ + )} + you need to burn 1 to + unstake 1
); - }, [setAdviser]); + }, [setAdviser, unbondingDays]); useEffect(() => { setValidatorsData(validators); @@ -204,7 +216,7 @@ function Validators({ defaultAccount }) { return (
-
+ -
+ { // tabs - if (matchPath(routes.senateProposal.path, location.pathname)) { + if ( + [cybernetRoutes.verse.path, routes.senateProposal.path].some((path) => { + return matchPath(path, location.pathname); + }) + ) { return; } window.scrollTo(0, 0); }, [location.pathname]); + useEffect(() => { + dispatch(setTimeHistoryRoute(location.pathname)); + }, [location.pathname, dispatch]); + useEffect(() => { if (ipfsError && !location.pathname.includes('/drive')) { adviserContext.setAdviser( @@ -101,20 +110,28 @@ function App() { // }; return ( - - <> - {/* not move portal order */} - {(location.pathname.includes('/brain') || - location.pathname.includes('/oracle2') || - location.pathname.includes('/graph')) && ( -
- )} - - {!(location.pathname === '/') && } - - - - + + + <> + {/* not move portal order */} + {(location.pathname.includes('/brain') || + location.pathname.includes('/oracle2') || + location.pathname.includes('/graph')) && ( +
+ )} + + {![ + /* routes.home.path, */ + /* routes.teleport.path, */ + // cybernetRoutes.verse.path, + ].some((path) => { + return matchPath(path, location.pathname); + }) && } + + + + + ); } diff --git a/src/containers/application/AppMenu.tsx b/src/containers/application/AppMenu.tsx deleted file mode 100644 index 3e7bceeb7..000000000 --- a/src/containers/application/AppMenu.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { Networks } from 'src/types/networks'; -import { Bookmarks } from '../../components/appMenu/AppMenu'; - -import nebulaIcon from '../../image/temple/nebula.png'; -import teleport from '../../image/temple/teleport.png'; -import hfr from '../../image/temple/hfr.png'; -import temple from '../../image/temple/temple.png'; -import robot from '../../image/temple/robot.png'; -import shpere from '../../image/temple/shpere.png'; -import senate from '../../image/temple/senate.png'; -import portal from '../../image/space-pussy.svg'; -import oracle from '../../image/temple/oracle.png'; -import warp from '../../image/temple/warp.png'; -import hub from '../../image/temple/hub.png'; -import congress from './images/congress.png'; - -import { routes } from '../../routes'; -import { CHAIN_ID } from 'src/constants/config'; - -const itemsMenu = () => { - const listItemMenu = [ - { - name: 'My robot', - icon: robot, - to: '/robot', - subItems: [], - // subItems: myRobotLinks, - }, - { - name: 'Oracle', - to: '/', - icon: oracle, - subItems: [ - { name: 'Particles', to: '/particles' }, - { name: 'Stats', to: '/oracle/stats' }, - { name: 'Blocks', to: '/network/bostrom/blocks' }, - { name: 'Txs', to: '/network/bostrom/tx' }, - { name: 'Contracts', to: '/contracts' }, - { name: 'Libs', to: '/libs' }, - ], - }, - { name: 'Temple', to: routes.temple.path, subItems: [], icon: temple }, - { name: 'Nebula', to: '/nebula', subItems: [], icon: nebulaIcon }, - { - name: 'Teleport', - to: '/teleport', - icon: teleport, - active: false, - subItems: [ - { name: 'Send', to: routes.teleport.send.path }, - { name: 'Bridge', to: routes.teleport.bridge.path }, - { name: 'Swap', to: routes.teleport.swap.path }, - ], - }, - { - name: 'Warp', - icon: warp, - to: '/warp', - subItems: [ - { name: 'Add liquidity', to: '/warp/add-liquidity' }, - { name: 'Create pool', to: '/warp/create-pool' }, - { name: 'Sub liquidity', to: '/warp/sub-liquidity' }, - ], - }, - { - name: 'Sphere', - icon: shpere, - to: routes.sphere.path, - subItems: [{ name: 'Heroes at rest', to: routes.sphereJailed.path }], - }, - { name: 'HFR', icon: hfr, to: '/hfr', subItems: [] }, - // { name: 'Lifeforms', to: '/contracts', subItems: [] }, - // { - // name: 'Hub', - // to: '/search/hub', - // icon: hub, - // subItems: [ - // { name: 'Networks', to: '/networks' }, - // { name: 'Add network', to: '/networks/add' }, - // ], - // }, - { name: 'Senate', icon: senate, to: '/senate', subItems: [] }, - - { - name: 'Cyberver 🟣', - icon: require('./images/cyberver.png'), - to: 'https://spacepussy.ai/cyberver', - subItems: [], - }, - { name: 'About', icon: congress, to: routes.social.path, subItems: [] }, - // { - // name: 'Help', - // icon: zhdun, - // to: '/help', - // subItems: [ - // { - // name: 'Guide', - // to: '/ipfs/QmRumrGFrqxayDpySEkhjZS1WEtMyJcfXiqeVsngqig3ak', - // }, - // { name: 'story', to: '/genesis' }, - // { - // name: 'vision', - // to: '/ipfs/QmXzGkfxZV2fzpFmq7CjAYsYL1M581ZD4yuF9jztPVTpCn', - // }, - // { - // name: 'great web', - // to: '/ipfs/QmUamt7diQP54eRnmzqMZNEtXNTzbgkQvZuBsgM6qvbd57', - // }, - // { - // name: 'vs govs', - // to: '/ipfs/QmPmJ4JwzCi82HZp7adtv5GVBFTsKF5Yoy43wshHH7x3ty', - // }, - // { - // name: 'vs corps', - // to: '/ipfs/QmQvKF9Jb6QKmsqHJzEZJUfcbB9aBBKwa5dh3pMxYEj7oi', - // }, - // { - // name: 'roadmap', - // to: '/ipfs/QmSBYCCYFNfHNQD7MWm4zBaNuztMaT2KghA2SbeZZm9vLH', - // }, - // { - // name: 'distribution', - // to: '/ipfs/QmVPgNeay23Ae5itAamMcr4iEAUKuhw5qD9U1zNqN4gpew', - // }, - // { - // name: 'gift', - // to: '/ipfs/QmPAi1h1rwWnHkNnxnHZg28eGivpUK8wy8eciqoPSR4PHv', - // }, - // { - // name: 'congress', - // to: '/network/bostrom/contract/bostrom1xszmhkfjs3s00z2nvtn7evqxw3dtus6yr8e4pw', - // }, - // ], - // }, - ]; - - if (CHAIN_ID === Networks.BOSTROM || CHAIN_ID === Networks.SPACE_PUSSY) { - listItemMenu.splice(2, 0, { - name: 'Portal', - icon: portal, - to: '/portal', - subItems: [ - { name: 'Citizenship', to: '/citizenship' }, - { name: 'Gift', to: '/gift' }, - // { name: 'Release', to: '/release' }, - ], - }); - } - return listItemMenu; -}; - -export type MenuItems = ReturnType; -export type MenuItem = MenuItems[0]; - -function AppMenu({ addressActive, closeMenu }) { - return ( -
- -
- ); -} - -export default AppMenu; diff --git a/src/containers/application/Header/Commander/Commander.module.scss b/src/containers/application/Header/Commander/Commander.module.scss index 615ed1cbd..90e25cff5 100644 --- a/src/containers/application/Header/Commander/Commander.module.scss +++ b/src/containers/application/Header/Commander/Commander.module.scss @@ -1,13 +1,19 @@ .wrapper { width: 62%; transform: translate(-50%, -80%); - // background: rgb(0 0 0 / 79%); + background: rgb(0 0 0 / 79%); margin-right: -50%; left: 50%; position: absolute; top: 50%; - padding: 0px 20px; z-index: 1; + // width left and right sideBar , gap and paddings + max-width: calc(100vw - 200px - 300px - 100px); + + @media (max-width: 540px) { + width: 40%; + max-width: unset; + } } .input { diff --git a/src/containers/application/Header/SwitchNetwork/SwitchNetwork.module.scss b/src/containers/application/Header/CurrentApp/CurrentApp.module.scss similarity index 81% rename from src/containers/application/Header/SwitchNetwork/SwitchNetwork.module.scss rename to src/containers/application/Header/CurrentApp/CurrentApp.module.scss index 3b8aa3d1f..9d4f82206 100644 --- a/src/containers/application/Header/SwitchNetwork/SwitchNetwork.module.scss +++ b/src/containers/application/Header/CurrentApp/CurrentApp.module.scss @@ -3,7 +3,7 @@ grid-template-columns: 100px 1fr; gap: 25px; align-items: center; - height: 100px; + height: 90px; border: none; background: transparent; color: rgb(31, 203, 255); @@ -13,8 +13,8 @@ &:hover { .networkBtn { &::before { - width: 105px; - height: 105px; + width: 95px; + height: 95px; } } @@ -33,12 +33,15 @@ border: none; background: transparent; position: relative; + display: flex; + align-items: center; + justify-content: center; cursor: pointer; &::before { content: ''; - width: 100px; - height: 100px; + width: 90px; + height: 90px; background: #000000; box-shadow: 0px 0px 20px rgba(31, 203, 255, 0.4); position: absolute; @@ -56,25 +59,7 @@ width: 60px; height: 60px; position: relative; -} - -.networkBtnImgIconMenu { - position: absolute; - top: 50%; - left: 50%; - transition: 0.2s; - transform: translate(-50%, -50%) rotate(-90deg); - - &Close { - transform: translate(-50%, -50%) rotate(0deg); - } - - div { - width: 35px; - height: 5px; - background-color: black; - margin: 6px 0; - } + object-fit: contain; } .btnContainerText { @@ -116,7 +101,7 @@ padding-left: 15px; width: 250px; padding-bottom: 15px; - opacity: 0; + // opacity: 0; transition: 0.2s; backdrop-filter: blur(7px); @@ -164,9 +149,8 @@ .tooltipContainer { position: absolute; - left: 0px !important; - // top: unset !important; - top: 90px !important; + left: 0px; + top: 130px; z-index: 3; } @@ -177,9 +161,29 @@ transition: 0.2s; } +.buttonWrapper { + display: grid; + gap: 25px; + grid-template-columns: 100px 1fr; + align-items: center; + height: 90px; + + @media (width < 768px) { + grid-template-columns: 100px; + } +} + .buttonWrapper { @media (max-width: 480px) { height: unset !important; grid-template-columns: unset !important; } } + +.containerSubItems { + display: flex; + flex-direction: column; + background: #0000008c; + width: 170px; + backdrop-filter: blur(7px); +} diff --git a/src/containers/application/Header/CurrentApp/CurrentApp.tsx b/src/containers/application/Header/CurrentApp/CurrentApp.tsx new file mode 100644 index 000000000..250c0cf75 --- /dev/null +++ b/src/containers/application/Header/CurrentApp/CurrentApp.tsx @@ -0,0 +1,72 @@ +import { useMemo, useState } from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { routes } from 'src/routes'; +import { CHAIN_ID } from 'src/constants/config'; +import { useAppSelector } from 'src/redux/hooks'; +import usePassportByAddress from 'src/features/passport/hooks/usePassportByAddress'; +import { selectCurrentAddress } from 'src/redux/features/pocket'; +import SubMenu from 'src/components/appMenu/SubMenu/SubMenu'; +import useMediaQuery from '../../../../hooks/useMediaQuery'; +import styles from './CurrentApp.module.scss'; +import { selectNetworkImg } from '../../../../utils/utils'; +import ChainInfo from './ui/ChainInfo/ChainInfo'; +import findSelectAppByUrl from './utils/findSelectAppByUrl'; +import AppSideBar from './ui/AppSideBar/AppSideBar'; +import { menuButtonId } from './utils/const'; + +function CurrentApp() { + const mediaQuery = useMediaQuery('(min-width: 768px)'); + const location = useLocation(); + const address = useAppSelector(selectCurrentAddress); + const { passport } = usePassportByAddress(address); + const [openMenu, setOpenMenu] = useState(false); + + const getRoute = useMemo(() => { + const { pathname } = location; + + return findSelectAppByUrl(pathname, passport, address); + }, [location, address, passport]); + + const toggleMenu = (newState: boolean) => { + setOpenMenu(newState); + }; + + const closeMenu = () => { + toggleMenu(false); + }; + + const toggleMenuFc = useMemo(() => () => toggleMenu(!openMenu), [openMenu]); + + return ( + <> +
+ + cyb + + {mediaQuery && } +
+ + {getRoute && getRoute[0] && ( + + + + )} + + ); +} + +export default CurrentApp; diff --git a/src/containers/application/Header/CurrentApp/ui/AppName/AppName.module.scss b/src/containers/application/Header/CurrentApp/ui/AppName/AppName.module.scss new file mode 100644 index 000000000..4f40e057e --- /dev/null +++ b/src/containers/application/Header/CurrentApp/ui/AppName/AppName.module.scss @@ -0,0 +1,6 @@ +.wrapper { + width: 100%; + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} \ No newline at end of file diff --git a/src/containers/application/Header/CurrentApp/ui/AppName/AppName.tsx b/src/containers/application/Header/CurrentApp/ui/AppName/AppName.tsx new file mode 100644 index 000000000..3a58090f4 --- /dev/null +++ b/src/containers/application/Header/CurrentApp/ui/AppName/AppName.tsx @@ -0,0 +1,42 @@ +import { useLocation } from 'react-router-dom'; +import { CHAIN_ID } from 'src/constants/config'; +import { PATTERN_CYBER } from 'src/constants/patterns'; +import { routes } from 'src/routes'; +import getMenuItems from 'src/utils/appsMenu/appsMenu'; +import findApp from 'src/utils/findApp'; +import { Helmet } from 'react-helmet'; +import styles from './AppName.module.scss'; + +function AppName() { + let { pathname } = useLocation(); + const isRobot = pathname.includes('@') || pathname.includes('neuron/'); + const isOracle = pathname.includes('oracle'); + + if (isRobot) { + const pathnameArr = pathname.replace(/^\/|\/$/g, '').split('/'); + const findItem = pathnameArr[pathnameArr.length - 1]; + pathname = + findItem.includes('@') || findItem.match(PATTERN_CYBER) + ? routes.robot.path + : findItem; + } + + if (isOracle) { + pathname = routes.oracle.path; + } + + const value = findApp(getMenuItems(), pathname); + + const content = value[0]?.name || CHAIN_ID; + + return ( + <> + + {content ? `${content.toLowerCase()} | cyb` : ''} + + {content} + + ); +} + +export default AppName; diff --git a/src/containers/application/Header/CurrentApp/ui/AppSideBar/AppSideBar.module.scss b/src/containers/application/Header/CurrentApp/ui/AppSideBar/AppSideBar.module.scss new file mode 100644 index 000000000..0bbb83607 --- /dev/null +++ b/src/containers/application/Header/CurrentApp/ui/AppSideBar/AppSideBar.module.scss @@ -0,0 +1,35 @@ +.sideBar { + position: fixed; + top: 130px; + left: 0; + width: 170px; + backdrop-filter: blur(7px); + background: #0000008c; + color: #fff; + transition: 0.3s; + z-index: 4; + opacity: 0.9; + box-shadow: 1px 0 20px 2px #000; +} + +.sideBarHide { + left: -170px; +} + +.toggleBtn { + display: flex; + width: 20px; + height: 20px; + position: absolute; + backdrop-filter: blur(7px); + background: #0000008c; + left: 100%; + top: 50%; + transform: translate(0, -50%); + border-radius: 0px 5px 5px 0px; + box-shadow: 0px 0px 5px 2px #ffffff54; + + &>div>div { + background-color: var(--blue-light); + } +} \ No newline at end of file diff --git a/src/containers/application/AppSideBar.tsx b/src/containers/application/Header/CurrentApp/ui/AppSideBar/AppSideBar.tsx similarity index 53% rename from src/containers/application/AppSideBar.tsx rename to src/containers/application/Header/CurrentApp/ui/AppSideBar/AppSideBar.tsx index dacff0de8..73f232f58 100644 --- a/src/containers/application/AppSideBar.tsx +++ b/src/containers/application/Header/CurrentApp/ui/AppSideBar/AppSideBar.tsx @@ -1,13 +1,18 @@ import cx from 'classnames'; -import styles from './styles.scss'; import useOnClickOutside from 'src/hooks/useOnClickOutside'; import { useRef } from 'react'; -import { menuButtonId } from './Header/SwitchNetwork/SwitchNetwork'; +import useMediaQuery from 'src/hooks/useMediaQuery'; +import styles from './AppSideBar.module.scss'; +import { menuButtonId } from '../../utils/const'; +import BurgerIcon from '../BurgerIcon/BurgerIcon'; interface Props { children: React.ReactNode; - openMenu: boolean; - closeMenu: () => void; + menuProps: { + isOpen: boolean; + toggleMenu: () => void; + closeMenu: () => void; + }; } function findElementInParents(element: HTMLElement, targetSelector: string) { @@ -22,10 +27,16 @@ function findElementInParents(element: HTMLElement, targetSelector: string) { return null; } -function AppSideBar({ children, openMenu, closeMenu }: Props) { +function AppSideBar({ children, menuProps }: Props) { + const mediaQuery = useMediaQuery('(min-width: 768px)'); + const { isOpen, closeMenu, toggleMenu } = menuProps; const ref = useRef(null); useOnClickOutside(ref, (e) => { + if (mediaQuery) { + return; + } + const buttonMenu = findElementInParents(e.target, `#${menuButtonId}`); if (buttonMenu) { @@ -39,9 +50,14 @@ function AppSideBar({ children, openMenu, closeMenu }: Props) { ); diff --git a/src/containers/application/Header/CurrentApp/ui/BurgerIcon/BurgerIcon.module.scss b/src/containers/application/Header/CurrentApp/ui/BurgerIcon/BurgerIcon.module.scss new file mode 100644 index 000000000..c11932b59 --- /dev/null +++ b/src/containers/application/Header/CurrentApp/ui/BurgerIcon/BurgerIcon.module.scss @@ -0,0 +1,18 @@ +.networkBtnImgIconMenu { + position: absolute; + top: 50%; + left: 50%; + transition: 0.2s; + transform: translate(-50%, -50%) rotate(-90deg); + + &Close { + transform: translate(-50%, -50%) rotate(0deg); + } + + div { + width: 15px; + height: 2px; + background-color: black; + margin: 2px 0; + } +} \ No newline at end of file diff --git a/src/containers/application/Header/CurrentApp/ui/BurgerIcon/BurgerIcon.tsx b/src/containers/application/Header/CurrentApp/ui/BurgerIcon/BurgerIcon.tsx new file mode 100644 index 000000000..8d6e7e290 --- /dev/null +++ b/src/containers/application/Header/CurrentApp/ui/BurgerIcon/BurgerIcon.tsx @@ -0,0 +1,18 @@ +import cx from 'classnames'; +import styles from './BurgerIcon.module.scss'; + +function BurgerIcon({ openMenu }: { openMenu: boolean }) { + return ( +
+
+
+
+
+ ); +} + +export default BurgerIcon; diff --git a/src/containers/application/Header/CurrentApp/ui/ChainInfo/ChainInfo.module.scss b/src/containers/application/Header/CurrentApp/ui/ChainInfo/ChainInfo.module.scss new file mode 100644 index 000000000..948d943cd --- /dev/null +++ b/src/containers/application/Header/CurrentApp/ui/ChainInfo/ChainInfo.module.scss @@ -0,0 +1,22 @@ +.containerBandwidthBar { + width: 100%; +} + +.btnContainerText { + border: none; + font-size: 16px; + background: transparent; + color: rgb(31, 203, 255); + cursor: pointer; +} + +.containerInfoSwitch { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 75px; + gap: 10px; + color: var(--blue-light); +} \ No newline at end of file diff --git a/src/containers/application/Header/CurrentApp/ui/ChainInfo/ChainInfo.tsx b/src/containers/application/Header/CurrentApp/ui/ChainInfo/ChainInfo.tsx new file mode 100644 index 000000000..ee31153ea --- /dev/null +++ b/src/containers/application/Header/CurrentApp/ui/ChainInfo/ChainInfo.tsx @@ -0,0 +1,16 @@ +import { BandwidthBar } from 'src/components'; +import styles from './ChainInfo.module.scss'; +import AppName from '../AppName/AppName'; + +function ChainInfo() { + return ( +
+ +
+ +
+
+ ); +} + +export default ChainInfo; diff --git a/src/containers/application/Header/CurrentApp/ui/IconMenu/IconMenu.module.scss b/src/containers/application/Header/CurrentApp/ui/IconMenu/IconMenu.module.scss new file mode 100644 index 000000000..76e3997cb --- /dev/null +++ b/src/containers/application/Header/CurrentApp/ui/IconMenu/IconMenu.module.scss @@ -0,0 +1,30 @@ + +.networkBtnImgIconMenu { + position: absolute; + top: 50%; + left: 50%; + transition: 0.2s; + transform: translate(-50%, -50%) rotate(-90deg); + + &Close { + transform: translate(-50%, -50%) rotate(0deg); + } + + div { + width: 35px; + height: 5px; + background-color: black; + margin: 6px 0; + } +} + + +@media (max-width: 768px) { + .networkBtnImgIconMenu { + div { + width: 25px; + height: 3px; + margin: 4px 0; + } + } +} diff --git a/src/containers/application/Header/CurrentApp/ui/IconMenu/IconMenu.tsx b/src/containers/application/Header/CurrentApp/ui/IconMenu/IconMenu.tsx new file mode 100644 index 000000000..5e12da01f --- /dev/null +++ b/src/containers/application/Header/CurrentApp/ui/IconMenu/IconMenu.tsx @@ -0,0 +1,18 @@ +import cx from 'classnames'; +import styles from './IconMenu.module.scss'; + +function IconMenu({ openMenu }: { openMenu: boolean }) { + return ( +
+
+
+
+
+ ); +} + +export default IconMenu; diff --git a/src/containers/application/Header/CurrentApp/utils/const.ts b/src/containers/application/Header/CurrentApp/utils/const.ts new file mode 100644 index 000000000..4d23746f2 --- /dev/null +++ b/src/containers/application/Header/CurrentApp/utils/const.ts @@ -0,0 +1 @@ +export const menuButtonId = 'menu-button'; diff --git a/src/containers/application/Header/CurrentApp/utils/findSelectAppByUrl.ts b/src/containers/application/Header/CurrentApp/utils/findSelectAppByUrl.ts new file mode 100644 index 000000000..d29770e49 --- /dev/null +++ b/src/containers/application/Header/CurrentApp/utils/findSelectAppByUrl.ts @@ -0,0 +1,31 @@ +import { Nullable, Option } from 'src/types'; +import { Citizenship } from 'src/types/citizenship'; +import { routes } from 'src/routes'; +import findApp from 'src/utils/findApp'; +import reduceRobotSubItems from './reduceRobotSubItems'; + +const findSelectAppByUrl = ( + url: string, + passport: Nullable, + address: Option +) => { + let pathname = url; + const isRobot = url.includes('@') || url.includes('neuron/'); + const isOracle = url.includes('oracle'); + + const itemsMenuObj = reduceRobotSubItems(passport, address); + + if (isRobot) { + pathname = routes.robot.path; + } + + if (isOracle) { + pathname = routes.oracle.path; + } + + const value = findApp(itemsMenuObj, pathname); + + return value; +}; + +export default findSelectAppByUrl; diff --git a/src/containers/application/Header/CurrentApp/utils/reduceRobotSubItems.ts b/src/containers/application/Header/CurrentApp/utils/reduceRobotSubItems.ts new file mode 100644 index 000000000..0c5810e88 --- /dev/null +++ b/src/containers/application/Header/CurrentApp/utils/reduceRobotSubItems.ts @@ -0,0 +1,36 @@ +import { CHAIN_ID } from 'src/constants/config'; +import { routes } from 'src/routes'; +import { Nullable, Option } from 'src/types'; +import { Citizenship } from 'src/types/citizenship'; +import { MenuItem, MenuItems } from 'src/types/menu'; +import { Networks } from 'src/types/networks'; +import getMenuItems from 'src/utils/appsMenu/appsMenu'; + +const reduceRobotSubItems = ( + passport: Nullable, + address: Option +) => { + const passportChain = CHAIN_ID === Networks.BOSTROM && passport; + + let linkApp: string; + if (passportChain) { + linkApp = routes.robotPassport.getLink(passport.extension.nickname); + } else if (address) { + linkApp = routes.neuron.getLink(address); + } + + return getMenuItems().reduce((acc: MenuItems, item: MenuItem) => { + if (item.to === routes.robot.path) { + item.subItems = !linkApp + ? [] + : item.subItems.map((item) => ({ + ...item, + to: `${linkApp}/${item.to}`, + })); + } + + return [...acc, { ...item }]; + }, []); +}; + +export default reduceRobotSubItems; diff --git a/src/containers/application/Header/Header.module.scss b/src/containers/application/Header/Header.module.scss index 08f5e49cb..0f2a07033 100644 --- a/src/containers/application/Header/Header.module.scss +++ b/src/containers/application/Header/Header.module.scss @@ -5,7 +5,7 @@ top: 0; padding: 15px; padding-bottom: 0; - z-index: 3; + z-index: 4; &::before { content: ''; diff --git a/src/containers/application/Header/Header.tsx b/src/containers/application/Header/Header.tsx index fb9356383..be5e9d5a6 100644 --- a/src/containers/application/Header/Header.tsx +++ b/src/containers/application/Header/Header.tsx @@ -1,20 +1,12 @@ -import SwitchNetwork from './SwitchNetwork/SwitchNetwork'; +import { useEffect, useState } from 'react'; +import cx from 'classnames'; +import CurrentApp from './CurrentApp/CurrentApp'; import Electricity from '../../home/electricity'; import SwitchAccount from './SwitchAccount/SwitchAccount'; import Commander from './Commander/Commander'; import styles from './Header.module.scss'; -import { useEffect, useState } from 'react'; -import cx from 'classnames'; -import AdviserContainer from 'src/features/adviser/AdviserContainer'; - -type Props = { - menuProps: { - isOpen: boolean; - toggleMenu: () => void; - }; -}; -function Header({ menuProps }: Props) { +function Header() { const [scroll, setScroll] = useState(false); useEffect(() => { @@ -42,10 +34,7 @@ function Header({ menuProps }: Props) { [styles.scroll]: scroll, })} > - + diff --git a/src/containers/application/Header/SwitchAccount/SwitchAccount.module.scss b/src/containers/application/Header/SwitchAccount/SwitchAccount.module.scss index 8e7ce10f2..a8e3ec29a 100644 --- a/src/containers/application/Header/SwitchAccount/SwitchAccount.module.scss +++ b/src/containers/application/Header/SwitchAccount/SwitchAccount.module.scss @@ -1,7 +1,7 @@ @mixin before-Avatar { content: ''; - width: 100px; - height: 100px; + width: 90px; + height: 90px; background: #000000; position: absolute; border-radius: 50%; @@ -33,13 +33,19 @@ grid-template-columns: 1fr 105px; gap: 25px; align-items: center; - height: 105px; + height: 95px; + + @media (max-width: 540px) { + grid-template-columns: 1fr 60px; + gap: 10px; + height: 60px; + } .noAccount { color: var(--primary-color); } - & + & { + &+& { margin-top: -30px; } @@ -54,8 +60,8 @@ .containerAvatarConnectTrue { &::before { - width: 105px; - height: 105px; + width: 95px; + height: 95px; } } } @@ -97,8 +103,8 @@ } .containerAvatarConnect { - width: 100px; - height: 100px; + width: 90px; + height: 90px; // padding: 5px; transition: all 0.2s; position: relative; @@ -123,6 +129,7 @@ &True { width: 100%; + &::before { @include before-Avatar; box-shadow: 0px 0px 20px #1fcbff66; @@ -143,4 +150,4 @@ width: 60px; height: 60px; } -} +} \ No newline at end of file diff --git a/src/containers/application/Header/SwitchAccount/SwitchAccount.tsx b/src/containers/application/Header/SwitchAccount/SwitchAccount.tsx index cc2e3101a..7a72f1897 100644 --- a/src/containers/application/Header/SwitchAccount/SwitchAccount.tsx +++ b/src/containers/application/Header/SwitchAccount/SwitchAccount.tsx @@ -9,17 +9,17 @@ import useOnClickOutside from 'src/hooks/useOnClickOutside'; import { routes } from 'src/routes'; import Pill from 'src/components/Pill/Pill'; -import { useBackend } from 'src/contexts/backend/backend'; import { useSigningClient } from 'src/contexts/signerClient'; import useIsOnline from 'src/hooks/useIsOnline'; import { useAppSelector } from 'src/redux/hooks'; import BroadcastChannelSender from 'src/services/backend/channels/BroadcastChannelSender'; +import { useBackend } from 'src/contexts/backend/backend'; +import { AvataImgIpfs } from '../../../portal/components/avataIpfs'; +import styles from './SwitchAccount.module.scss'; +import networkStyles from '../CurrentApp/CurrentApp.module.scss'; import useMediaQuery from '../../../../hooks/useMediaQuery'; import robot from '../../../../image/temple/robot.png'; -import { AvataImgIpfs } from '../../../portal/components/avataIpfs'; import Karma from '../../Karma/Karma'; -import networkStyles from '../SwitchNetwork/SwitchNetwork.module.scss'; -import styles from './SwitchAccount.module.scss'; // should be refactored function AccountItem({ @@ -153,13 +153,15 @@ function SwitchAccount() { return (
-
+
+ {(!useGetAddress || !mediaQuery) && ( + + {mediaQuery ? 'Settings' : '⚙️'} + + )} {mediaQuery && useGetAddress && (
{ - const newObj = {}; - Object.keys(data).forEach((key) => { - const valueObj = data[key]; - if (Object.prototype.hasOwnProperty.call(valueObj, 'cyber')) { - const { bech32 } = valueObj.cyber; - const bech32NewPrefix = fromBech32(bech32, prefix); - newObj[key] = { - ...valueObj, - cyber: { - ...valueObj.cyber, - bech32: bech32NewPrefix, - }, - }; - } - }); - return newObj; -}; - -const updateAddress = (prefix: any) => { - const localStoragePocketAccount = localStorage.getItem('pocketAccount'); - const localStoragePocket = localStorage.getItem('pocket'); - - if (localStoragePocket !== null) { - const localStoragePocketData = JSON.parse(localStoragePocket); - const newObjPocketData = forEachObjbech32(localStoragePocketData, prefix); - localStorage.setItem('pocket', JSON.stringify(newObjPocketData)); - } - if (localStoragePocketAccount !== null) { - const localStoragePocketAccountData = JSON.parse(localStoragePocketAccount); - const newObjAccountData = forEachObjbech32( - localStoragePocketAccountData, - prefix - ); - localStorage.setItem('pocketAccount', JSON.stringify(newObjAccountData)); - } -}; - -function SwitchNetwork({ onClickOpenMenu, openMenu }) { - const mediaQuery = useMediaQuery('(min-width: 768px)'); - - const location = useLocation(); - // const navigate = useNavigate(); - const params = useParams(); - // const dispatch = useDispatch(); - - const [controlledVisible, setControlledVisible] = React.useState(false); - const { networks } = useNetworks(); - const { getTooltipProps, setTooltipRef, visible } = usePopperTooltip({ - trigger: 'click', - closeOnOutsideClick: false, - visible: controlledVisible, - onVisibleChange: setControlledVisible, - placement: 'bottom', - }); - - const onClickChain = async (chainId: Networks, prefix: any) => { - localStorage.setItem('chainId', chainId); - updateAddress(prefix); - - // dispatch(initPocket()); - - let redirectHref = location.pathname; - if (matchPath(routes.neuron.path, location.pathname)) { - const newAddress = fromBech32(params.address, prefix); - - redirectHref = routes.neuron.getLink(newAddress); - } else if (location.pathname.includes('@')) { - redirectHref = routes.robot.path; - } - - // TODO: remove reload page (need fix config) - window.location.pathname = redirectHref; - }; - - const renderItemChain = - networks && - Object.keys(networks) - .filter((itemKey) => itemKey !== CHAIN_ID) - .map((key) => ( - - )); - - return ( - <> -
- - {mediaQuery && ( -
- -
- -
-
- )} -
- - {/* {renderItemChain && Object.keys(renderItemChain).length > 0 && ( - - {(state) => { - return ( -
-
- {renderItemChain} -
-
- ); - }} -
*/} - {/* )} */} - - ); -} - -export default SwitchNetwork; diff --git a/src/containers/application/Karma/Karma.module.scss b/src/containers/application/Karma/Karma.module.scss index eae5b50eb..94bf99bdc 100644 --- a/src/containers/application/Karma/Karma.module.scss +++ b/src/containers/application/Karma/Karma.module.scss @@ -1,4 +1,5 @@ .containerKarma { + display: flex; gap: 6px; color: #fff; font-size: 16px; diff --git a/src/containers/application/styles.scss b/src/containers/application/styles.scss index ad3bc1775..46fda257c 100644 --- a/src/containers/application/styles.scss +++ b/src/containers/application/styles.scss @@ -1,20 +1,3 @@ -.sideBar { - position: fixed; - top: 130px; - left: 0; - width: 218px; - background: #000000; - color: #fff; - transition: 0.3s; - z-index: 4; - opacity: 0.9; - box-shadow: 1px 0 20px 2px #000; -} - -.sideBarHide { - left: -250px; -} - .portal { // z-index: -1; overflow: hidden; diff --git a/src/containers/blok/blockDetails.tsx b/src/containers/blok/blockDetails.tsx index 6387ddacd..d2b2b4ea1 100644 --- a/src/containers/blok/blockDetails.tsx +++ b/src/containers/blok/blockDetails.tsx @@ -1,14 +1,14 @@ import { useEffect, useMemo, useState } from 'react'; import withRouter from 'src/components/helpers/withRouter'; import { useBlockByHeightQuery } from 'src/generated/graphql'; -import InformationBlock from './informationBlock'; -import { CardTemplate, MainContainer, TextTable } from '../../components'; -import ActionBarContainer from '../Search/ActionBarContainer'; import Table from 'src/components/Table/Table'; import StatusTxs from 'src/components/TableTxsInfinite/component/StatusTxs'; import TxHash from 'src/components/TableTxsInfinite/component/txHash'; import Display from 'src/components/containerGradient/Display/Display'; import DisplayTitle from 'src/components/containerGradient/DisplayTitle/DisplayTitle'; +import ActionBarContainer from '../Search/ActionBarContainer'; +import { MainContainer, TextTable } from '../../components'; +import InformationBlock from './informationBlock'; const initialState = { height: null, @@ -64,7 +64,7 @@ function BlockDetails({ router }) { return (
- + }> ; + return ( +
+ +
+ ); } if (error) { @@ -89,7 +99,7 @@ function Block() { } return ( -
+ -
+ ); } diff --git a/src/containers/energy/component/actionBar.tsx b/src/containers/energy/component/actionBar.tsx index a00f34164..ab57e633b 100644 --- a/src/containers/energy/component/actionBar.tsx +++ b/src/containers/energy/component/actionBar.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { ActionBar as ActionBarContainer, Tab } from '@cybercongress/gravity'; +import { Tab } from '@cybercongress/gravity'; import { coin } from '@cosmjs/launchpad'; import { Link, useNavigate } from 'react-router-dom'; import { useSigningClient } from 'src/contexts/signerClient'; @@ -56,37 +56,7 @@ function Btn({ onSelect, checkedSwitch, text, ...props }) { ); } -function ActionBarSteps({ - children, - btnText, - onClickFnc, - onClickBack, - disabled, -}) { - return ( - - {onClickBack && ( - - )} - - {children} - - {btnText && ( - - )} - - ); -} - function ActionBar({ selected, updateFnc, addressActive, selectedRoute }) { - const navigate = useNavigate(); const { signer, signingClient } = useSigningClient(); const [stage, setStage] = useState(STAGE_INIT); const [txHash, setTxHash] = useState(null); @@ -216,7 +186,7 @@ function ActionBar({ selected, updateFnc, addressActive, selectedRoute }) { if (addressActive === null) { return ( - + Start by adding a address to @@ -224,19 +194,19 @@ function ActionBar({ selected, updateFnc, addressActive, selectedRoute }) { . - + ); } if (!signingClient && !signer) { return ( - + - + ); } - if (stage === STAGE_INIT && selected === 'myEnegy') { + if (stage === STAGE_INIT && !selected) { return ( setStage(STAGE_INIT)} - btnText="Add Router" > setAliasInput(e.target.value)} placeholder="alias" /> - + ); } @@ -353,26 +325,25 @@ function ActionBar({ selected, updateFnc, addressActive, selectedRoute }) { if (stage === STAGE_DELETE_ROUTER) { return ( - setStage(STAGE_INIT)} - btnText="Delete Router" + button={{ text: 'Delete Route', onClick: generationTxs }} > Delete energy route for{' '} {Object.keys(selectedRoute).length > 0 && ( )} - + ); } if (stage === STAGE_SUBMITTED) { return ( - + check the transaction - + ); } diff --git a/src/containers/energy/component/statistics.jsx b/src/containers/energy/component/statistics.tsx similarity index 65% rename from src/containers/energy/component/statistics.jsx rename to src/containers/energy/component/statistics.tsx index 8e2177d2a..78e3ea36f 100644 --- a/src/containers/energy/component/statistics.jsx +++ b/src/containers/energy/component/statistics.tsx @@ -3,10 +3,25 @@ import { useNavigate } from 'react-router-dom'; import Card from '../ui/card'; import { formatNumber } from '../../../utils/utils'; -function Statistics({ myEnegy = 0, income = 0, outcome = 0, active }) { +type Props = { + myEnergy: number; + income: number; + outcome: number; + active?: string; +}; + +function Statistics({ myEnergy = 0, income = 0, outcome = 0, active }: Props) { const navigate = useNavigate(); - const freeEnergy = myEnegy + income - outcome; + const freeEnergy = myEnergy + income - outcome; + + const onClickNavigate = (to?: string) => { + if (!active) { + navigate(`./${to || ''}`); + } else { + navigate(`../${to || ''}`, { relative: 'path' }); + } + }; return ( navigate('./')} + value={`${formatNumber(myEnergy)} W`} + onClick={() => onClickNavigate()} /> + @@ -30,7 +45,7 @@ function Statistics({ myEnegy = 0, income = 0, outcome = 0, active }) { active={active === 'income'} title="Income" value={`${formatNumber(income)} W`} - onClick={() => navigate('./income')} + onClick={() => onClickNavigate('income')} /> - @@ -39,7 +54,7 @@ function Statistics({ myEnegy = 0, income = 0, outcome = 0, active }) { active={active === 'outcome'} title="Outcome" value={`${formatNumber(outcome)} W`} - onClick={() => navigate('./outcome')} + onClick={() => onClickNavigate('outcome')} /> = diff --git a/src/containers/energy/index.tsx b/src/containers/energy/index.tsx index a12fb3ae5..b9e5c9927 100644 --- a/src/containers/energy/index.tsx +++ b/src/containers/energy/index.tsx @@ -1,20 +1,18 @@ import { useState, useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; -import { MyEnergy, Income, Outcome } from './tab'; -import { Statistics, ActionBar } from './component'; -import useGetSlots from '../mint/useGetSlots'; -import useGetSourceRoutes from './hooks/useSourceRouted'; -import { convertResources } from '../../utils/utils'; -import { ContainerGradientText } from 'src/components'; +import { useParams } from 'react-router-dom'; import { useRobotContext } from 'src/pages/robot/robot.context'; import Display from 'src/components/containerGradient/Display/Display'; import { useAppSelector } from 'src/redux/hooks'; import { selectCurrentAddress } from 'src/redux/features/pocket'; import { useAdviser } from 'src/features/adviser/context'; +import { MyEnergy, Income, Outcome } from './tab'; +import useGetSlots from '../mint/useGetSlots'; +import { Statistics, ActionBar } from './component'; +import useGetSourceRoutes from './hooks/useSourceRouted'; +import { convertResources } from '../../utils/utils'; function RoutedEnergy() { - const location = useLocation(); - const [selected, setSelected] = useState('myEnegy'); + const { pageId } = useParams(); const [selectedIndex, setSelectedIndex] = useState(null); const { address } = useRobotContext(); @@ -36,7 +34,7 @@ function RoutedEnergy() { const { slotsData, loadingAuthAccounts, - balacesResource, + balancesResource, update: updateSlots, } = useGetSlots(address); const { @@ -50,38 +48,23 @@ function RoutedEnergy() { const selectedRoute = selectedIndex !== null && sourceRouted[Number(selectedIndex)]; - useEffect(() => { - const { pathname } = location; - if (pathname.match(/income/gm) && pathname.match(/income/gm).length > 0) { - setSelected('income'); - } else if ( - pathname.match(/outcome/gm) && - pathname.match(/outcome/gm).length > 0 - ) { - setSelected('outcome'); - } else { - setSelected('myEnegy'); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location.pathname]); - let content; - if (selected === 'myEnegy') { + if (!pageId) { content = ( ); } - if (selected === 'income') { + if (pageId === 'income') { content = ; } - if (selected === 'outcome') { + if (pageId === 'outcome') { content = ( -
- - - - - {content} -
+ + + + {content} +
{isOwner && ( { diff --git a/src/containers/energy/tab/myEnergy.tsx b/src/containers/energy/tab/myEnergy.tsx index e8693449c..36c9faf30 100644 --- a/src/containers/energy/tab/myEnergy.tsx +++ b/src/containers/energy/tab/myEnergy.tsx @@ -7,7 +7,7 @@ import { Link } from 'react-router-dom'; // TODO: finish type Props = { - balacesResource: { + balancesResource: { milliampere: number | null; millivolt: number | null; }; @@ -15,7 +15,7 @@ type Props = { loadingAuthAccounts: unknown; }; -function MyEnergy({ slotsData, balacesResource, loadingAuthAccounts }: Props) { +function MyEnergy({ slotsData, balancesResource, loadingAuthAccounts }: Props) { return (
} value={ - balacesResource.milliampere - ? formatNumber(balacesResource.milliampere) + balancesResource.milliampere + ? formatNumber(balancesResource.milliampere) : 0 } stylesContainer={{ maxWidth: '200px' }} @@ -54,8 +54,8 @@ function MyEnergy({ slotsData, balacesResource, loadingAuthAccounts }: Props) { } value={ - balacesResource.millivolt - ? formatNumber(balacesResource.millivolt) + balancesResource.millivolt + ? formatNumber(balancesResource.millivolt) : 0 } stylesContainer={{ maxWidth: '200px' }} @@ -66,9 +66,9 @@ function MyEnergy({ slotsData, balacesResource, loadingAuthAccounts }: Props) { send Deposit +
- {BASE_DENOM.toUpperCase()}
- +
- + ); } diff --git a/src/containers/txs/txsDetails.tsx b/src/containers/txs/txsDetails.tsx index f72be0c37..3c7a2cfc2 100644 --- a/src/containers/txs/txsDetails.tsx +++ b/src/containers/txs/txsDetails.tsx @@ -1,14 +1,14 @@ import { useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { useDevice } from 'src/contexts/device'; +import { MainContainer } from 'src/components'; +import { useAdviser } from 'src/features/adviser/context'; import InformationTxs from './informationTxs'; import Msgs from './msgs'; import ActionBarContainer from '../Search/ActionBarContainer'; -import { MainContainer } from '../portal/components'; import { getTxs } from './api/data'; import { ValueInformation } from './type'; import { mapResponseDataGetTxs } from './api/mapping'; -import { useAdviser } from 'src/features/adviser/context'; function TxsDetails() { const { isMobile: mobile } = useDevice(); @@ -35,7 +35,7 @@ function TxsDetails() { return ( <> - + {msgs && } diff --git a/src/containers/validator/index.tsx b/src/containers/validator/index.tsx index 397df7c4f..825327bbc 100644 --- a/src/containers/validator/index.tsx +++ b/src/containers/validator/index.tsx @@ -12,7 +12,7 @@ import { getDelegators, } from '../../utils/search/utils'; import { fromBech32, trimString } from '../../utils/utils'; -import { Loading, Copy, Tabs } from '../../components'; +import { Loading, Copy, Tabs, MainContainer } from '../../components'; import Delegated from './delegated'; import Fans from './fans'; import NotFound from '../application/notFound'; @@ -245,7 +245,7 @@ class ValidatorsDetails extends React.PureComponent { return (
-
+ )} -
+ state.pocket); + const currentAddress = useCurrentAddress(); + const { liquidBalances: accountBalances } = useGetBalances(currentAddress); + const { myCap } = useGetMySharesInPools(accountBalances); + const { vol24Total, vol24ByPool } = useWarpDexTickers(); const data = usePoolListInterval(); const { poolsData, totalCap, loading } = usePoolsAssetAmount(data); - const { addressActive } = useSetActiveAddress(defaultAccount); - const { liquidBalances: accountBalances } = useGetBalances(addressActive); - const { myCap } = useGetMySharesInPools(accountBalances); const { totalSupplyAll } = useGetTotalSupply(); - const { setAdviser } = useAdviser(); - - useEffect(() => { - if (loading) { - setAdviser('loading...', 'yellow'); - } else { - setAdviser( - - warp is power dex for all things cyber.{' '} - - api docs - - - ); - } - }, [setAdviser, loading]); + useAdviserTexts({ + isLoading: loading, + loadingText: 'loading pools', + defaultText: ( + + warp is power dex for all things cyber
+ api docs +
+ ), + }); const useMyProcent = useMemo(() => { if (totalCap > 0 && myCap > 0) { @@ -57,8 +50,8 @@ function WarpDashboardPools() { }, [totalCap, myCap]); const itemsPools = useMemo(() => { - if (poolsData.length > 0) { - return poolsData.map((item) => { + return ( + poolsData.map((item) => { const keyItem = uuidv4(); let vol24: Coin | undefined; @@ -75,14 +68,12 @@ function WarpDashboardPools() { vol24={vol24} /> ); - }); - } - return []; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [poolsData, vol24ByPool]); + }) || [] + ); + }, [poolsData, vol24ByPool, accountBalances, totalSupplyAll]); return ( - +
- {Object.keys(itemsPools).length > 0 - ? itemsPools - : !loading && } + {loading ? ( + + ) : Object.keys(itemsPools).length > 0 ? ( +
{itemsPools}
+ ) : ( + + )}
); diff --git a/src/containers/warp/hooks/usePoolsAssetAmount.ts b/src/containers/warp/hooks/usePoolsAssetAmount.ts index 8ebd77bfd..468eb9298 100644 --- a/src/containers/warp/hooks/usePoolsAssetAmount.ts +++ b/src/containers/warp/hooks/usePoolsAssetAmount.ts @@ -12,19 +12,17 @@ import { AssetsType, } from '../type'; import { convertAmount, reduceBalances } from '../../../utils/utils'; +import { useQuery } from '@tanstack/react-query'; const usePoolsAssetAmount = (pools: Option) => { const queryClient = useQueryClient(); const { marketData } = useAppData(); const { tracesDenom } = useIbcDenom(); - const [poolsBal, setPoolsBal] = useState< - OptionNeverArray - >([]); + const [poolsData, setPoolsData] = useState< OptionNeverArray >([]); const [totalCap, setTotalCap] = useState(0); - const [loading, setLoading] = useState(false); useEffect(() => { const lastPoolCapLocalStorage = localStorage.getItem('lastPoolCap'); @@ -42,19 +40,22 @@ const usePoolsAssetAmount = (pools: Option) => { // } }, []); - useEffect(() => { - (async () => { - if (!queryClient || !pools) { - return; - } - setLoading(true); + const { + data: poolsBal, + isLoading, + isInitialLoading, + isFetching, + error, + } = useQuery( + ['getAllBalances', pools?.map((pool) => pool.id).join(',')], + async () => { const newArrPools: PoolsWithAssetsType[] = []; // eslint-disable-next-line no-restricted-syntax - for await (const pool of pools) { + for await (const pool of pools!) { const assetsData: AssetsType = {}; const { reserveAccountAddress } = pool; - const getBalancePromise = await queryClient.getAllBalances( + const getBalancePromise = await queryClient!.getAllBalances( reserveAccountAddress ); @@ -69,14 +70,16 @@ const usePoolsAssetAmount = (pools: Option) => { newArrPools.push({ ...pool, assets: { ...assetsData } }); } } + return newArrPools; + }, + { + enabled: Boolean(queryClient && pools?.length > 0), + } + ); - setPoolsBal(newArrPools); - setLoading(false); - })(); - }, [queryClient, pools, tracesDenom]); - + // TODO: replace useEffect with useMemo useEffect(() => { - if (poolsBal.length > 0) { + if (poolsBal?.length > 0) { const newArrPools: PoolsWithAssetsCapType[] = []; let totalCapTemp = new BigNumber(0); poolsBal.forEach((pool) => { @@ -120,7 +123,7 @@ const usePoolsAssetAmount = (pools: Option) => { } }, [poolsBal, marketData]); - return { poolsData, totalCap, loading }; + return { poolsData, totalCap, loading: isLoading }; }; export default usePoolsAssetAmount; diff --git a/src/containers/warp/pool/PoolCard.tsx b/src/containers/warp/pool/PoolCard.tsx index ed22e6ec0..17e89efb2 100644 --- a/src/containers/warp/pool/PoolCard.tsx +++ b/src/containers/warp/pool/PoolCard.tsx @@ -92,7 +92,7 @@ function PoolCard({ {vol24 && (
-
Vol 24
+
Vol 24h
diff --git a/src/containers/warp/pool/styles.module.scss b/src/containers/warp/pool/styles.module.scss index df7047309..32ddd7513 100644 --- a/src/containers/warp/pool/styles.module.scss +++ b/src/containers/warp/pool/styles.module.scss @@ -50,7 +50,7 @@ font-weight: 600; } } - + &Inactive { position: absolute; right: 0px; @@ -79,3 +79,8 @@ margin-top: 10px; } } + +.pools { + display: grid; + gap: 15px 0; +} diff --git a/src/containers/wasm/codes/codePage/index.jsx b/src/containers/wasm/codes/codePage/index.jsx index 0e6780e86..3fa14262e 100644 --- a/src/containers/wasm/codes/codePage/index.jsx +++ b/src/containers/wasm/codes/codePage/index.jsx @@ -7,6 +7,7 @@ import CodeInfo from './CodeInfo'; import TableInstance from './TableInstance'; import styles from './styles.scss'; import { FlexWrapCantainer } from '../../ui/ui'; +import { MainContainer } from 'src/components'; const initDetails = { checksum: '', @@ -73,7 +74,7 @@ function CodePage() { ); return ( -
+ @@ -101,7 +102,7 @@ function CodePage() { -
+ ); } diff --git a/src/containers/wasm/codes/index.jsx b/src/containers/wasm/codes/index.jsx index 2871379ff..43c458594 100644 --- a/src/containers/wasm/codes/index.jsx +++ b/src/containers/wasm/codes/index.jsx @@ -6,6 +6,7 @@ import ActionBar from './actionBar'; import useSetActiveAddress from '../../../hooks/useSetActiveAddress'; import styles from './styles.scss'; +import { MainContainer } from 'src/components'; function Codes({ defaultAccount }) { const queryClient = useQueryClient(); @@ -34,14 +35,14 @@ function Codes({ defaultAccount }) { return ( <> -
+
{codes.length > 0 && codes.map((item) => { return ; })}
-
+ setUpdateFunc((item) => item + 1)} diff --git a/src/containers/wasm/contract/ContractTable.jsx b/src/containers/wasm/contract/ContractTable.jsx index 6f92fd23f..e6dc6702c 100644 --- a/src/containers/wasm/contract/ContractTable.jsx +++ b/src/containers/wasm/contract/ContractTable.jsx @@ -62,6 +62,7 @@ function ContractTable({ contracts, count, setOffset }) { style={{ borderSpacing: '5px 16px', borderCollapse: 'separate', + width: '100%', }} className="table" > diff --git a/src/containers/wasm/contract/DashboardPage.tsx b/src/containers/wasm/contract/DashboardPage.tsx index f08789950..56c10910b 100644 --- a/src/containers/wasm/contract/DashboardPage.tsx +++ b/src/containers/wasm/contract/DashboardPage.tsx @@ -4,8 +4,8 @@ import { Link } from 'react-router-dom'; import { useQueryClient } from 'src/contexts/queryClient'; import { BASE_DENOM } from 'src/constants/config'; import { useWasmDashboardPageQuery } from 'src/generated/graphql'; +import { CardStatisics, Dots, MainContainer } from 'src/components'; import { formatNumber } from '../../../utils/utils'; -import { CardStatisics, Dots } from '../../../components'; import { ContainerCardStatisics, ContainerCol } from '../ui/ui'; import ContractTable from './ContractTable'; @@ -66,7 +66,7 @@ function DashboardPage() { const { codes } = useGetCodes(); return ( -
+ )} -
+ ); } diff --git a/src/containers/wasm/contract/index.jsx b/src/containers/wasm/contract/index.jsx index 6ad6f78c0..e3440ab3c 100644 --- a/src/containers/wasm/contract/index.jsx +++ b/src/containers/wasm/contract/index.jsx @@ -13,7 +13,7 @@ import { FlexWrapCantainer, CardCantainer } from '../ui/ui'; import styles from './stylesContractPage.scss'; import RenderAbi from './renderAbi'; import ExecuteTable from './ExecuteTable'; -import { DenomArr } from '../../../components'; +import { DenomArr, MainContainer } from '../../../components'; import { BASE_DENOM } from 'src/constants/config'; function isStargateMsgExecuteContract(msg) { @@ -168,7 +168,7 @@ function ContractPage() { } = useGetInfoContractAddress(contractAddress, updateFnc); return ( -
+ @@ -211,7 +211,7 @@ function ContractPage() { -
+ ); } diff --git a/src/containers/wasm/contract/renderAbi/JsonSchemaParse.jsx b/src/containers/wasm/contract/renderAbi/JsonSchemaParse.jsx index c226aee40..c4a89e7ad 100644 --- a/src/containers/wasm/contract/renderAbi/JsonSchemaParse.jsx +++ b/src/containers/wasm/contract/renderAbi/JsonSchemaParse.jsx @@ -59,7 +59,16 @@ function JsonSchemaParse({ {contractResponse !== null && contractResponse.key === keyItem && (
Response: - +
)}
diff --git a/src/contexts/appData.tsx b/src/contexts/appData.tsx index 9d583c91b..e64e6080b 100644 --- a/src/contexts/appData.tsx +++ b/src/contexts/appData.tsx @@ -1,5 +1,6 @@ import React, { useContext, useEffect, useMemo, useState } from 'react'; import useGetMarketData from 'src/hooks/useGetMarketData'; +import useConvertMarketData from 'src/hooks/warp/useConvertMarketData'; import { ObjKeyValue } from 'src/types/data'; import { useWebsockets } from 'src/websockets/context'; @@ -28,9 +29,14 @@ export function useAppData() { function DataProvider({ children }: { children: React.ReactNode }) { const { marketData, dataTotal } = useGetMarketData(); + const convertMarketData = useConvertMarketData(marketData); const { cyber } = useWebsockets(); const [blockHeight, setBlockHeight] = useState(null); + const resultMarketData = Object.keys(convertMarketData).length + ? convertMarketData + : marketData; + useEffect(() => { if (!cyber?.connected) { return; @@ -68,11 +74,11 @@ function DataProvider({ children }: { children: React.ReactNode }) { const valueMemo = useMemo( () => ({ - marketData, + marketData: resultMarketData, dataTotalSupply: dataTotal, block: blockHeight, }), - [marketData, dataTotal, blockHeight] + [resultMarketData, dataTotal, blockHeight] ); return ( diff --git a/src/contexts/backend/backend.tsx b/src/contexts/backend/backend.tsx index ed215a3ff..80de1217a 100644 --- a/src/contexts/backend/backend.tsx +++ b/src/contexts/backend/backend.tsx @@ -3,6 +3,7 @@ import React, { useContext, useEffect, useMemo, + useRef, useState, } from 'react'; import { useAppDispatch, useAppSelector } from 'src/redux/hooks'; @@ -18,13 +19,16 @@ import DbApiWrapper from 'src/services/backend/services/DbApi/DbApi'; import { CozoDbWorker } from 'src/services/backend/workers/db/worker'; import { BackgroundWorker } from 'src/services/backend/workers/background/worker'; -import { SyncEntryName } from 'src/services/backend/types/services'; import { DB_NAME } from 'src/services/CozoDb/cozoDb'; import { RESET_SYNC_STATE_ACTION_NAME } from 'src/redux/reducers/backend'; import BroadcastChannelSender from 'src/services/backend/channels/BroadcastChannelSender'; // import BroadcastChannelListener from 'src/services/backend/channels/BroadcastChannelListener'; +import { Observable } from 'rxjs'; +import { EmbeddingApi } from 'src/services/backend/workers/background/api/mlApi'; import { SenseApi, createSenseApi } from './services/senseApi'; +import { RuneEngine } from 'src/services/scripting/engine'; +import { Option } from 'src/types'; const setupStoragePersistence = async () => { let isPersistedStorage = await navigator.storage.persisted(); @@ -33,8 +37,8 @@ const setupStoragePersistence = async () => { isPersistedStorage = await navigator.storage.persisted(); } const message = isPersistedStorage - ? `🔰 Storage is persistent.` - : `⚠️ Storage is non-persitent.`; + ? `🔰 storage is persistent` + : `⚠️ storage is non-persitent`; console.log(message); @@ -45,22 +49,19 @@ type BackendProviderContextType = { cozoDbRemote: Remote | null; senseApi: SenseApi; ipfsApi: Remote | null; - defferedDbApi: Remote | null; dbApi: DbApiWrapper | null; - ipfsNode?: Remote | null; ipfsError?: string | null; - loadIpfs?: () => Promise; - restartSync?: (name: SyncEntryName) => void; isIpfsInitialized: boolean; isDbInitialized: boolean; isSyncInitialized: boolean; isReady: boolean; + embeddingApi$: Promise>; + rune: Remote; }; const valueContext = { cozoDbRemote: null, senseApi: null, - defferedDbApi: null, isIpfsInitialized: false, isDbInitialized: false, isSyncInitialized: false, @@ -80,9 +81,6 @@ window.cyb.db = { clear: () => indexedDB.deleteDatabase(DB_NAME), }; -// const dbApi = new DbApiWrapper(); -const bcSender = new BroadcastChannelSender(); - function BackendProvider({ children }: { children: React.ReactNode }) { const dispatch = useAppDispatch(); // const { defaultAccount } = useAppSelector((state) => state.pocket); @@ -111,6 +109,47 @@ function BackendProvider({ children }: { children: React.ReactNode }) { }, [friends, following]); const isReady = isDbInitialized && isIpfsInitialized && isSyncInitialized; + const [embeddingApi$, setEmbeddingApi] = + useState>>(undefined); + // const embeddingApiRef = useRef>(); + useEffect(() => { + console.log( + process.env.IS_DEV + ? '🧪 Starting backend in DEV mode...' + : '🧬 Starting backend in PROD mode...' + ); + + (async () => { + // embeddingApiRef.current = await backgroundWorkerInstance.embeddingApi$; + const embeddingApiInstance$ = + await backgroundWorkerInstance.embeddingApi$; + setEmbeddingApi(embeddingApiInstance$); + })(); + + setupStoragePersistence(); + + const channel = new RxBroadcastChannelListener(dispatch); + + backgroundWorkerInstance.ipfsApi + .start(getIpfsOpts()) + .then(() => { + setIpfsError(null); + }) + .catch((err) => { + setIpfsError(err); + console.log(`☠️ Ipfs error: ${err}`); + }); + + cozoDbWorkerInstance.init().then(() => { + // const dbApi = createDbApi(); + const dbApi = new DbApiWrapper(); + + dbApi.init(proxy(cozoDbWorkerInstance)); + setDbApi(dbApi); + // pass dbApi into background worker + return backgroundWorkerInstance.injectDb(proxy(dbApi)); + }); + }, []); useEffect(() => { backgroundWorkerInstance.setParams({ myAddress }); @@ -118,7 +157,7 @@ function BackendProvider({ children }: { children: React.ReactNode }) { }, [myAddress, dispatch]); useEffect(() => { - isReady && console.log('🟢 Backend started.'); + isReady && console.log('🟢 backend started!'); }, [isReady]); const [dbApi, setDbApi] = useState(null); @@ -130,118 +169,31 @@ function BackendProvider({ children }: { children: React.ReactNode }) { return null; }, [isDbInitialized, dbApi, myAddress, followings]); - const createDbApi = useCallback(() => { - const dbApi = new DbApiWrapper(); - - dbApi.init(proxy(cozoDbWorkerInstance)); - setDbApi(dbApi); - return dbApi; - }, []); - - const loadIpfs = async () => { - const ipfsOpts = getIpfsOpts(); - await backgroundWorkerInstance.ipfsApi.stop(); - console.time('🔋 Ipfs started.'); - - await backgroundWorkerInstance.ipfsApi - .start(ipfsOpts) - .then(() => { - setIpfsError(null); - console.timeEnd('🔋 Ipfs started.'); - }) - .catch((err) => { - setIpfsError(err); - console.log(`☠️ Ipfs error: ${err}`); - }); - }; - useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - // const channel = new BroadcastChannelListener((msg) => { - // console.log('--------msg.data', msg.data); - // dispatch(msg.data); - // }); - const channel = new RxBroadcastChannelListener(dispatch); - - const loadCozoDb = async () => { - console.time('🔋 CozoDb worker started.'); - await cozoDbWorkerInstance - .init() - .then(async () => { - const dbApi = createDbApi(); - // pass dbApi into background worker - await backgroundWorkerInstance.init(proxy(dbApi)); - }) - .then(() => console.timeEnd('🔋 CozoDb worker started.')); - }; - (async () => { - console.log( - process.env.IS_DEV - ? '🧪 Starting backend in DEV mode...' - : '🧬 Starting backend in PROD mode...' - ); - await setupStoragePersistence(); - - const ipfsLoadPromise = async () => { - const isInitialized = await backgroundWorkerInstance.isInitialized(); - if (isInitialized) { - console.log('🔋 Background worker already active.'); - bcSender.postServiceStatus('ipfs', 'started'); - bcSender.postServiceStatus('sync', 'started'); - return Promise.resolve(); - } - return loadIpfs(); - }; - - const cozoDbLoadPromise = async () => { - const isInitialized = await cozoDbWorkerInstance.isInitialized(); - if (isInitialized) { - console.log('🔋 CozoDb worker already active.'); - bcSender.postServiceStatus('db', 'started'); - createDbApi(); - return Promise.resolve(); - } - return loadCozoDb(); - }; - - // Loading non-blocking, when ready state.backend.services.* should be changef - Promise.all([ipfsLoadPromise(), cozoDbLoadPromise()]); + backgroundWorkerInstance.setRuneDeps({ + address: myAddress, + // TODO: proxify particular methods + // senseApi: senseApi ? proxy(senseApi) : undefined, + // signingClient: signingClient ? proxy(signingClient) : undefined, + }); })(); - - window.q = backgroundWorkerInstance.ipfsQueue; - return () => channel.close(); - }, [dispatch, createDbApi]); + }, [myAddress]); const ipfsApi = useMemo( () => (isIpfsInitialized ? backgroundWorkerInstance.ipfsApi : null), [isIpfsInitialized] ); - const ipfsNode = useMemo( - () => - isIpfsInitialized ? backgroundWorkerInstance.ipfsApi.getIpfsNode() : null, - [isIpfsInitialized] - ); - - const defferedDbApi = useMemo( - () => (isDbInitialized ? backgroundWorkerInstance.defferedDbApi : null), - [isDbInitialized] - ); - const valueMemo = useMemo( () => ({ - // backgroundWorker: backgroundWorkerInstance, + rune: backgroundWorkerInstance.rune, + embeddingApi$: backgroundWorkerInstance.embeddingApi$, cozoDbRemote: cozoDbWorkerInstance, ipfsApi, - defferedDbApi, - ipfsNode, - restartSync: (name: SyncEntryName) => - backgroundWorkerInstance.restartSync(name), dbApi, senseApi, - loadIpfs, ipfsError, isIpfsInitialized, isDbInitialized, @@ -256,6 +208,7 @@ function BackendProvider({ children }: { children: React.ReactNode }) { ipfsError, senseApi, dbApi, + ipfsApi, ] ); diff --git a/src/contexts/backend/services/senseApi.ts b/src/contexts/backend/services/senseApi.ts index 33c40fcde..6c8c36c3d 100644 --- a/src/contexts/backend/services/senseApi.ts +++ b/src/contexts/backend/services/senseApi.ts @@ -87,10 +87,10 @@ export const createSenseApi = ( ) => ({ getList: async () => { const result = await dbApi.getSenseList(myAddress); - console.log( - '--- getList unread', - result.filter((r) => r.unreadCount > 0) - ); + // console.log( + // '--- getList unread', + // result.filter((r) => r.unreadCount > 0) + // ); return result; }, markAsRead: async ( @@ -194,7 +194,7 @@ export const createSenseApi = ( await dbApi.putSyncStatus(newItem); new BroadcastChannelSender().postSenseUpdate([newItem]); }, - putCyberlinsks: (links: LinkDto | LinkDto[]) => dbApi.putCyberlinks(links), + putCyberlink: (links: LinkDto | LinkDto[]) => dbApi.putCyberlinks(links), getTransactions: (neuron: NeuronAddress) => dbApi.getTransactions(neuron), getFriendItems: async (userAddress: NeuronAddress) => { if (!myAddress) { diff --git a/src/contexts/previousPage.tsx b/src/contexts/previousPage.tsx new file mode 100644 index 000000000..b0a5445ea --- /dev/null +++ b/src/contexts/previousPage.tsx @@ -0,0 +1,76 @@ +import React, { createContext, useState, useEffect, useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; + +/* + copied from, in future maybe will be possible to delete this file + https://github.com/remix-run/react-router/discussions/9860 +*/ + +interface IPreviousPageContextData { + previousPathname: string | undefined; + /** Is true if the previous page has the same origin as the current page */ + previousPageIsAppPage: boolean; + /** Is true when the previous page is lower in the path-tree */ + previousPageIsShallower: boolean; +} + +const PreviousPageContext = createContext( + undefined +); + +export function usePreviousPage() { + // if (context === undefined) { + // throw new Error( + // 'usePreviousPage must be used within a PreviousPageProvider' + // ); + // } + return React.useContext(PreviousPageContext); + // return context; +} + +interface IPreviousPageProps { + children: React.ReactNode; +} + +function PreviousPageProvider({ children }: IPreviousPageProps) { + const [currentPathname, setCurrentPathname] = useState( + undefined + ); + const [previousPathname, setPreviousPathname] = useState( + undefined + ); + const { pathname: pathnameState } = useLocation(); + + useEffect(() => { + // Guard + if (currentPathname !== window.location.pathname) { + // Update pathname + if (currentPathname !== undefined) { + setPreviousPathname(currentPathname); + } + + setCurrentPathname(window.location.pathname + window.location.search); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pathnameState]); + + const providerValue = useMemo( + () => ({ + previousPageIsAppPage: previousPathname !== undefined, + previousPageIsShallower: + previousPathname !== undefined && + previousPathname.length < window.location.pathname.length, + previousPathname, + }), + [previousPathname] + ); + + return ( + // This component will be used to encapsulate the whole App, + // so all components will have access to the Context + + {children} + + ); +} +export { PreviousPageContext, PreviousPageProvider }; diff --git a/src/contexts/scripting/scripting.tsx b/src/contexts/scripting/scripting.tsx new file mode 100644 index 000000000..fae0a08d4 --- /dev/null +++ b/src/contexts/scripting/scripting.tsx @@ -0,0 +1,147 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { UserContext } from 'src/services/scripting/types'; +import { Remote, proxy } from 'comlink'; +import { useAppDispatch, useAppSelector } from 'src/redux/hooks'; +import { + selectRuneEntypoints, + setEntrypoint, +} from 'src/redux/reducers/scripting'; +import { selectCurrentPassport } from 'src/features/passport/passports.redux'; +import { RuneEngine } from 'src/services/scripting/engine'; +import { Option } from 'src/types'; +import { EmbeddingApi } from 'src/services/backend/workers/background/api/mlApi'; +import { useBackend } from '../backend/backend'; + +type RuneFrontend = Omit; + +type ScriptingContextType = { + isSoulInitialized: boolean; + rune: Option>; + embeddingApi: Option; +}; + +const ScriptingContext = React.createContext({ + isSoulInitialized: false, + rune: undefined, + embeddingApi: undefined, +}); + +export function useScripting() { + return React.useContext(ScriptingContext); +} + +function ScriptingProvider({ children }: { children: React.ReactNode }) { + const { + rune: runeBackend, + ipfsApi, + isIpfsInitialized, + embeddingApi$, + } = useBackend(); + + const [isSoulInitialized, setIsSoulInitialized] = useState(false); + const runeRef = useRef>>(); + const embeddingApiRef = useRef>>(); + + const dispatch = useAppDispatch(); + + useEffect(() => { + runeBackend.pushContext('secrets', secrets); + + const setupObservervable = async () => { + const { isSoulInitialized$ } = runeBackend; + + const soulSubscription = (await isSoulInitialized$).subscribe((v) => { + if (v) { + runeRef.current = runeBackend; + console.log('👻 soul initalized'); + } + setIsSoulInitialized(!!v); + }); + + const embeddingApiSubscription = (await embeddingApi$).subscribe( + proxy((embeddingApi) => { + if (embeddingApi) { + embeddingApiRef.current = embeddingApi; + console.log('+ embedding api initalized', embeddingApi); + } + }) + ); + + return () => { + soulSubscription.unsubscribe(); + embeddingApiSubscription.unsubscribe(); + }; + }; + + setupObservervable(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const runeEntryPoints = useAppSelector(selectRuneEntypoints); + + const citizenship = useAppSelector(selectCurrentPassport); + const secrets = useAppSelector((state) => state.scripting.context.secrets); + + useEffect(() => { + if (!isSoulInitialized || !runeRef.current) { + return; + } + + if (citizenship) { + const particleCid = citizenship.extension.particle; + + runeRef.current.pushContext('user', { + address: citizenship.owner, + nickname: citizenship.extension.nickname, + citizenship, + particle: particleCid, + } as UserContext); + } else { + runeRef.current.popContext(['user']); + } + }, [citizenship, isSoulInitialized]); + + useEffect(() => { + if (isSoulInitialized && runeRef.current) { + runeRef.current.pushContext('secrets', secrets); + } + }, [secrets, isSoulInitialized]); + + useEffect(() => { + (async () => { + if (citizenship && ipfsApi) { + const particleCid = citizenship.extension.particle; + + if (particleCid && isIpfsInitialized) { + (async () => { + const result = await ipfsApi.fetchWithDetails(particleCid, 'text'); + + dispatch( + setEntrypoint({ name: 'particle', code: result?.content || '' }) + ); + })(); + } + } + })(); + }, [citizenship, isIpfsInitialized, ipfsApi, dispatch]); + + useEffect(() => { + runeBackend.setEntrypoints(runeEntryPoints); + }, [runeEntryPoints, runeBackend]); + + const value = useMemo(() => { + return { + rune: runeRef.current, + embeddingApi: embeddingApiRef.current, + isSoulInitialized, + }; + }, [isSoulInitialized]); + + return ( + + {children} + + ); +} + +export default ScriptingProvider; diff --git a/src/contexts/signerClient.tsx b/src/contexts/signerClient.tsx index 7cd4b18e9..7d4eca748 100644 --- a/src/contexts/signerClient.tsx +++ b/src/contexts/signerClient.tsx @@ -16,6 +16,7 @@ import { addAddressPocket, setDefaultAccount } from 'src/redux/features/pocket'; import { accountsKeplr } from 'src/utils/utils'; import usePrevious from 'src/hooks/usePrevious'; import { RPC_URL, BECH32_PREFIX, CHAIN_ID } from 'src/constants/config'; + // TODO: interface for keplr and OfflineSigner // type SignerType = OfflineSigner & { // keplr: Keplr; diff --git a/src/features/TimeFooter/TimeFooter.module.scss b/src/features/TimeFooter/TimeFooter.module.scss new file mode 100644 index 000000000..6b22a0dd9 --- /dev/null +++ b/src/features/TimeFooter/TimeFooter.module.scss @@ -0,0 +1,16 @@ +.wrapper { + display: flex; + gap: 5px; + font-size: 20px; + + &:hover { + >span { + color: var(--primary-color); + } + } + +} + +.utcTime { + color: var(--blue-light); +} \ No newline at end of file diff --git a/src/features/TimeFooter/TimeFooter.tsx b/src/features/TimeFooter/TimeFooter.tsx new file mode 100644 index 000000000..d6dff8e42 --- /dev/null +++ b/src/features/TimeFooter/TimeFooter.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from 'react'; +import dateFormat from 'dateformat'; +import { getNowUtcTime } from 'src/utils/utils'; +import { useAppSelector } from 'src/redux/hooks'; +import usePassportByAddress from 'src/features/passport/hooks/usePassportByAddress'; +import { routes } from 'src/routes'; +import { Time } from 'src/components'; +import { Link } from 'react-router-dom'; +import useMediaQuery from 'src/hooks/useMediaQuery'; +import styles from './TimeFooter.module.scss'; + +function TimeFooter() { + const { defaultAccount } = useAppSelector((state) => state.pocket); + const mediaQuery = useMediaQuery('(min-width: 1230px)'); + const useGetAddress = defaultAccount?.account?.cyber?.bech32 || null; + const { passport } = usePassportByAddress(useGetAddress); + const useGetName = passport?.extension.nickname; + const [timeSeconds, setTimeSeconds] = useState(0); + + const linkAddress = useGetName + ? routes.robotPassport.getLink(useGetName) + : useGetAddress + ? routes.neuron.getLink(useGetAddress) + : undefined; + + const linkTime = linkAddress ? `${linkAddress}/time` : routes.robot.path; + + useEffect(() => { + const getTime = () => { + const utcTime = getNowUtcTime(); + setTimeSeconds(utcTime); + }; + getTime(); + + const timeInterval = setInterval(() => { + getTime(); + }, 60000); + + return () => { + clearInterval(timeInterval); + }; + }, []); + + return ( + + {mediaQuery &&
+ } + > + } /> + } /> + } /> + } /> + + }> + {/* no page

} /> */} + } /> + } /> + + } /> + } /> + + } /> + + + } /> + + } /> + } /> + } /> + } /> + + + shouldnt be here

} /> + + ); +} + +export default Cybernet; diff --git a/src/features/cybernet/ui/components/SubnetPreview/SubnetPreview.tsx b/src/features/cybernet/ui/components/SubnetPreview/SubnetPreview.tsx new file mode 100644 index 000000000..5d448401c --- /dev/null +++ b/src/features/cybernet/ui/components/SubnetPreview/SubnetPreview.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { useCurrentContract, useCybernet } from '../../cybernet.context'; +import AvatarImgIpfs from 'src/containers/portal/components/avataIpfs/AvataImgIpfs'; +import { Tooltip } from 'src/components'; +import { Link } from 'react-router-dom'; +import { cybernetRoutes } from '../../routes'; +import SubnetPreview from './SubnetPreview'; + +function SubnetPreview({ subnetUID, withName }: { subnetUID: string }) { + const { subnetsQuery } = useCybernet(); + + const subnet = subnetsQuery.data?.find( + (subnet) => subnet.netuid === subnetUID + ); + + const { name, logo } = subnet?.metadata || {}; + + const { contractName } = useCurrentContract(); + + return ( + + + {logo && ( + + )} + + {withName && name} + + + ); +} + +export function SubnetPreviewGroup({ uids }: { uids: string[] }) { + return ( +
+ {uids.map((uid) => ( + + ))} +
+ ); +} + +export default SubnetPreview; diff --git a/src/features/cybernet/ui/cybernet.context.tsx b/src/features/cybernet/ui/cybernet.context.tsx new file mode 100644 index 000000000..7ca6ad70f --- /dev/null +++ b/src/features/cybernet/ui/cybernet.context.tsx @@ -0,0 +1,157 @@ +import React, { useMemo, useState } from 'react'; + +import useQueryCybernetContract from './useQueryCybernetContract.refactor'; +import { + ContractTypes, + ContractWithData, + Economy, + Metadata, + SubnetInfo, +} from '../types'; +import { matchPath, useLocation, useParams } from 'react-router-dom'; +import { isPussyAddress } from 'src/utils/address'; +import { cybernetRoutes } from './routes'; +import { CYBERVER_CONTRACTS_LEGACY, CYBERVER_CONTRACTS } from '../constants'; + +const CybernetContext = React.createContext<{ + contracts: ContractWithData[]; + selectContract: (address: string) => void; + selectedContract: ContractWithData; + // subnetsQuery: ReturnType; + // fix + subnetsQuery: { + data: SubnetInfo[]; + }; + subnetsQuery: any; +}>({ + contracts: {}, + selectContract: null, + subnetsQuery: null, + selectedContract: null, +}); + +export function useCybernet() { + return React.useContext(CybernetContext); +} + +function useCybernetContractWithData(address: string) { + const location = useLocation(); + + const isMainPage = !!matchPath(cybernetRoutes.verse.path, location.pathname); + + const metadataQuery = useQueryCybernetContract({ + contractAddress: address, + query: { + get_verse_metadata: {}, + }, + }); + + const economyQuery = useQueryCybernetContract({ + contractAddress: address, + query: { + get_economy: {}, + }, + + refetchInterval: isMainPage ? 20 * 1000 : undefined, + }); + + const { name, types } = metadataQuery.data || {}; + const type = + name?.includes(ContractTypes.Graph) || types?.includes(ContractTypes.Graph) + ? ContractTypes.Graph + : ContractTypes.ML; + + return { + address, + type, + metadata: { + ...metadataQuery.data, + name: CYBERVER_CONTRACTS_LEGACY.includes(address) ? '' : name, + }, + economy: economyQuery.data, + isLoading: metadataQuery.loading || economyQuery.loading, + }; +} + +function CybernetProvider({ children }: { children: React.ReactNode }) { + const [selectedContractAddress, setSelectedContractAddress] = useState( + CYBERVER_CONTRACTS[0] + ); + + const { nameOrAddress } = useParams(); + + const c1 = useCybernetContractWithData(CYBERVER_CONTRACTS[0]); + const c2 = useCybernetContractWithData(CYBERVER_CONTRACTS[1]); + + const c3 = useCybernetContractWithData(CYBERVER_CONTRACTS_LEGACY[0]); + const c4 = useCybernetContractWithData(CYBERVER_CONTRACTS_LEGACY[1]); + + const contracts = useMemo(() => [c1, c2, c3, c4], [c1, c2, c3, c4]); + + const currentContract = + nameOrAddress && + contracts.find( + (contract) => + contract.address === nameOrAddress || + contract.metadata?.name === nameOrAddress + ); + + let address; + + if (nameOrAddress && isPussyAddress(nameOrAddress)) { + address = nameOrAddress; + } else if ( + nameOrAddress && + currentContract && + currentContract.metadata?.name === nameOrAddress + ) { + address = currentContract.address; + } + + if (address && selectedContractAddress !== address) { + setSelectedContractAddress(address); + } + + const currentContract2 = contracts.find( + (contract) => contract.address === selectedContractAddress + ); + + const subnetsQuery = useQueryCybernetContract({ + query: { + get_subnets_info: {}, + }, + contractAddress: address, + }); + + return ( + { + console.log('verses', contracts); + + return { + contracts, + subnetsQuery, + selectedContract: currentContract2, + }; + }, [contracts, subnetsQuery, currentContract2])} + > + {children} + + ); +} + +export function useCurrentContract() { + const { selectedContract } = useCybernet(); + + const { metadata } = selectedContract; + const contractName = metadata?.name; + const type = selectedContract?.type; + + return { + contractName: contractName || selectedContract.address, + network: 'pussy', + type, + }; +} + +export default CybernetProvider; diff --git a/src/features/cybernet/ui/cybernetTexts.ts b/src/features/cybernet/ui/cybernetTexts.ts new file mode 100644 index 000000000..e44509676 --- /dev/null +++ b/src/features/cybernet/ui/cybernetTexts.ts @@ -0,0 +1,68 @@ +export const texts: { + [key: string]: { + default: string | { single: string; plural: string }; + graph: string | { single: string; plural: string }; + }; +} = { + contract: { + default: 'contract', + graph: 'verse', + }, + root: { + default: 'root', + graph: 'board', + }, + subnetwork: { + default: 'subnet', + graph: { + single: 'faculty', + plural: 'faculties', + }, + }, + uid: { + default: 'uid', + graph: 'card', + }, + contractOwner: { + default: 'owner', + graph: 'rector', + }, + subnetOwner: { + default: 'owner', + graph: 'dean', + }, + delegate: { + default: 'delegate', + graph: 'mentor', + }, + delegator: { + default: 'delegator', + graph: 'learner', + }, + validator: { + default: 'validator', + graph: 'professor', + }, + rootValidator: { + default: 'validator', + graph: 'lead', + }, + miner: { + default: 'miner', + graph: 'teacher', + }, +}; + +// fix +export type Texts = + | 'contract' + | 'root' + | 'subnetwork' + | 'uid' + | 'contractOwner' + | 'subnetOwner' + | 'delegate' + | 'delegator' + | 'validator' + | 'rootValidator' + | 'miner'; diff --git a/src/features/cybernet/ui/hooks/useCurrentAccountStake.ts b/src/features/cybernet/ui/hooks/useCurrentAccountStake.ts new file mode 100644 index 000000000..70c62a4d7 --- /dev/null +++ b/src/features/cybernet/ui/hooks/useCurrentAccountStake.ts @@ -0,0 +1,32 @@ +import { useAppSelector } from 'src/redux/hooks'; +import useCybernetContract from 'src/features/cybernet/ui/useQueryCybernetContract.refactor'; +import { selectCurrentAddress } from 'src/redux/features/pocket'; +import { StakeInfo } from '../../types'; + +type Props = { + address: string; + contractAddress?: string; + skip?: boolean; +}; + +export function useStake({ address, contractAddress, skip }: Props) { + const query = useCybernetContract({ + query: { + get_stake_info_for_coldkey: { + coldkey: address, + }, + }, + contractAddress, + skip: !address || skip, + }); + + return query; +} + +function useCurrentAccountStake({ skip } = {}) { + const currentAddress = useAppSelector(selectCurrentAddress); + + return useStake({ address: currentAddress, skip }); +} + +export default useCurrentAccountStake; diff --git a/src/features/cybernet/ui/hooks/useDelegate.ts b/src/features/cybernet/ui/hooks/useDelegate.ts new file mode 100644 index 000000000..64dbac7e6 --- /dev/null +++ b/src/features/cybernet/ui/hooks/useDelegate.ts @@ -0,0 +1,26 @@ +import { Delegator } from '../../types'; +import useQueryCybernetContract from '../useQueryCybernetContract.refactor'; + +function useDelegate(address: string) { + const query = useQueryCybernetContract({ + query: { + get_delegate: { + delegate: address, + }, + }, + }); + + return query; +} + +export function useDelegates() { + const query = useQueryCybernetContract({ + query: { + get_delegates: {}, + }, + }); + + return query; +} + +export default useDelegate; diff --git a/src/features/cybernet/ui/pages/Delegate/Delegate.module.scss b/src/features/cybernet/ui/pages/Delegate/Delegate.module.scss new file mode 100644 index 000000000..2b2b6ff47 --- /dev/null +++ b/src/features/cybernet/ui/pages/Delegate/Delegate.module.scss @@ -0,0 +1,23 @@ +.nominators { + margin: 20px 0; + + display: flex; + flex-direction: column; + gap: 10px; +} + +.nominatorsHeader { + display: flex; + justify-content: space-between; + align-items: center; + flex: 1; + + h3 { + font-weight: 400; + } +} + +.list { + display: flex; + gap: 0 5px; +} diff --git a/src/features/cybernet/ui/pages/Delegate/Delegate.tsx b/src/features/cybernet/ui/pages/Delegate/Delegate.tsx new file mode 100644 index 000000000..06d225772 --- /dev/null +++ b/src/features/cybernet/ui/pages/Delegate/Delegate.tsx @@ -0,0 +1,217 @@ +/* eslint-disable react/no-unstable-nested-components */ +import { useParams } from 'react-router-dom'; +import { Account, AmountDenom } from 'src/components'; +import Display from 'src/components/containerGradient/Display/Display'; +import DisplayTitle from 'src/components/containerGradient/DisplayTitle/DisplayTitle'; + +import DelegateActionBar from './DelegateActionBar/DelegateActionBar'; +import styles from './Delegate.module.scss'; +import { + Delegator, + Delegator as DelegatorType, +} from 'src/features/cybernet/types'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; +import { useAppSelector } from 'src/redux/hooks'; +import { selectCurrentAddress } from 'src/redux/features/pocket'; +import Table from 'src/components/Table/Table'; +import { createColumnHelper } from '@tanstack/react-table'; +import MusicalAddress from 'src/components/MusicalAddress/MusicalAddress'; +import subnetStyles from '../Subnet/Subnet.module.scss'; +import useDelegate from '../../hooks/useDelegate'; +import useCybernetTexts from '../../useCybernetTexts'; +import { useCurrentContract, useCybernet } from '../../cybernet.context'; +import IconsNumber from 'src/components/IconsNumber/IconsNumber'; +import { SubnetPreviewGroup } from '../../components/SubnetPreview/SubnetPreview'; +import AdviserHoverWrapper from 'src/features/adviser/AdviserHoverWrapper/AdviserHoverWrapper'; + +const columnHelper = createColumnHelper(); + +const config: keyof DelegatorType = { + take: { + text: 'Commission', + }, + validator_permits: { + text: 'Validator permits', + }, + total_daily_return: { + text: 'Total daily return', + }, + return_per_1000: { + text: 'Return per 1000 🟣', + }, +}; + +function Delegator() { + const { id } = useParams(); + + const currentAddress = useAppSelector(selectCurrentAddress); + + const address = id !== 'my' ? id : currentAddress; + + const { loading, data, error, refetch } = useDelegate(address); + const { getText } = useCybernetTexts(); + + useAdviserTexts({ + isLoading: loading, + loadingText: `loading ${getText('delegate')}`, + error, + defaultText: `${getText('delegate')} info`, + }); + + const myStake = data?.nominators.find( + ([address]) => address === currentAddress + )?.[1]; + + const nominators = data?.nominators; + + const totalStake = nominators?.reduce((acc, [, stake]) => acc + stake, 0); + + return ( + <> + {myStake && data.delegate !== currentAddress && ( + }> + + + )} + + } />} + > + {!loading && !data && ( +
+ no {getText('delegate')} found, or staking not enabled +
+ )} + +
    + {data && + Object.keys(data) + .filter((item) => !['nominators', 'delegate'].includes(item)) + .map((item) => { + const value = data[item]; + let content = value; + + if (item === 'owner') { + content = ( + + ); + } + + if (item === 'take') { + content = {(value / 65535).toFixed(2) * 100}%; + } + + if ( + [ + 'total_daily_return', + 'return_per_1000', + 'return_per_giga', + ].includes(item) + ) { + content = ( +
    + +
    + ); + } + + if (item === 'registrations' || item === 'validator_permits') { + content = ( +
      + +
    + ); + } + + return ( +
  • + {config[item]?.text || item}: {content} +
  • + ); + })} +
+
+ + {!!nominators?.length && ( + + +

{getText('delegator', true)}

+
+ +
+ +
+ + } + /> + } + > +
( + + ), + }), + columnHelper.accessor('amount', { + header: 'teach power', + cell: (info) => { + const value = info.getValue(); + + return ( +
+ +
+ ); + }, + }), + ]} + data={nominators.map(([address, amount]) => { + return { + address, + amount, + }; + })} + initialState={{ + sorting: [ + { + id: 'amount', + desc: true, + }, + ], + }} + /> + + )} + + + + ); +} + +export default Delegator; diff --git a/src/features/cybernet/ui/pages/Delegate/DelegateActionBar/DelegateActionBar.tsx b/src/features/cybernet/ui/pages/Delegate/DelegateActionBar/DelegateActionBar.tsx new file mode 100644 index 000000000..af0a9a173 --- /dev/null +++ b/src/features/cybernet/ui/pages/Delegate/DelegateActionBar/DelegateActionBar.tsx @@ -0,0 +1,233 @@ +import { useState } from 'react'; +import { AmountDenom, Button, InputNumber } from 'src/components'; + +import ActionBar from 'src/components/actionBar'; +import useExecuteCybernetContract from '../../../useExecuteCybernetContract'; +import { useGetBalance } from 'src/containers/sigma/hooks/utils'; +import { useQueryClient } from 'src/contexts/queryClient'; +import { useAppSelector } from 'src/redux/hooks'; +import { selectCurrentAddress } from 'src/redux/features/pocket'; +import useDelegate from '../../../hooks/useDelegate'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; +import useCybernetTexts from '../../../useCybernetTexts'; + +enum Steps { + INITIAL, + STAKE, + UNSTAKE, +} + +type Props = { + // delegator address + address: string; + stakedAmount: number | undefined; + onSuccess: () => void; +}; + +function DelegateActionBar({ address, stakedAmount, onSuccess }: Props) { + const [step, setStep] = useState(Steps.INITIAL); + + const currentAddress = useAppSelector(selectCurrentAddress); + + const query = useDelegate(address); + const isDelegateExists = !query.loading && !!query?.data; + + const balanceQuery = useGetBalance(currentAddress); + const availableBalance = balanceQuery.data?.liquid?.amount; + + const { getText } = useCybernetTexts(); + + const isOwner = currentAddress === address; + + const [amount, setAmount] = useState(0); + + const { setAdviser } = useAdviserTexts(); + + function handleSuccess() { + setStep(Steps.INITIAL); + setAmount(0); + balanceQuery.refetch(); + onSuccess(); + } + + const executeStake = useExecuteCybernetContract({ + query: { + add_stake: { + hotkey: address, + }, + }, + funds: [ + { + denom: 'pussy', + amount: String(amount), + }, + ], + onSuccess: handleSuccess, + successMessage: 'Stake has been successfully added', + }); + + const executeUnstake = useExecuteCybernetContract({ + query: { + remove_stake: { + hotkey: address, + amount, + }, + }, + onSuccess: handleSuccess, + successMessage: 'Stake has been successfully removed', + }); + + const executeBecomeDelegate = useExecuteCybernetContract({ + query: { + become_delegate: { + hotkey: currentAddress, + }, + }, + // onSuccess: handleSuccess, + successMessage: `You have successfully became a ${getText('delegate')}`, + }); + + let button; + let content; + let onClickBack; + + function handleClickBack() { + setStep(Steps.INITIAL); + setAmount(0); + } + + switch (step) { + case Steps.INITIAL: + if (!isDelegateExists) { + if (isOwner) { + content = ( + + ); + } + + break; + } + + content = ( + <> + + + {stakedAmount && ( + + )} + + ); + + setAdviser('Stake or unstake'); + + break; + + case Steps.STAKE: { + const { mutate, isReady, isLoading } = executeStake; + + content = ( + setAmount(Number(val))} + /> + ); + + onClickBack = handleClickBack; + + setAdviser( +
+

Stake

+ {availableBalance >= 0 && ( +

+ Available balance:{' '} + +

+ )} +
+ ); + + button = { + text: 'Stake', + onClick: mutate, + disabled: !isReady || amount === 0, + pending: isLoading, + }; + + break; + } + + case Steps.UNSTAKE: { + const { mutate, isReady, isLoading } = executeUnstake; + + content = ( + setAmount(Number(val))} + /> + ); + + onClickBack = handleClickBack; + + setAdviser( +
+

Unstake

+

+ Available balance:{' '} + +

+
+ ); + + button = { + text: 'Unstake', + onClick: mutate, + disabled: !isReady || amount === 0, + pending: isLoading, + }; + + break; + } + + default: + break; + } + + return ( + + {content} + + ); +} + +export default DelegateActionBar; diff --git a/src/features/cybernet/ui/pages/Delegates/Delegates.tsx b/src/features/cybernet/ui/pages/Delegates/Delegates.tsx new file mode 100644 index 000000000..33ecc4591 --- /dev/null +++ b/src/features/cybernet/ui/pages/Delegates/Delegates.tsx @@ -0,0 +1,31 @@ +import DelegatesTable from './DelegatesTable/DelegatesTable'; +import Display from 'src/components/containerGradient/Display/Display'; +import DisplayTitle from 'src/components/containerGradient/DisplayTitle/DisplayTitle'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; +import useCybernetTexts from '../../useCybernetTexts'; +import { useDelegates } from '../../hooks/useDelegate'; + +function Delegates() { + const { loading, error } = useDelegates(); + + const { getText } = useCybernetTexts(); + + useAdviserTexts({ + isLoading: loading, + loadingText: `loading ${getText('delegate', true)}`, + error, + defaultText: `choose ${getText('delegate')} for learning`, + }); + + return ( + } + > + + + ); +} + +export default Delegates; diff --git a/src/features/cybernet/ui/pages/Delegates/DelegatesTable/DelegatesTable.tsx b/src/features/cybernet/ui/pages/Delegates/DelegatesTable/DelegatesTable.tsx new file mode 100644 index 000000000..024bd37cc --- /dev/null +++ b/src/features/cybernet/ui/pages/Delegates/DelegatesTable/DelegatesTable.tsx @@ -0,0 +1,207 @@ +/* eslint-disable react/no-unstable-nested-components */ +import React, { useMemo } from 'react'; +import { SubnetInfo } from '../../types'; +import { createColumnHelper } from '@tanstack/react-table'; +import Table from 'src/components/Table/Table'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { Delegator } from 'src/features/cybernet/types'; +import { Account, AmountDenom } from 'src/components'; +import useCurrentAddress from 'src/features/cybernet/_move/useCurrentAddress'; +import useCybernetTexts from '../../../useCybernetTexts'; +import { cybernetRoutes } from '../../../routes'; +import { useCybernet } from '../../../cybernet.context'; +import { useDelegates } from '../../../hooks/useDelegate'; +import useCurrentAccountStake from '../../../hooks/useCurrentAccountStake'; +import IconsNumber from '../../../../../../components/IconsNumber/IconsNumber'; +import SubnetPreview, { + SubnetPreviewGroup, +} from '../../../components/SubnetPreview/SubnetPreview'; +import { tableIDs } from 'src/components/Table/tableIDs'; + +type Props = {}; + +const columnHelper = createColumnHelper(); + +const columnsIds = { + stake: 'stake', + myStake: 'myStake', +}; + +function DelegatesTable({}: Props) { + const currentAddress = useCurrentAddress(); + const { getText } = useCybernetTexts(); + + const { pathname } = useLocation(); + // maybe use props, think + const isMyLearnerPage = pathname.includes('learners/my'); + + const stakeQuery = useCurrentAccountStake({ + skip: !isMyLearnerPage, + }); + + const { data, loading: isLoading } = useDelegates(); + + const { selectedContract, subnetsQuery } = useCybernet(); + const contractName = selectedContract?.metadata?.name; + + function getTotalStake(nominators: Delegator['nominators']) { + return nominators.reduce((acc, [, stake]) => acc + stake, 0); + } + + function getMyStake(nominators: Delegator['nominators']) { + return nominators.find(([address]) => address === currentAddress)?.[1]; + } + + // maybe rename + function getYieldValue(value: number) { + return Number(((value / 1_000_000_000) * 100 * 365).toFixed(2)); + } + + const subnets = subnetsQuery.data || []; + + const navigate = useNavigate(); + + const columns = useMemo(() => { + const cols = [ + columnHelper.accessor('delegate', { + header: getText('delegate'), + enableSorting: false, + cell: (info) => { + const address = info.getValue(); + + return ( + + ); + }, + }), + + columnHelper.accessor('return_per_giga', { + header: 'yield', + sortingFn: (rowA, rowB) => { + const a = getYieldValue(rowA.original.return_per_giga.amount); + const b = getYieldValue(rowB.original.return_per_giga.amount); + + return a - b; + }, + cell: (info) => { + const value = info.getValue(); + + const yieldVal = getYieldValue(value.amount); + return `${yieldVal}%`; + }, + }), + + columnHelper.accessor('registrations', { + header: getText('subnetwork', true), + sortingFn: (rowA, rowB) => { + const a = rowA.original.registrations.length; + const b = rowB.original.registrations.length; + + return a - b; + }, + cell: (info) => { + const value = info.getValue(); + + return ( +
+ +
+ ); + }, + }), + + columnHelper.accessor('nominators', { + header: 'teach power', + id: columnsIds.stake, + sortingFn: (rowA, rowB) => { + const totalA = getTotalStake(rowA.original.nominators); + const totalB = getTotalStake(rowB.original.nominators); + + return totalA - totalB; + }, + cell: (info) => { + const nominators = info.getValue(); + const total = getTotalStake(nominators); + + return ; + }, + }), + columnHelper.accessor('nominators', { + header: 'my stake', + id: columnsIds.myStake, + sortingFn: (rowA, rowB) => { + const myStakeA = getMyStake(rowA.original.nominators) || 0; + const myStakeB = getMyStake(rowB.original.nominators) || 0; + + return myStakeA - myStakeB; + }, + cell: (info) => { + const nominators = info.getValue(); + const myStake = getMyStake(nominators); + + if (!myStake) { + return null; + } + return ; + }, + }), + ]; + + if (isMyLearnerPage) { + // cols.push() + } + + return cols; + }, [currentAddress, getText, subnets, contractName, isMyLearnerPage]); + + // use 1 loop + const myMentors = + stakeQuery.data + ?.filter(({ stake }) => stake > 0) + ?.map((stake) => stake.hotkey) || []; + + const renderData = !isMyLearnerPage + ? data + : data?.filter((mentor) => myMentors.includes(mentor.delegate)); + + return ( +
{ + const index = Number(row); + const { owner } = renderData!.find((_, i) => i === index)!; + + navigate( + cybernetRoutes.delegator.getLink('pussy', contractName, owner) + ); + }} + columns={columns} + data={renderData || []} + isLoading={isLoading} + initialState={{ + sorting: [ + { + id: columnsIds[!isMyLearnerPage ? 'stake' : 'myStake'], + desc: true, + }, + ], + }} + /> + ); +} + +export default DelegatesTable; diff --git a/src/features/cybernet/ui/pages/Main/Banner/Banner.module.scss b/src/features/cybernet/ui/pages/Main/Banner/Banner.module.scss new file mode 100644 index 000000000..84c8724fe --- /dev/null +++ b/src/features/cybernet/ui/pages/Main/Banner/Banner.module.scss @@ -0,0 +1,78 @@ +.banner { + display: flex; + position: relative; + justify-content: center; + + h1, + h2 { + font-weight: 400; + } + h1 { + color: var(--pink); + font-size: 42px; + align-self: flex-end; + + position: absolute; + right: 50px; + bottom: 15px; + } + + h2 { + font-size: 20px; + position: absolute; + left: 0; + z-index: 1; + top: 30px; + + color: var(--green-light); + align-self: flex-start; + } +} + +.center { + align-self: center; + position: relative; + display: flex; + + $size: 195px; + + min-height: $size; + + > img { + @keyframes appear { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + + animation: appear 1500ms ease-out; + position: absolute; + top: 0; + height: 100%; + left: calc(50% - #{$size} / 2); + bottom: 0; + } +} + +.rewardsBlock { + display: flex; + align-self: center; + margin: auto; + flex-direction: column; + position: relative; + z-index: 1; + top: 10px; + + span { + font-size: 40px; + text-shadow: 0 0 10px #ffffff; + } + + .rewardsText { + font-size: 16px; + align-self: flex-end; + } +} diff --git a/src/features/cybernet/ui/pages/Main/Banner/Banner.tsx b/src/features/cybernet/ui/pages/Main/Banner/Banner.tsx new file mode 100644 index 000000000..62eb73a93 --- /dev/null +++ b/src/features/cybernet/ui/pages/Main/Banner/Banner.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { useCybernet } from '../../../cybernet.context'; +import { AmountDenom } from 'src/components'; +import styles from './Banner.module.scss'; +import { TypingText } from 'src/containers/temple/pages/play/PlayBanerContent'; + +function Banner() { + const { contracts } = useCybernet(); + + const totalPaid = contracts.reduce( + (acc, contract) => + acc + Number(contract.economy?.total_rewards?.amount || 0), + 0 + ); + + return ( +
+

+ +

+ +
+ cyberver + + {!!totalPaid && ( +
+ + + rewards payed +
+ )} +
+ +

+ +

+
+ ); +} + +export default Banner; diff --git a/src/features/cybernet/ui/pages/Main/Banner/logo.png b/src/features/cybernet/ui/pages/Main/Banner/logo.png new file mode 100644 index 000000000..021c9c00f Binary files /dev/null and b/src/features/cybernet/ui/pages/Main/Banner/logo.png differ diff --git a/src/features/cybernet/ui/pages/Main/ContractsTable/ContractsTable.module.scss b/src/features/cybernet/ui/pages/Main/ContractsTable/ContractsTable.module.scss new file mode 100644 index 000000000..c5580ec38 --- /dev/null +++ b/src/features/cybernet/ui/pages/Main/ContractsTable/ContractsTable.module.scss @@ -0,0 +1,39 @@ +.wrapper { + table thead { + display: none; + } +} + +.nameCell { + position: relative; + padding-left: 30px; + text-align: left; + + > span { + color: var(--green-light); + position: absolute; + left: 10px; + } + + > a { + display: inline-flex; + + > img { + margin-right: 7px; + height: 20px; + width: 20px; + } + } +} + +.descriptionCell { + text-align: left; + display: flex; + align-items: center; + gap: 0 7px; +} + +.smallText { + font-size: 14px; + color: #a0a0a0; +} diff --git a/src/features/cybernet/ui/pages/Main/ContractsTable/ContractsTable.tsx b/src/features/cybernet/ui/pages/Main/ContractsTable/ContractsTable.tsx new file mode 100644 index 000000000..4bf635ec4 --- /dev/null +++ b/src/features/cybernet/ui/pages/Main/ContractsTable/ContractsTable.tsx @@ -0,0 +1,164 @@ +/* eslint-disable react/no-unstable-nested-components */ +import { createColumnHelper } from '@tanstack/react-table'; +import React, { useMemo } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { Cid, DenomArr } from 'src/components'; +import Table from 'src/components/Table/Table'; +import { routes } from 'src/routes'; +import { trimString } from 'src/utils/utils'; +import { useCybernet } from '../../../cybernet.context'; +import { ContractTypes, ContractWithData } from 'src/features/cybernet/types'; +import { cybernetRoutes } from '../../../routes'; +import styles from './ContractsTable.module.scss'; +import { AvataImgIpfs } from 'src/containers/portal/components/avataIpfs'; +import useParticleDetails from 'src/features/particle/useParticleDetails'; +import CIDResolver from 'src/components/CIDResolver/CIDResolver'; + +const columnHelper = createColumnHelper(); + +function ContractsTable() { + const { contracts, selectedContract } = useCybernet(); + + const navigate = useNavigate(); + + return ( +
+
{ + if (!row) { + return; + } + + const contract = contracts[row!]; + const { address, metadata: { name } = {} } = contract; + + navigate(cybernetRoutes.verse.getLink('pussy', name || address)); + }} + enableSorting={false} + columns={useMemo( + () => [ + columnHelper.accessor('metadata.name', { + header: '', + maxSize: 250, + size: 0, + cell: (info) => { + const value = info.getValue(); + + const { original } = info.row; + const logo = original.metadata?.logo; + + const { address } = original; + + const selected = selectedContract?.address === address; + + return ( +
+ {selected && } + + + {logo && } + {value || trimString(address, 6, 3)} + +
+ ); + }, + }), + + columnHelper.accessor('metadata.particle', { + header: '', + size: 300, + cell: (info) => { + const value = info.getValue(); + const row = info.row.original; + + if (!row.metadata) { + return '-'; + } + + const { type } = row; + const difficulty = + type === ContractTypes.Graph ? 'easy' : 'hard'; + + return ( +
+ {difficulty}:{' '} +

+ info} + /> +

+
+ ); + }, + }), + + columnHelper.accessor('economy.staker_apr', { + header: '', + cell: (info) => { + const value = info.getValue(); + + if (!value) { + return '-'; + } + + return ( + + {Number(info.getValue()).toFixed(2)} + % teach yield + + ); + }, + }), + columnHelper.accessor('metadata.description', { + header: '', + cell: (info) => { + const cid = info.getValue(); + + if (!cid) { + return '-'; + } + + return rules; + }, + }), + columnHelper.accessor('address', { + header: '', + id: 'network', + cell: (info) => { + const value = info.getValue(); + + return ; + }, + }), + columnHelper.accessor('address', { + header: '', + cell: (info) => { + const value = info.getValue(); + + if (value === '-') { + return '-'; + } + + return ( + + {trimString(value, 9, 3)} + + ); + }, + }), + ], + [selectedContract] + )} + data={contracts} + /> + + ); +} + +export default ContractsTable; diff --git a/src/features/cybernet/ui/pages/Main/Main.module.scss b/src/features/cybernet/ui/pages/Main/Main.module.scss new file mode 100644 index 000000000..9749f3dc2 --- /dev/null +++ b/src/features/cybernet/ui/pages/Main/Main.module.scss @@ -0,0 +1,88 @@ +.verses { + > div > div { + padding-bottom: 0; + } +} + +.info { + color: #a0a0a0; +} + +.settings { + display: flex; + align-items: center; + gap: 30px; +} + +.bgWrapper { + position: relative; + &:nth-of-type(1) { + background-image: url('./images/1.jpg'); + } + &:nth-of-type(2) { + background-image: url('./images/3.jpg'); + } + &:nth-of-type(3) { + background-image: url('./images/2.jpg'); + } + + > div { + height: 100%; + } + background-repeat: no-repeat; + background-size: cover; + background-position: center; +} + +.actions { + justify-content: space-between; + align-items: stretch; + display: flex; + gap: 0 15px; + + .actionTitle { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + + h3 { + font-size: 30px; + font-weight: 400; + } + } + + .actionText { + color: #a0a0a0; + margin-bottom: 20px; + } + + .apr { + font-size: 14px; + + color: #a0a0a0; + + span { + font-size: 20px; + display: block; + color: var(--green-light); + } + } + + .links { + display: flex; + gap: 15px; + flex-direction: column; + } + + > div { + flex: 1; + } +} + +.delegatorsBtn { + font-size: 16px; + margin: 0; + text-align: left; + cursor: not-allowed; +} diff --git a/src/features/cybernet/ui/pages/Main/Main.tsx b/src/features/cybernet/ui/pages/Main/Main.tsx new file mode 100644 index 000000000..dde33e5eb --- /dev/null +++ b/src/features/cybernet/ui/pages/Main/Main.tsx @@ -0,0 +1,235 @@ +import Display from 'src/components/containerGradient/Display/Display'; +import { LinkWindow } from 'src/components'; +import DisplayTitle from 'src/components/containerGradient/DisplayTitle/DisplayTitle'; +import { Link, Navigate, useParams } from 'react-router-dom'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; +import { cybernetRoutes } from '../../routes'; +import useCurrentAddress from 'src/features/cybernet/_move/useCurrentAddress'; +import styles from './Main.module.scss'; +import useCurrentAccountStake from '../../hooks/useCurrentAccountStake'; +import useDelegate from '../../hooks/useDelegate'; +import ContractsTable from './ContractsTable/ContractsTable'; +import useCybernetTexts from '../../useCybernetTexts'; +import { useCybernet } from '../../cybernet.context'; +import Banner from './Banner/Banner'; +import { Stars } from 'src/containers/portal/components'; + +function Main() { + const address = useCurrentAddress(); + + const { getText } = useCybernetTexts(); + + useAdviserTexts({ + defaultText: ( +
+ Rewards are currently not being distributed because +
contract execution is exceeding the block gas limit +
+ This issue{' '} + + will be resolved soon + +
+ ), + }); + + const { data } = useDelegate(address); + const currentAddressIsDelegator = !!data; + + const { data: currentStake } = useCurrentAccountStake(); + const haveStake = currentStake?.some(({ stake }) => stake > 0); + + const { selectedContract, contracts } = useCybernet(); + + const { nameOrAddress } = useParams(); + + if (!nameOrAddress && contracts.length) { + return ( + + ); + } + + const { + metadata: { name } = {}, + address: contractAddress, + network = 'pussy', + } = selectedContract || {}; + + const contractNameOrAddress = name || contractAddress; + + const { staker_apr, validator_apr } = selectedContract?.economy || {}; + + return ( + <> + + + +
+ } + > + + +
+ +
+
+ +

stake

+ + {staker_apr && ( +
+ yield up to
+ + {Number( + selectedContract?.economy?.staker_apr + ).toFixed(2)} + % + +
+ )} +
+ } + /> + } + > +

+ learn by staking on {getText('delegate', true)} +

+
+ + {getText('delegate', true)} + + + + + {haveStake && ( + + my {getText('delegator')} + + )} +
+ +
+ +
+ +

mine

+ {validator_apr && ( +
+ yield up to + + {Number( + selectedContract?.economy?.validator_apr + ).toFixed(2)} + % + +
+ )} +
+ } + /> + } + > +

teach by linking content

+ +
+ + {getText('root')} + + + + {getText('subnetwork', true)} + + + {currentAddressIsDelegator && ( + + my {getText('delegate')} + + )} +
+ + + +
+ +

deploy

+
+ } + /> + } + > +
+ + cli and python package + + + + subnet template + + + + cosmwasm contract + + + docs +
+ + + + + ); +} + +export default Main; diff --git a/src/features/cybernet/ui/pages/Main/images/1.jpg b/src/features/cybernet/ui/pages/Main/images/1.jpg new file mode 100644 index 000000000..d88265c76 Binary files /dev/null and b/src/features/cybernet/ui/pages/Main/images/1.jpg differ diff --git a/src/features/cybernet/ui/pages/Main/images/2.jpg b/src/features/cybernet/ui/pages/Main/images/2.jpg new file mode 100644 index 000000000..6f014c6f1 Binary files /dev/null and b/src/features/cybernet/ui/pages/Main/images/2.jpg differ diff --git a/src/features/cybernet/ui/pages/Main/images/3.jpg b/src/features/cybernet/ui/pages/Main/images/3.jpg new file mode 100644 index 000000000..4b8edec20 Binary files /dev/null and b/src/features/cybernet/ui/pages/Main/images/3.jpg differ diff --git a/src/features/cybernet/ui/pages/MyStake/MyStake.module.scss b/src/features/cybernet/ui/pages/MyStake/MyStake.module.scss new file mode 100644 index 000000000..7c2fee918 --- /dev/null +++ b/src/features/cybernet/ui/pages/MyStake/MyStake.module.scss @@ -0,0 +1,10 @@ +.list { + display: flex; + flex-direction: column; + gap: 15px; + + li { + display: flex; + flex-direction: column; + } +} diff --git a/src/features/cybernet/ui/pages/MyStake/MyStake.tsx b/src/features/cybernet/ui/pages/MyStake/MyStake.tsx new file mode 100644 index 000000000..c43336b52 --- /dev/null +++ b/src/features/cybernet/ui/pages/MyStake/MyStake.tsx @@ -0,0 +1,58 @@ +import Display from 'src/components/containerGradient/Display/Display'; +import DisplayTitle from 'src/components/containerGradient/DisplayTitle/DisplayTitle'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; +import useCurrentAccountStake from '../../hooks/useCurrentAccountStake'; +import { Helmet } from 'react-helmet'; +import DelegatesTable from '../Delegates/DelegatesTable/DelegatesTable'; +import { HeaderItem } from '../Subnet/SubnetHeader/SubnetHeader'; +import IconsNumber from 'src/components/IconsNumber/IconsNumber'; + +function MyStake() { + const { loading, error, data } = useCurrentAccountStake(); + + useAdviserTexts({ + isLoading: loading, + error: error?.message, + defaultText: 'my learner', + }); + + let length = 0; + + const totalStake = + data?.reduce((acc, { stake }) => { + if (stake > 0) { + length += 1; + } + + return acc + stake; + }, 0) || 0; + + return ( + <> + + my learner | cyb + + + +
+ + + } + /> +
+
+ }> + + + + ); +} + +export default MyStake; diff --git a/src/features/cybernet/ui/pages/Sigma/Sigma.module.scss b/src/features/cybernet/ui/pages/Sigma/Sigma.module.scss new file mode 100644 index 000000000..b7ba23c06 --- /dev/null +++ b/src/features/cybernet/ui/pages/Sigma/Sigma.module.scss @@ -0,0 +1,32 @@ +.wrapper { + > div { + // for chooser + z-index: 1; + } +} + +.chooser { + margin-left: 15px; + + > * { + width: 380px; + } +} + +.legacy { + color: gray; + font-size: 14px; + + position: relative; + top: 2.5px; +} + +.item { + display: flex; + align-items: center; + justify-content: space-between; + + & + .item { + margin-top: 20px; + } +} diff --git a/src/features/cybernet/ui/pages/Sigma/Sigma.tsx b/src/features/cybernet/ui/pages/Sigma/Sigma.tsx new file mode 100644 index 000000000..f28d6d5e0 --- /dev/null +++ b/src/features/cybernet/ui/pages/Sigma/Sigma.tsx @@ -0,0 +1,171 @@ +import { Account, AmountDenom, MainContainer } from 'src/components'; +import Sigma from '../../../../../containers/sigma/index'; +import useCurrentAddress from 'src/features/cybernet/_move/useCurrentAddress'; +import Display from 'src/components/containerGradient/Display/Display'; +import { cybernetRoutes } from '../../routes'; +import { useStake } from '../../hooks/useCurrentAccountStake'; +import DisplayTitle from 'src/components/containerGradient/DisplayTitle/DisplayTitle'; +import { trimString } from 'src/utils/utils'; +import styles from './Sigma.module.scss'; +import Loader2 from 'src/components/ui/Loader2'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; +import { Link } from 'react-router-dom'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { routes } from 'src/routes'; +import { useCybernet } from '../../cybernet.context'; +import { AccountInput } from 'src/pages/teleport/components/Inputs'; +import IconsNumber from 'src/components/IconsNumber/IconsNumber'; +import { + CYBERVER_CONTRACTS, + CYBERVER_CONTRACTS_LEGACY, +} from 'src/features/cybernet/constants'; + +function Item({ contractAddress, callback, address }) { + const query = useStake({ + address, + contractAddress, + }); + + const filteredData = query.data?.filter(({ stake }) => stake > 0); + + const total = useMemo(() => { + return filteredData?.reduce((acc, { stake }) => acc + stake, 0) || 0; + }, [filteredData]); + + useEffect(() => { + callback(total, contractAddress); + }, [total, callback, contractAddress]); + + const isLegacy = CYBERVER_CONTRACTS_LEGACY.includes(contractAddress); + + const { contracts } = useCybernet(); + + const contractName = contracts.find( + (contract) => contract.address === contractAddress + )?.metadata?.name; + + return ( + + + {contractName || trimString(contractAddress, 6, 6)} + + + {isLegacy && (legacy)} + + } + > + + + } + > + {query.loading ? ( + + ) : query.error ? ( + query.error.message + ) : filteredData?.length > 0 ? ( + filteredData + .sort((a, b) => b.stake - a.stake) + .map(({ hotkey, stake }) => { + return ( +
+ + +
+ +
+
+ ); + }) + ) : ( + 'No stakes' + )} +
+ ); +} + +function Sigma() { + useAdviserTexts({ + defaultText: 'learners stake stat', + }); + + const [total, setTotal] = useState<{ + [key: string]: number; + }>({}); + + const currentAddress = useCurrentAddress(); + const [address, setAddress] = useState(currentAddress); + + useEffect(() => { + if (!address && currentAddress) { + setAddress(currentAddress); + } + }, [currentAddress, address]); + + const sum = Object.values(total).reduce((acc, value) => acc + value, 0); + + const handleTotal = useCallback((value: number, contractAddress: string) => { + setTotal((prev) => ({ + ...prev, + [contractAddress]: value, + })); + }, []); + + return ( + <> +
+ + Sigma +
+ +
+ + } + > + + + } + > + {' '} +
+
+ + {CYBERVER_CONTRACTS.map((contractAddress) => ( + + ))} + + ); +} + +export default Sigma; diff --git a/src/features/cybernet/ui/pages/Subnet/GradeSetterInput/GradeSetterInput.tsx b/src/features/cybernet/ui/pages/Subnet/GradeSetterInput/GradeSetterInput.tsx new file mode 100644 index 000000000..a64a3c688 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/GradeSetterInput/GradeSetterInput.tsx @@ -0,0 +1,79 @@ +import { InputNumber } from 'src/components'; +import { useSubnet } from '../subnet.context'; +import { useEffect, useRef } from 'react'; +import { usePreviousPage } from 'src/contexts/previousPage'; + +type Props = { + uid: number; +}; + +function GradeSetterInput({ uid }: Props) { + const { + subnetQuery, + neuronsQuery: { data: neurons }, + grades: { newGrades }, + } = useSubnet(); + + const rootSubnet = subnetQuery.data?.netuid === 0; + + const { previousPathname } = usePreviousPage(); + + const ref = useRef(null); + + const handled = useRef(false); + + // need this because autoFocus not updateable + // bullshit, need refactor + useEffect(() => { + if (handled.current === true) { + return; + } + + if (rootSubnet) { + return; + } + + const search = new URLSearchParams(previousPathname?.split('?')[1]); + const neuron = search.get('neuron'); + + const hothey = neurons?.find((n) => n.uid === uid)?.hotkey; + + if (ref.current && neuron === hothey) { + ref.current.querySelector('input')?.focus(); + handled.current = true; + } + }, [previousPathname, neurons, rootSubnet, uid]); + + return ( +
+ { + newGrades?.setGrade(uid.toString(), +e); + }} + /> + + {/* { + newGrades?.setGrade(uid.toString(), amount); + }} + value={newGrades?.data?.[uid] || 0} + railStyle={{ + backgroundColor: 'red', + }} + marks={{ + 0: '0', + 5: '5', + 10: '10', + }} + /> */} +
+ ); +} + +export default GradeSetterInput; diff --git a/src/features/cybernet/ui/pages/Subnet/Subnet.module.scss b/src/features/cybernet/ui/pages/Subnet/Subnet.module.scss new file mode 100644 index 000000000..b7d4f1a6e --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/Subnet.module.scss @@ -0,0 +1,20 @@ +@import '../../../../../style/mixins.scss'; + +.list { + min-width: 50%; + + > li { + display: flex; + padding: 10px 15px; + justify-content: space-between; + width: 100%; + + > div { + text-align: right; + } + + &:hover { + @include tableHover; + } + } +} diff --git a/src/features/cybernet/ui/pages/Subnet/Subnet.tsx b/src/features/cybernet/ui/pages/Subnet/Subnet.tsx new file mode 100644 index 000000000..294419ba7 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/Subnet.tsx @@ -0,0 +1,107 @@ +import { Route, Routes, useParams } from 'react-router-dom'; +import { Tabs } from 'src/components'; +import ActionBar from './SubnetActionBar/SubnetActionBar'; +import Weights from './tabs/Weights/Weights'; +import SubnetInfo from './tabs/SubnetInfo/SubnetInfo'; +import SubnetProvider, { useCurrentSubnet } from './subnet.context'; +import SubnetNeurons from './tabs/SubnetNeurons/SubnetNeurons'; +import SubnetSubnets from './tabs/SubnetSubnets/SubnetSubnets'; +import useCybernetTexts from '../../useCybernetTexts'; +import SubnetHeader from './SubnetHeader/SubnetHeader'; +import Loader2 from 'src/components/ui/Loader2'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; + +function Subnet() { + const { ...rest } = useParams(); + const tab = rest['*']; + + // const {selectedContract} = useCybernet(); + + const { subnetQuery, neuronsQuery, subnetRegistrationQuery, isRootSubnet } = + useCurrentSubnet(); + + const { getText } = useCybernetTexts(); + + useAdviserTexts({ + isLoading: subnetQuery.loading, + loadingText: `loading ${getText('subnetwork')}`, + error: subnetQuery.error || neuronsQuery.error, + // defaultText: 'subnet', + }); + + const addressRegisteredInSubnet = subnetRegistrationQuery.data !== null; + + const tabs = [ + { + to: './info', + key: 'info', + text: 'info', + }, + { + to: './', + key: 'delegates', + text: getText(isRootSubnet ? 'rootValidator' : 'delegate', true), + }, + + { + to: './grades', + key: 'grades', + text: 'grades', + disabled: isRootSubnet, + }, + ]; + + if (isRootSubnet) { + tabs.push({ + to: './faculties', + key: 'faculties', + text: getText('subnetwork', true), + }); + } + + return ( + <> + + + + + + + } + /> + + } /> + + {subnetQuery.data?.subnetwork_n > 0 && ( + } /> + )} + + + } + /> + + + {subnetQuery.loading && } + + + + ); +} + +export default function SubnetWithProvider() { + return ( + + + + ); +} diff --git a/src/features/cybernet/ui/pages/Subnet/SubnetActionBar/SubnetActionBar.module.scss b/src/features/cybernet/ui/pages/Subnet/SubnetActionBar/SubnetActionBar.module.scss new file mode 100644 index 000000000..8f963f642 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/SubnetActionBar/SubnetActionBar.module.scss @@ -0,0 +1,5 @@ +@import '../../../../../../style/mixins.scss'; + +.installBtn { + @include withShareIcon; +} diff --git a/src/features/cybernet/ui/pages/Subnet/SubnetActionBar/SubnetActionBar.tsx b/src/features/cybernet/ui/pages/Subnet/SubnetActionBar/SubnetActionBar.tsx new file mode 100644 index 000000000..a6240ace4 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/SubnetActionBar/SubnetActionBar.tsx @@ -0,0 +1,159 @@ +import { useState } from 'react'; + +import ActionBar from 'src/components/actionBar'; +import { useAdviser } from 'src/features/adviser/context'; +import { selectCurrentAddress } from 'src/redux/features/pocket'; +import { useAppSelector } from 'src/redux/hooks'; +import useExecuteCybernetContract from '../../../useExecuteCybernetContract'; +import useCybernetTexts from '../../../useCybernetTexts'; +import { useCurrentContract, useCybernet } from '../../../cybernet.context'; +import { ContractTypes } from 'src/features/cybernet/types'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; +import { AmountDenom } from 'src/components'; +import styles from './SubnetActionBar.module.scss'; +import { useCurrentSubnet } from '../subnet.context'; + +type Props = {}; + +enum Steps { + INITIAL, + REGISTER_CONFIRM, +} + +function SubnetActionBar({}: Props) { + const [step, setStep] = useState(Steps.INITIAL); + + const { netuid, subnetQuery, subnetRegistrationQuery, neuronsQuery } = + useCurrentSubnet(); + + const address = useAppSelector(selectCurrentAddress); + + const { setAdviser } = useAdviser(); + const { getText } = useCybernetTexts(); + + const notRegistered = subnetRegistrationQuery.data === null; + + const burn = subnetQuery.data?.burn; + + function handleSuccess() { + setAdviser('Registered', 'green'); + setStep(Steps.INITIAL); + + subnetRegistrationQuery.refetch(); + neuronsQuery.refetch(); + + // think if need + subnetQuery.refetch(); + } + + const subnetRegisterExecution = useExecuteCybernetContract({ + query: { + burned_register: { + netuid, + hotkey: address, + }, + }, + funds: [ + { + denom: 'pussy', + amount: String(burn), + }, + ], + onSuccess: handleSuccess, + }); + + const rootSubnetRegisterExecution = useExecuteCybernetContract({ + query: { + root_register: { + hotkey: address, + }, + }, + onSuccess: handleSuccess, + }); + + let button; + let content; + let onClickBack: undefined | (() => void); + + const { type } = useCurrentContract(); + + const isMlVerse = type === ContractTypes.ML; + + let text; + + // refactor ifs + if (notRegistered && !isMlVerse) { + text = `join ${getText('subnetwork')}`; + } else if (isMlVerse) { + text = 'use cli to register in ML verse subnets'; + } + + useAdviserTexts({ + defaultText: text, + }); + + const isRoot = netuid === 0; + + if (notRegistered && netuid === 0) { + return ( + + ); + } + + switch (step) { + case Steps.INITIAL: + if (!notRegistered) { + break; + } + + if (!isRoot && isMlVerse) { + button = { + text: install, + link: 'https://github.com/cybercongress/cybertensor', + }; + break; + } + + button = { + text: `Register to ${getText('subnetwork')}`, + onClick: () => setStep(Steps.REGISTER_CONFIRM), + }; + + break; + + case Steps.REGISTER_CONFIRM: + button = { + text: 'Confirm registration', + onClick: subnetRegisterExecution.mutate, + disabled: subnetRegisterExecution.isLoading, + }; + + onClickBack = () => setStep(Steps.INITIAL); + + content = ( + <> + fee is + + + ); + + break; + + default: + break; + } + + return ( + + {content} + + ); +} + +export default SubnetActionBar; diff --git a/src/features/cybernet/ui/pages/Subnet/SubnetHeader/SubnetHeader.module.scss b/src/features/cybernet/ui/pages/Subnet/SubnetHeader/SubnetHeader.module.scss new file mode 100644 index 000000000..8d75a21bd --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/SubnetHeader/SubnetHeader.module.scss @@ -0,0 +1,36 @@ +.wrapper { + display: flex; + justify-content: space-around; + + align-items: center; + gap: 0 13px; + + .name { + color: var(--pink); + font-size: 30px; + margin-top: -5px; + display: block; + } + + .logo { + img { + max-width: 80px; + } + } +} + +.item { + height: 100%; + align-self: stretch; + + font-size: 20px; + + h6 { + color: #616161; + font-size: 14px; + font-weight: 400; + display: block; + + margin-bottom: 15px; + } +} diff --git a/src/features/cybernet/ui/pages/Subnet/SubnetHeader/SubnetHeader.tsx b/src/features/cybernet/ui/pages/Subnet/SubnetHeader/SubnetHeader.tsx new file mode 100644 index 000000000..93b96ea35 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/SubnetHeader/SubnetHeader.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { AmountDenom, Cid } from 'src/components'; +import Display from 'src/components/containerGradient/Display/Display'; +import DisplayTitle from 'src/components/containerGradient/DisplayTitle/DisplayTitle'; +import { trimString } from 'src/utils/utils'; +import { useSubnet } from '../subnet.context'; +import styles from './SubnetHeader.module.scss'; +import useCybernetTexts from '../../../useCybernetTexts'; +import { AvataImgIpfs } from 'src/containers/portal/components/avataIpfs'; +import IconsNumber from 'src/components/IconsNumber/IconsNumber'; + +function Item({ title, content }) { + return ( +
+
{title}
+ {content} +
+ ); +} + +export const HeaderItem = Item; + +function SubnetHeader() { + const { subnetQuery, neuronsQuery } = useSubnet(); + const { getText } = useCybernetTexts(); + + const metadata = subnetQuery.data?.metadata || {}; + + // fix + if (!metadata?.name) { + return null; + } + if (!subnetQuery.data) { + return null; + } + + const totalNeuronsStake = + neuronsQuery.data?.reduce( + (acc, neuron) => acc + neuron.stake.reduce((acc, b) => acc + b[1], 0), + 0 + ) || 0; + + const { + netuid, + difficulty, + tempo, + max_allowed_validators: maxAllowedValidators, + } = subnetQuery.data; + + const { logo, description, name } = metadata; + + return ( + +
+ {name}} + /> + + + +
+ +
+ description + } + /> +
+
+ ); +} + +export default SubnetHeader; diff --git a/src/features/cybernet/ui/pages/Subnet/subnet.context.tsx b/src/features/cybernet/ui/pages/Subnet/subnet.context.tsx new file mode 100644 index 000000000..75162f197 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/subnet.context.tsx @@ -0,0 +1,130 @@ +import React, { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import useCurrentAddress from 'src/features/cybernet/_move/useCurrentAddress'; +import { + SubnetHyperParameters, + SubnetInfo, + SubnetNeuron, +} from 'src/features/cybernet/types'; +import useCybernetContract from 'src/features/cybernet/ui/useQueryCybernetContract.refactor'; +import useCurrentSubnetGrades from './useCurrentSubnetGrades'; + +const SubnetContext = React.createContext<{ + subnetQuery: ReturnType>; + hyperparamsQuery: ReturnType< + typeof useCybernetContract + >; + neuronsQuery: ReturnType>; + + addressRegisteredInSubnet: boolean; + isRootSubnet: boolean; + netuid: number; + subnetRegistrationQuery: ReturnType< + typeof useCybernetContract + >; + grades: ReturnType; + + // refetch: () => void; +}>({ + subnetQuery: null, + neuronsQuery: null, + hyperparamsQuery: null, + addressRegisteredInSubnet: false, + isRootSubnet: false, + netuid: 0, +}); + +/** + @deprecated + */ +export function useSubnet() { + return React.useContext(SubnetContext); +} + +export function useCurrentSubnet() { + return useSubnet(); +} + +function SubnetProvider({ children }: { children: React.ReactNode }) { + const { id } = useParams(); + const f = id === 'board' ? 0 : +id; + const netuid = Number(f); + const isRootSubnet = netuid === 0; + + const currentAddress = useCurrentAddress(); + + const subnetQuery = useCybernetContract({ + query: { + get_subnet_info: { + netuid, + }, + }, + }); + + const hyperparamsQuery = useCybernetContract({ + query: { + get_subnet_hyperparams: { + netuid, + }, + }, + }); + + const neuronsQuery = useCybernetContract({ + query: { + get_neurons: { + netuid, + }, + }, + }); + + const subnetRegistrationQuery = useCybernetContract({ + query: { + get_uid_for_hotkey_on_subnet: { + netuid, + hotkey: currentAddress, + }, + }, + }); + + const grades = useCurrentSubnetGrades({ + netuid, + neuronsQuery, + hyperparamsQuery, + }); + + const addressRegisteredInSubnet = subnetRegistrationQuery?.data !== null; + + const value = useMemo(() => { + return { + netuid, + isRootSubnet, + subnetQuery, + hyperparamsQuery, + neuronsQuery, + addressRegisteredInSubnet, + subnetRegistrationQuery, + grades, + // refetch: () => { + // subnetQuery.refetch(); + // neuronsQuery.refetch(); + // subnetUidQuery.refetch(); + // }, + }; + }, [ + addressRegisteredInSubnet, + subnetQuery, + neuronsQuery, + grades, + netuid, + isRootSubnet, + hyperparamsQuery, + subnetRegistrationQuery, + // subnetUidQuery, + ]); + + return ( + {children} + ); +} + +export default SubnetProvider; diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/SubnetHyperParams/SubnetHyperParams.tsx b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetHyperParams/SubnetHyperParams.tsx new file mode 100644 index 000000000..15e9742bd --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetHyperParams/SubnetHyperParams.tsx @@ -0,0 +1,110 @@ +import { Link, useParams } from 'react-router-dom'; +import Display from 'src/components/containerGradient/Display/Display'; +import DisplayTitle from 'src/components/containerGradient/DisplayTitle/DisplayTitle'; +import { SubnetHyperParameters } from 'src/features/cybernet/types'; +import useCybernetContract from 'src/features/cybernet/ui/useQueryCybernetContract.refactor'; + +import styles from '../../Subnet.module.scss'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; +import { routes } from 'src/routes'; +import { useCurrentSubnet } from '../../subnet.context'; + +const config: { [K in keyof SubnetHyperParameters]: { text: string } } = { + rho: { + text: 'Rho', + }, + kappa: { + text: 'Kappa', + }, + immunity_period: { + text: 'Immunity Period', + }, + min_allowed_weights: { + text: 'Min Allowed Weights', + }, + max_weights_limit: { + text: 'Max Weights Limit', + }, + tempo: { + text: 'Tempo', + }, + min_difficulty: { + text: 'Min Difficulty', + }, + max_difficulty: { + text: 'Max Difficulty', + }, + weights_version: { + text: 'Weights Version', + }, + weights_rate_limit: { + text: 'Weights Rate Limit', + }, + adjustment_interval: { + text: 'Adjustment Interval', + }, + activity_cutoff: { + text: 'Activity Cutoff', + }, + registration_allowed: { + text: 'Registration Allowed', + }, + target_regs_per_interval: { + text: 'Target Regs Per Interval', + }, + min_burn: { + text: 'Min Burn', + }, + max_burn: { + text: 'Max Burn', + }, + bonds_moving_avg: { + text: 'Bonds Moving Avg', + }, + max_regs_per_block: { + text: 'Max Regs Per Block', + }, +}; + +function SubnetHyperParams() { + const { hyperparamsQuery } = useCurrentSubnet(); + + useAdviserTexts({ + isLoading: hyperparamsQuery.loading, + error: hyperparamsQuery.error, + defaultText: 'Subnet hyperparams', + }); + + return ( + }> +
    + {hyperparamsQuery.data && + Object.keys(hyperparamsQuery.data).map((item) => { + const value = hyperparamsQuery.data[item]; + let content = value; + + if (['min_burn', 'max_burn'].includes(item)) { + content = {value.toLocaleString()} 🟣; + } + + if (item === 'registration_allowed') { + content = {value === true ? 'yes' : 'no'}; + } + + const title = config[item].text || item; + + return ( +
  • + + {title} + +
    {content}
    +
  • + ); + })} +
+
+ ); +} + +export default SubnetHyperParams; diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/SubnetInfo/SubnetInfo.tsx b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetInfo/SubnetInfo.tsx new file mode 100644 index 000000000..c49140b86 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetInfo/SubnetInfo.tsx @@ -0,0 +1,127 @@ +import Display from 'src/components/containerGradient/Display/Display'; +import styles from '../../Subnet.module.scss'; +import { SubnetInfo, SubnetNeuron } from 'src/features/cybernet/types'; +import { Link, useParams } from 'react-router-dom'; +import { routes } from 'src/routes'; +import SubnetHyperParams from '../SubnetHyperParams/SubnetHyperParams'; +import useCybernetTexts from 'src/features/cybernet/ui/useCybernetTexts'; +import MusicalAddress from 'src/components/MusicalAddress/MusicalAddress'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; +import { useCurrentSubnet } from '../../subnet.context'; + +type Props = {}; + +const config: { [K in keyof SubnetInfo]: { text: string } } = { + blocks_since_last_step: { + text: 'Blocks since last step', + }, + burn: { + text: 'Burn', + }, + difficulty: { + text: 'Difficulty', + }, + emission_values: { + text: 'Emission Values', + }, + immunity_period: { + text: 'Immunity Period', + }, + kappa: { + text: 'Kappa', + }, + max_allowed_uids: { + text: 'Max Allowed UIDs', + }, + max_allowed_validators: { + text: 'Max Allowed Validators', + }, + max_weights_limit: { + text: 'Max Weights Limit', + }, + metadata: { + text: 'Metadata', + }, + min_allowed_weights: { + text: 'Min Allowed Weights', + }, + netuid: { + text: 'NetUID', + }, + network_modality: { + text: 'Network Modality', + }, + owner: { + text: 'Owner', + }, + rho: { + text: 'Rho', + }, + subnetwork_n: { + text: 'Subnetwork N', + }, + tempo: { + text: 'Tempo', + }, +}; + +function SubnetInfo({}: Props) { + const { + subnetQuery: { data: subnetInfoData }, + } = useCurrentSubnet(); + + const { getText } = useCybernetTexts(); + + useAdviserTexts({ + defaultText: `${getText('subnetwork')} params`, + }); + + return ( + <> + +
    + {subnetInfoData && + Object.keys(subnetInfoData).map((item) => { + const value = subnetInfoData[item]; + let content = value; + + if (item === 'owner') { + content = ( + + // {value} + ); + } + + if (item === 'metadata') { + content = ''; + + // content = ( + // + // // {value} + // ); + } + + if (['burn'].includes(item)) { + content = {value.toLocaleString()} 🟣; + } + + const title = config[item].text || item; + + return ( +
  • + + {title} + +
    {content}
    +
  • + ); + })} +
+
+ + + + ); +} + +export default SubnetInfo; diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/SubnetNeurons/SubnetMentorsActionBar/SubnetMentorsActionBar.tsx b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetNeurons/SubnetMentorsActionBar/SubnetMentorsActionBar.tsx new file mode 100644 index 000000000..00c3fafd6 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetNeurons/SubnetMentorsActionBar/SubnetMentorsActionBar.tsx @@ -0,0 +1,32 @@ +import { useSubnet } from '../../../subnet.context'; +import ActionBar from 'src/components/actionBar'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; + +function SubnetMentorsActionBar() { + const { + grades: { + newGrades: { save, isGradesUpdated, isLoading, blocksLeftToSetGrades }, + }, + } = useSubnet(); + + useAdviserTexts({ + defaultText: + blocksLeftToSetGrades && + `you have ${blocksLeftToSetGrades} blocks left to set grades`, + }); + + return ( + + ); +} + +export default SubnetMentorsActionBar; diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/SubnetNeurons/SubnetNeurons.module.scss b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetNeurons/SubnetNeurons.module.scss new file mode 100644 index 000000000..2f92dd7bd --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetNeurons/SubnetNeurons.module.scss @@ -0,0 +1,4 @@ +.neurons { + display: flex; + align-content: center; +} diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/SubnetNeurons/SubnetNeurons.tsx b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetNeurons/SubnetNeurons.tsx new file mode 100644 index 000000000..dca56320d --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetNeurons/SubnetNeurons.tsx @@ -0,0 +1,40 @@ +import SubnetNeuronsTable from './SubnetNeuronsTable/SubnetNeuronsTable'; +import { useCurrentSubnet, useSubnet } from '../../subnet.context'; +import Display from 'src/components/containerGradient/Display/Display'; +import useCybernetTexts from 'src/features/cybernet/ui/useCybernetTexts'; +import { useCurrentContract } from 'src/features/cybernet/ui/cybernet.context'; +import { checkIsMLVerse } from 'src/features/cybernet/ui/utils/verses'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; +import SubnetMentorsActionBar from './SubnetMentorsActionBar/SubnetMentorsActionBar'; + +type Props = { + addressRegisteredInSubnet: boolean; +}; + +function SubnetNeurons({ addressRegisteredInSubnet }: Props) { + const { isRootSubnet } = useCurrentSubnet(); + + const { getText } = useCybernetTexts(); + const { type } = useCurrentContract(); + + const isMLVerse = checkIsMLVerse(type); + + useAdviserTexts({ + defaultText: `${getText(isRootSubnet ? 'root' : 'subnetwork')} ${getText( + isRootSubnet ? 'rootValidator' : 'delegate', + true + )}`, + }); + + return ( + + + + {addressRegisteredInSubnet && !isRootSubnet && !isMLVerse && ( + + )} + + ); +} + +export default SubnetNeurons; diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/SubnetNeurons/SubnetNeuronsTable/SubnetNeuronsTable.tsx b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetNeurons/SubnetNeuronsTable/SubnetNeuronsTable.tsx new file mode 100644 index 000000000..70a7baab9 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetNeurons/SubnetNeuronsTable/SubnetNeuronsTable.tsx @@ -0,0 +1,438 @@ +/* eslint-disable react/no-unstable-nested-components */ +import { Link } from 'react-router-dom'; +import { Delegator, SubnetNeuron } from 'src/features/cybernet/types'; +import { cybernetRoutes } from '../../../../../routes'; +import Table from 'src/components/Table/Table'; +import { createColumnHelper } from '@tanstack/react-table'; +import { routes } from 'src/routes'; +import { Account, Tooltip } from 'src/components'; +import { useCurrentSubnet } from '../../../subnet.context'; +import useCurrentAddress from 'src/features/cybernet/_move/useCurrentAddress'; +import { useAppData } from 'src/contexts/appData'; +import GradeSetterInput from '../../../GradeSetterInput/GradeSetterInput'; +import { useEffect, useMemo } from 'react'; +import useCybernetTexts from 'src/features/cybernet/ui/useCybernetTexts'; +import { + useCurrentContract, + useCybernet, +} from 'src/features/cybernet/ui/cybernet.context'; +import { getColor } from '../../Weights/WeightsTable/WeightsTable'; +import colorStyles from '../../Weights/WeightsTable/temp.module.scss'; +import { checkIsMLVerse } from 'src/features/cybernet/ui/utils/verses'; +import IconsNumber from 'src/components/IconsNumber/IconsNumber'; +import AdviserHoverWrapper from 'src/features/adviser/AdviserHoverWrapper/AdviserHoverWrapper'; +import { tableIDs } from 'src/components/Table/tableIDs'; +import { useDelegates } from 'src/features/cybernet/ui/hooks/useDelegate'; +import { SubnetPreviewGroup } from 'src/features/cybernet/ui/components/SubnetPreview/SubnetPreview'; + +type Props = {}; + +enum TableColumnIDs { + uid = 'uid', + hotkey = 'hotkey', + stake = 'stake', + trust = 'trust', + lastRewards = 'lastRewards', + jobDone = 'jobDone', + grade = 'grade', + setGrade = 'setGrade', + registrations = 'registrations', +} + +const columnHelper = createColumnHelper(); + +// need refacfor +const key = 'subnetNeuronViewedBlock'; + +function getKey(address: string) { + return `${key}_${address}`; +} + +type LSType = { + // current address + [key: string]: { + // subnet + [key: string]: { + // neuron + [key: string]: number; + }; + }; +}; + +function getData(address: string) { + const data = localStorage.getItem(getKey(address)); + + return data ? (JSON.parse(data) as LSType) : null; +} + +function save(data: LSType, address: string) { + localStorage.setItem(getKey(address), JSON.stringify(data)); +} + +function handleSave( + neuron: string, + subnetId: number, + block: number, + currentAddress: string +) { + let data = getData(currentAddress); + + if (!data) { + data = {}; + } + + if (!data[currentAddress]) { + data[currentAddress] = {}; + } + + data[currentAddress] = { + ...data[currentAddress], + [subnetId]: { + ...data[currentAddress][subnetId], + [neuron]: block, + }, + }; + + save(data, currentAddress); +} + +function SubnetNeuronsTable({}: Props) { + const { + subnetQuery, + addressRegisteredInSubnet, + neuronsQuery, + + isRootSubnet, + netuid, + grades: { + all: { averageGrades }, + }, + } = useCurrentSubnet(); + + const { metadata } = subnetQuery?.data || {}; + + const address = useCurrentAddress(); + + const { block } = useAppData(); + + const { selectedContract } = useCybernet(); + + const { type } = useCurrentContract(); + const isMLVerse = checkIsMLVerse(type); + + const { getText } = useCybernetTexts(); + + const { data: delegatesData } = useDelegates(); + + const neurons = useMemo(() => { + return neuronsQuery.data || []; + }, [neuronsQuery.data]); + + const stakeByNeurons = useMemo(() => { + const stakes = neurons.reduce>((acc, neuron) => { + const { stake } = neuron; + const total = stake.reduce((acc, s) => acc + s[1], 0); + + acc[neuron.uid] = total; + + return acc; + }, {}); + + return stakes; + }, [neurons]); + + const registrationsByNeuron = useMemo(() => { + return delegatesData?.reduce< + Record + >((acc, delegator) => { + const { registrations, delegate } = delegator; + + acc[delegate] = registrations; + + return acc; + }, {}); + }, [delegatesData]); + + const viewedBlocks = getData(address); + const cur = viewedBlocks?.[address]?.[netuid]; + + const columns = useMemo(() => { + const cols = [ + columnHelper.accessor('uid', { + header: getText('uid'), + id: TableColumnIDs.uid, + cell: (info) => { + const uid = info.getValue(); + return uid; + }, + }), + columnHelper.accessor('hotkey', { + header: getText(isRootSubnet ? 'rootValidator' : 'delegate'), + id: TableColumnIDs.hotkey, + size: 200, + enableSorting: false, + cell: (info) => { + const hotkey = info.getValue(); + const { validator_permit: validatorPermit } = info.row.original; + + const isProfessor = !!validatorPermit; + + return ( +
+ + + {isProfessor && ( + + 💼 + + )} +
+ ); + }, + }), + + columnHelper.accessor('uid', { + header: 'teach power', + id: TableColumnIDs.stake, + sortingFn: (rowA, rowB) => { + const a = stakeByNeurons[rowA.original.uid]; + const b = stakeByNeurons[rowB.original.uid]; + + return a - b; + }, + cell: (info) => { + const uid = info.getValue(); + const total = stakeByNeurons[uid]; + + return ; + }, + }), + ]; + + if (!isRootSubnet) { + cols.push( + // @ts-ignore + columnHelper.accessor('hotkey', { + header: 'job done', + id: TableColumnIDs.jobDone, + enableSorting: false, + cell: (info) => { + const hotkey = info.getValue(); + + if (!metadata) { + return null; + } + + const viewedBlock = cur?.[hotkey]; + + return ( + <> + { + if (!block) { + return; + } + + handleSave(hotkey, netuid, +block, address); + }} + to={ + routes.oracle.ask.getLink(metadata.particle) + + `?neuron=${hotkey}&subnet=${netuid}` + } + > + + 🔍 + + + +
+ {viewedBlock && block && ( + + (viewed {block - viewedBlock} blocks ago) + + )} + + ); + }, + }), + + columnHelper.accessor('validator_trust', { + header: 'trust', + id: TableColumnIDs.trust, + cell: (info) => { + const validatorTrust = info.getValue(); + const formatted = ((validatorTrust / 65536) * 100).toFixed(2); + + return `${formatted}%`; + }, + }), + columnHelper.accessor('emission', { + header: 'last rewards', + id: TableColumnIDs.lastRewards, + cell: (info) => { + const emission = info.getValue(); + return ; + }, + }), + + columnHelper.accessor('uid', { + header: 'grade', + id: TableColumnIDs.grade, + sortingFn: (rowA, rowB) => { + const a = averageGrades[rowA.original.uid]; + const b = averageGrades[rowB.original.uid]; + + return a - b; + }, + cell: (info) => { + const uid = info.getValue(); + + // if (!allGrades) { + // return null; + // } + + const avg = averageGrades[uid]; + + const color = getColor(avg); + + return {avg}; + }, + }) + ); + + if (addressRegisteredInSubnet && !isMLVerse) { + cols.push( + // @ts-ignore + columnHelper.accessor('uid', { + header: 'Set grade', + id: TableColumnIDs.setGrade, + enableSorting: false, + cell: (info) => { + const uid = info.getValue(); + + return ; + }, + }) + ); + } + } else { + // eslint-disable-next-line no-lonely-if + if (registrationsByNeuron) { + cols.push( + // @ts-ignore + columnHelper.accessor('hotkey', { + header: 'Registrations', + id: TableColumnIDs.registrations, + sortingFn: (rowA, rowB) => { + const a = registrationsByNeuron[rowA.original.hotkey].length; + const b = registrationsByNeuron[rowB.original.hotkey].length; + + return a - b; + }, + cell: (info) => { + const hotkey = info.getValue(); + const registrations = registrationsByNeuron[hotkey]; + + const subnetsWithoutRoot = registrations.filter((r) => r !== 0); + + return ( +
+ {subnetsWithoutRoot.length > 0 ? ( + + ) : ( + '-' + )} +
+ ); + }, + }) + ); + } + } + + return cols; + }, [ + averageGrades, + isMLVerse, + stakeByNeurons, + getText, + registrationsByNeuron, + selectedContract, + metadata, + netuid, + addressRegisteredInSubnet, + isRootSubnet, + address, + // block, + // cur, + ]); + + let order; + if (isRootSubnet) { + order = [ + TableColumnIDs.uid, + TableColumnIDs.hotkey, + TableColumnIDs.registrations, + TableColumnIDs.stake, + ]; + } else { + order = [ + TableColumnIDs.uid, + TableColumnIDs.hotkey, + TableColumnIDs.jobDone, + TableColumnIDs.stake, + TableColumnIDs.lastRewards, + TableColumnIDs.trust, + TableColumnIDs.grade, + TableColumnIDs.setGrade, + ]; + } + + return ( +
+ ); +} + +export default SubnetNeuronsTable; diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/SubnetSubnets/SubnetSubnets.tsx b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetSubnets/SubnetSubnets.tsx new file mode 100644 index 000000000..3b31bec15 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/SubnetSubnets/SubnetSubnets.tsx @@ -0,0 +1,43 @@ +import { ActionBar } from 'src/components'; +import Display from 'src/components/containerGradient/Display/Display'; +import { SubnetInfo } from 'src/features/cybernet/types'; +import useQueryCybernetContract from 'src/features/cybernet/ui/useQueryCybernetContract.refactor'; +import SubnetsTable from '../../../Subnets/SubnetsTable/SubnetsTable'; +import { useSubnet } from '../../subnet.context'; +import DisplayTitle from 'src/components/containerGradient/DisplayTitle/DisplayTitle'; +import { isEqual } from 'lodash'; + +function SubnetSubnets({ addressRegisteredInSubnet }) { + const { data, loading, error } = useQueryCybernetContract({ + query: { + get_subnets_info: {}, + }, + }); + + const subnetsWithoutRoot = data?.filter((subnet) => subnet.netuid !== 0); + + const { + grades: { + newGrades: { save, data: newGrades, isGradesUpdated }, + }, + } = useSubnet(); + + return ( + + + + + + ); +} + +export default SubnetSubnets; diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/Weights/Weights.tsx b/src/features/cybernet/ui/pages/Subnet/tabs/Weights/Weights.tsx new file mode 100644 index 000000000..ef4acc7aa --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/Weights/Weights.tsx @@ -0,0 +1,24 @@ +import WeightsTable from './WeightsTable/WeightsTable'; +import DisplayTitle from 'src/components/containerGradient/DisplayTitle/DisplayTitle'; +import Display from 'src/components/containerGradient/Display/Display'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; + +type Props = {}; + +function Weights({}: Props) { + useAdviserTexts({ + defaultText: 'Subnet weights', + }); + + return ( +
+ + + + +
+
+ ); +} + +export default Weights; diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsSetter/WeightsSetter.module.scss b/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsSetter/WeightsSetter.module.scss new file mode 100644 index 000000000..b45573ed5 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsSetter/WeightsSetter.module.scss @@ -0,0 +1,17 @@ +.wrapper { + // position: absolute; + // left: 700px; + // top: 60px; +} + +.group { + display: grid; + gap: 24px; + margin-top: 20px; + + > span { + color: gray; + } + + max-width: 150px; +} diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsSetter/WeightsSetter.tsx b/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsSetter/WeightsSetter.tsx new file mode 100644 index 000000000..690779dcc --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsSetter/WeightsSetter.tsx @@ -0,0 +1,96 @@ +import { useEffect, useRef, useState } from 'react'; +import { ActionBar, InputNumber } from 'src/components'; +import styles from './WeightsSetter.module.scss'; +import { useAdviser } from 'src/features/adviser/context'; +import useExecuteCybernetContract from '../../../../../useExecuteCybernetContract'; +import { usePreviousPage } from 'src/contexts/previousPage'; +import { sessionStorageKeys } from 'src/constants/sessionStorageKeys'; +import { useSubnet } from '../../../subnet.context'; + +const DEFAULT_WEIGHT = 5; + +const sessionStorageKey = sessionStorageKeys.subnetWeights; + +type Props = { + callback: () => void; +}; + +function getSSData() { + const data = sessionStorage.getItem(sessionStorageKey); + + return data ? JSON.parse(data) : null; +} + +function WeightsSetter({ callback, weights: w }: Props) { + const { subnetQuery, neuronsQuery } = useSubnet(); + + const w2 = w?.reduce((acc, [uid, value], i) => { + acc[uid] = value; + return acc; + }, {}); + + const { + max_weights_limit: maxWeightsLimit, + subnetwork_n: length, + netuid, + } = subnetQuery.data!; + + const neurons = neuronsQuery.data || []; + + const ssData = getSSData()?.[netuid]; + + const [weights, setWeights] = useState( + // ssData || + new Array(length) + .fill(DEFAULT_WEIGHT) + .map((_, i) => (w2?.[i] ? (w2[i] / maxWeightsLimit) * 10 : 0).toFixed()) + ); + + useEffect(() => { + return () => { + sessionStorage.setItem( + sessionStorageKey, + JSON.stringify({ + [netuid]: weights, + }) + ); + }; + }, [netuid, weights]); + + return ( +
+
+ change grades + {new Array(length).fill(null).map((_, i) => { + const { hotkey } = neurons[i]; + + const value = weights[i]; + return ( +
+ { + const newWeights = [...weights]; + newWeights[i] = +e; + setWeights(newWeights); + }} + /> +
+ ); + })} +
+ + +
+ ); +} + +export default WeightsSetter; diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsTable/WeightsTable.module.scss b/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsTable/WeightsTable.module.scss new file mode 100644 index 000000000..b82d92197 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsTable/WeightsTable.module.scss @@ -0,0 +1,21 @@ +.wrapper { + display: flex; + + table:nth-of-type(1) { + tr { + &:hover { + background: transparent; + } + } + } + + table th { + height: 85px; + } +} + +.headerCell { + position: relative; + display: flex; + justify-content: center; +} diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsTable/WeightsTable.tsx b/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsTable/WeightsTable.tsx new file mode 100644 index 000000000..93fee7ab6 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsTable/WeightsTable.tsx @@ -0,0 +1,168 @@ +/* eslint-disable react/no-unstable-nested-components */ +import { createColumnHelper } from '@tanstack/react-table'; +import Table from 'src/components/Table/Table'; +import { Link } from 'react-router-dom'; +import { SubnetInfo, SubnetNeuron } from 'src/features/cybernet/types'; +import { cybernetRoutes } from '../../../../../routes'; +import styles from './WeightsTable.module.scss'; +import { Account } from 'src/components'; +import useCurrentAddress from 'src/features/cybernet/_move/useCurrentAddress'; +import useQueryCybernetContract from 'src/features/cybernet/ui/useQueryCybernetContract.refactor'; +import { useSubnet } from '../../../subnet.context'; +import { useMemo } from 'react'; +import { + useCurrentContract, + useCybernet, +} from 'src/features/cybernet/ui/cybernet.context'; +import colorStyles from './temp.module.scss'; +import SubnetPreview from 'src/features/cybernet/ui/components/SubnetPreview/SubnetPreview'; + +type Props = {}; + +export function getColor(value) { + let color; + + if (value < 3) { + color = 'red'; + } else if (value < 6) { + color = 'orange'; + } else { + color = 'green'; + } + + return color; +} + +const columnHelper = createColumnHelper(); + +function WeightsTable({}: Props) { + const address = useCurrentAddress(); + + const { subnetQuery, grades, neuronsQuery } = useSubnet(); + + const { subnetsQuery } = useCybernet(); + + const uid = subnetQuery.data?.netuid; + const isRootSubnet = uid === 0; + + const neurons = useMemo(() => { + return neuronsQuery.data || []; + }, [neuronsQuery.data]); + + const currentContract = useCurrentContract(); + + if (!neurons.length) { + return null; + } + + const columns = isRootSubnet + ? subnetsQuery.data + ?.map((subnet) => subnet.netuid) + .filter((subnet) => subnet !== 0) + : neurons.map((neuron) => neuron.uid); + + const data = grades.all.data; + + if (!columns?.length) { + return null; + } + + return ( +
+
+
{ + const uid = info.getValue(); + + return ( + + ); + }, + }), + ]} + /> + +
+ columns.map((uid) => { + return columnHelper.accessor(String(uid), { + id: `t${uid}`, + header: () => { + if (isRootSubnet) { + return ; + } + + const hotkey = neurons[uid].hotkey; + + return ( +
+ {address === hotkey && ( + + 🔑 + + )} + +
+ ); + }, + cell: (info) => { + const val = info.row.original[uid]; + + if (!val) { + return '-'; + } + + const color = getColor(val); + + return ( +
{val}
+ ); + }, + }); + }) + // [columns, address, isRootSubnet, neurons, currentContract] + // ) + } + data={data} + /> + + + ); +} + +export default WeightsTable; diff --git a/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsTable/temp.module.scss b/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsTable/temp.module.scss new file mode 100644 index 000000000..28a964523 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/tabs/Weights/WeightsTable/temp.module.scss @@ -0,0 +1,11 @@ +.color_ { + &red { + color: var(--red); + } + &green { + color: var(--green-light); + } + &orange { + color: orange; + } +} diff --git a/src/features/cybernet/ui/pages/Subnet/useCurrentSubnetGrades.tsx b/src/features/cybernet/ui/pages/Subnet/useCurrentSubnetGrades.tsx new file mode 100644 index 000000000..eb76c6366 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnet/useCurrentSubnetGrades.tsx @@ -0,0 +1,325 @@ +import { useCallback, useEffect, useState, useMemo } from 'react'; +import { + formatGradeToWeight, + formatWeightToGrade, +} from '../../utils/formatWeight'; +import useExecuteCybernetContract from '../../useExecuteCybernetContract'; +import { useAdviser } from 'src/features/adviser/context'; +import { isEqual } from 'lodash'; +import { useAppData } from '../../../../../contexts/appData'; +import useCurrentAddress from 'src/features/cybernet/_move/useCurrentAddress'; +import useCybernetContract from '../../useQueryCybernetContract.refactor'; +import { SubnetNeuron, Weights } from '../../../types'; + +const LS_KEY = 'setGrades'; +const LS_KEY2 = 'gradesUpdated'; + +function getLSKey(address: string) { + return `${LS_KEY}_${address}`; +} + +function getLSKe2(address: string) { + return `${LS_KEY2}_${address}`; +} + +function getLSData(address: string, subnetId: number) { + const data = sessionStorage.getItem(getLSKey(address)); + + const parsedData = data ? JSON.parse(data) : null; + + return parsedData ? parsedData[subnetId] : null; +} + +function getLSData2(address: string, subnetId: number) { + const data = localStorage.getItem(getLSKe2(address)); + + const parsedData = data ? JSON.parse(data) : null; + + return parsedData ? parsedData[subnetId] : null; +} + +function saveLSData2(block, address: string, subnetId: number) { + const newData = { + ...getLSData2(address, subnetId), + [subnetId]: block, + }; + + localStorage.setItem(getLSKe2(address), JSON.stringify(newData)); +} + +function saveLSData(data, address: string, subnetId: number) { + const newData = { + ...getLSData(address, subnetId), + [subnetId]: data, + }; + + sessionStorage.setItem(getLSKey(address), JSON.stringify(newData)); +} + +// const GradesContext = React.createContext<{ +// all: ReturnType>; +// my: { +// fromMe: { +// [uid: string]: number; +// } | null; +// toMe: { +// [uid: string]: number; +// } | null; +// }; +// newGrades: { +// data: { +// [uid: string]: number; +// }; +// setGrade: (uid: string, grade: number) => void; +// blocksLeftToSetGrades: number; +// } | null; +// }>({ +// all: null, +// my: null, +// newGrades: { +// data: null, +// setGrade: null, +// }, +// }); + +type Props = { + netuid: number; + neuronsQuery: ReturnType>; + hyperparamsQuery: any; +}; +/* + @deprecated +*/ +function useCurrentSubnetGrades({ + netuid, + neuronsQuery, + hyperparamsQuery, +}: Props) { + const currentAddress = useCurrentAddress(); + + const getLastGrades = useCallback(() => { + if (!currentAddress || !netuid) { + return null; + } + + const lastGrades = getLSData(currentAddress, netuid); + return lastGrades; + }, [currentAddress, netuid]); + + const { block } = useAppData(); + + const myUid = neuronsQuery.data?.find( + (n) => n.hotkey === currentAddress + )?.uid; + + const weightsQuery = useCybernetContract({ + query: { + get_weights_sparse: { + netuid, + }, + }, + }); + + useEffect(() => { + if (!currentAddress) { + return; + } + + const lastGrades = getLastGrades(); + + if (lastGrades) { + setNewGrades(lastGrades); + } + }, [currentAddress, netuid, getLastGrades]); + + const [newGrades, setNewGrades] = useState<{ + [uid: string]: number; + }>({}); + + const weightsRateLimit = hyperparamsQuery.data?.weights_rate_limit; + + const gradesSetBlockNumber = getLSData2(currentAddress, netuid); + + let blocksLeftToSetGrades = 0; + if (gradesSetBlockNumber && weightsRateLimit && block) { + const diff = block - gradesSetBlockNumber; + const t = weightsRateLimit - diff; + + blocksLeftToSetGrades = t > 0 ? t : 0; + } + + const { gradesFromNeurons, gradesToNeurons } = useMemo(() => { + type Item = { + [uid: string]: number; + }; + + const gradesToNeurons = Array.from( + { length: weightsQuery.data?.length || 0 }, + () => ({}) + ); + const gradesFromNeurons = Array.from( + { length: weightsQuery.data?.length || 0 }, + () => ({}) + ); + + weightsQuery.data?.forEach((weightsFromNeuron, index) => { + if (!weightsFromNeuron.length) { + return; + } + + weightsFromNeuron.forEach(([uid, weight]) => { + const grade = formatWeightToGrade(weight, 65535); + + gradesFromNeurons[index][uid] = grade; + gradesToNeurons[uid][index] = grade; + }); + }); + + return { + gradesFromNeurons, + gradesToNeurons, + }; + }, [weightsQuery.data]); + + const gradesFromMe = useMemo(() => { + const lastGrades = getLastGrades(); + + return lastGrades || gradesFromNeurons?.[myUid] || {}; + }, [gradesFromNeurons, myUid, getLastGrades]); + + useEffect(() => { + setNewGrades(gradesFromMe); + }, [gradesFromMe]); + + const setGrade = useCallback((uid: string, grade: number) => { + setNewGrades((prev) => ({ + ...prev, + [uid]: grade, + })); + }, []); + + useEffect(() => { + if (!currentAddress || !netuid) { + return; + } + + if (Object.keys(newGrades).length === 0) { + return; + } + + const lastData = getLSData(currentAddress, netuid); + + if (isEqual(newGrades, lastData)) { + return; + } + + saveLSData(newGrades, currentAddress, netuid); + }, [newGrades, currentAddress, netuid]); + + const { setAdviser } = useAdviser(); + + const { mutate: submit, isLoading } = useExecuteCybernetContract({ + query: { + set_weights: { + dests: + // data?.length && + // new Array(data?.length - 1).fill(0).map((_, i) => i + 1), + Object.keys(newGrades) + .sort((a, b) => +a - +b) + .map((uid) => +uid), + netuid, + weights: Object.values(newGrades).map((grade) => { + const weight = formatGradeToWeight(grade, 65535); + return weight; + }), + version_key: 0, + }, + }, + onSuccess: () => { + setAdviser('Weights set', 'green'); + weightsQuery.refetch(); + saveLSData2(block, currentAddress, netuid); + }, + }); + + const averageGrades = useMemo(() => { + if (!neuronsQuery.data) { + return {}; + } + + const professorsCount = neuronsQuery.data.reduce((acc, item) => { + return acc + Number(item.validator_permit); + }, 0); + + console.log('professorsCount', professorsCount); + + return gradesToNeurons.reduce((acc, item, index) => { + const { total, count } = Object.entries(item).reduce( + (acc, [uid, grade]) => { + const isProfessor = neuronsQuery.data[uid]?.validator_permit; + + if (isProfessor || netuid === 0) { + acc.total += grade; + acc.count++; + } + + return acc; + }, + { total: 0, count: 0 } + ); + + const avg = Number((total / professorsCount).toFixed(2)) || 0; + + // if (Number.isNaN(avg)) { + // debugger; + // } + acc[index] = avg; + + return acc; + }, {}); + }, [gradesToNeurons, neuronsQuery.data, netuid]); + + console.log('weigths', weightsQuery.data); + console.log('gradesFromNeurons', gradesFromNeurons); + console.log('gradesToNeurons', gradesToNeurons); + console.log('averageGrades', averageGrades); + + const value = useMemo(() => { + return { + all: { + ...weightsQuery, + data: gradesFromNeurons, + gradesToNeurons, + averageGrades, + }, + my: { + fromMe: gradesFromMe, + toMe: null, + }, + refetch: weightsQuery.refetch, + newGrades: { + data: newGrades, + setGrade, + save: submit, + isLoading, + blocksLeftToSetGrades, + isGradesUpdated: !isEqual(newGrades, gradesFromMe), + }, + }; + }, [ + weightsQuery, + averageGrades, + gradesFromNeurons, + gradesToNeurons, + gradesFromMe, + newGrades, + setGrade, + submit, + isLoading, + blocksLeftToSetGrades, + ]); + + return value; +} + +export default useCurrentSubnetGrades; diff --git a/src/features/cybernet/ui/pages/Subnets/Subnets.module.scss b/src/features/cybernet/ui/pages/Subnets/Subnets.module.scss new file mode 100644 index 000000000..5d21daec4 --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnets/Subnets.module.scss @@ -0,0 +1,4 @@ +.header { + display: flex; + gap: 0 10px; +} diff --git a/src/features/cybernet/ui/pages/Subnets/Subnets.tsx b/src/features/cybernet/ui/pages/Subnets/Subnets.tsx new file mode 100644 index 000000000..5f3c9a6ed --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnets/Subnets.tsx @@ -0,0 +1,76 @@ +import { Loading } from 'src/components'; +import Display from 'src/components/containerGradient/Display/Display'; +import DisplayTitle from 'src/components/containerGradient/DisplayTitle/DisplayTitle'; +import SubnetsTable from './SubnetsTable/SubnetsTable'; +import styles from './Subnets.module.scss'; +import { Helmet } from 'react-helmet'; +import useCybernetTexts from '../../useCybernetTexts'; +import { useCybernet } from '../../cybernet.context'; +import useAdviserTexts from 'src/features/adviser/useAdviserTexts'; +import AdviserHoverWrapper from 'src/features/adviser/AdviserHoverWrapper/AdviserHoverWrapper'; + +function Subnets() { + const { + subnetsQuery: { data, loading, error }, + } = useCybernet(); + + // possible to refactor to 1 loop + const rootSubnet = data?.find((subnet) => subnet.netuid === 0); + const graphSubnets = data?.filter((subnet) => subnet.network_modality === 0); + + const { getText } = useCybernetTexts(); + + useAdviserTexts({ + isLoading: loading, + error, + defaultText: 'explore the full list of faculties', + }); + return ( + <> + + {getText('subnetwork', true)} | cyb + + {loading && } + + {rootSubnet && ( + + {/* */} + {getText('root')} + {/* */} + + } + /> + } + > + + + )} + + {!!graphSubnets?.length && ( + + {/* */} + {getText('subnetwork', true)} + {/* */} + + } + /> + } + > + + + )} + + ); +} + +export default Subnets; diff --git a/src/features/cybernet/ui/pages/Subnets/SubnetsTable/SubnetsTable.tsx b/src/features/cybernet/ui/pages/Subnets/SubnetsTable/SubnetsTable.tsx new file mode 100644 index 000000000..c95f94a4a --- /dev/null +++ b/src/features/cybernet/ui/pages/Subnets/SubnetsTable/SubnetsTable.tsx @@ -0,0 +1,267 @@ +/* eslint-disable react/no-unstable-nested-components */ +import { useMemo } from 'react'; +import { SubnetInfo } from '../../../../types'; +import { createColumnHelper } from '@tanstack/react-table'; +import Table from 'src/components/Table/Table'; +import { useNavigate } from 'react-router-dom'; +import { routes } from 'src/routes'; + +import { Account, Cid, Tooltip } from 'src/components'; + +import useDelegate from '../../../hooks/useDelegate'; +import useCurrentAddress from 'src/features/cybernet/_move/useCurrentAddress'; +import GradeSetterInput from '../../Subnet/GradeSetterInput/GradeSetterInput'; +import { useSubnet } from '../../Subnet/subnet.context'; +import { routes as subnetRoutes } from '../../../routes'; +import useCybernetTexts from '../../../useCybernetTexts'; +import { useCurrentContract, useCybernet } from '../../../cybernet.context'; +import SubnetPreview from '../../../components/SubnetPreview/SubnetPreview'; +import CIDResolver from 'src/components/CIDResolver/CIDResolver'; +import { trimString } from 'src/utils/utils'; +import { tableIDs } from 'src/components/Table/tableIDs'; + +type Props = { + data: SubnetInfo[]; +}; + +const columnHelper = createColumnHelper(); + +// don't know good name +function CurrentToMax({ + value, + maxValue, +}: { + value: number; + maxValue: number; +}) { + return ( +
+ {value}{' '} + + / {maxValue} + +
+ ); +} + +function SubnetsTable({ data }: Props) { + const navigate = useNavigate(); + + const address = useCurrentAddress(); + + const { grades, subnetQuery } = useSubnet(); + + // debug + const { averageGrades } = grades?.all || {}; + + const { getText } = useCybernetTexts(); + + const rootSubnet = subnetQuery?.data?.netuid === 0; + + const { data: d2 } = useDelegate(address); + const myCurrentSubnetsJoined = d2?.registrations; + + const myAddressJoinedRootSubnet = myCurrentSubnetsJoined?.includes(0); + + const { selectedContract } = useCybernet(); + + const { contractName, network } = useCurrentContract(); + + const columns = useMemo(() => { + const col = [ + columnHelper.accessor('metadata', { + header: 'name', + id: 'subnetName', + cell: (info) => { + const value = info.getValue(); + + const { netuid } = info.row.original; + + const isMySubnet = myCurrentSubnetsJoined?.includes(netuid); + + return ( +
+ + {isMySubnet && ( + + ✅ + + )} +
+ ); + }, + }), + + columnHelper.accessor('owner', { + header: getText('subnetOwner'), + enableSorting: false, + cell: (info) => { + const value = info.getValue(); + + return ( + + ); + }, + }), + + columnHelper.accessor('metadata.particle', { + header: 'teaser', + id: 'teaser', + size: 150, + enableSorting: false, + cell: (info) => { + const cid = info.getValue(); + + return ; + }, + }), + + columnHelper.accessor('metadata.description', { + header: 'rules', + id: 'rules', + enableSorting: false, + cell: (info) => { + const cid = info.getValue(); + + return {trimString(cid, 3, 3)}; + }, + }), + ]; + + if (!rootSubnet) { + col.push( + // @ts-ignore + columnHelper.accessor('max_allowed_validators', { + header: getText('validator', true), + sortingFn: (rowA, rowB) => { + const a = rowA.original.subnetwork_n; + const b = rowB.original.subnetwork_n; + + return a - b; + }, + cell: (info) => { + const max = info.getValue(); + + const current = info.row.original.subnetwork_n; + + return ( + = max ? max : current} + maxValue={max} + /> + ); + }, + }), + columnHelper.accessor('max_allowed_uids', { + header: getText('miner', true), + sortingFn: (rowA, rowB) => { + const a = rowA.original.subnetwork_n; + const b = rowB.original.subnetwork_n; + + return a - b; + }, + cell: (info) => { + const max = info.getValue(); + + const current = info.row.original.subnetwork_n; + const maxAllowedValidators = + info.row.original.max_allowed_validators; + + const diff = current - maxAllowedValidators; + + return ( + = 0 ? diff : 0} + maxValue={max - maxAllowedValidators} + /> + ); + }, + }) + ); + } + + if (rootSubnet) { + col.push( + // @ts-ignore + columnHelper.accessor('netuid', { + header: 'Grade (average)', + id: 'grade', + sortingFn: (rowA, rowB) => { + const a = averageGrades[rowA.original.netuid]; + const b = averageGrades[rowB.original.netuid]; + + return a - b; + }, + cell: (info) => { + const uid = info.getValue(); + + // // fix + // if (!grades.all?.data) { + // return 0; + // } + + const v = averageGrades[uid]; + + return v; + }, + }) + ); + + if (myAddressJoinedRootSubnet) { + col.push( + // @ts-ignore + columnHelper.accessor('netuid', { + header: 'Set grade', + id: 'setGrade', + enableSorting: false, + cell: (info) => { + const uid = info.getValue(); + return ; + }, + }) + ); + } + } + + return col; + }, [ + myCurrentSubnetsJoined, + myAddressJoinedRootSubnet, + averageGrades, + rootSubnet, + getText, + ]); + + return ( +
{ + const { netuid } = data[row]; + + navigate(subnetRoutes.subnet.getLink('pussy', contractName, netuid)); + }} + columns={columns} + data={data} + // if 1 - root subnet + enableSorting={data.length !== 1} + /> + ); +} + +export default SubnetsTable; diff --git a/src/features/cybernet/ui/pages/Verse/Verse.tsx b/src/features/cybernet/ui/pages/Verse/Verse.tsx new file mode 100644 index 000000000..92b1c6641 --- /dev/null +++ b/src/features/cybernet/ui/pages/Verse/Verse.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; +import { useCybernet } from '../../cybernet.context'; +import Display from 'src/components/containerGradient/Display/Display'; +import { MainContainer } from 'src/components'; +import { AvataImgIpfs } from 'src/containers/portal/components/avataIpfs'; + +function Verse() { + const { selectedContract } = useCybernet(); + return ( + <> + {/*
+ +

{JSON.stringify(selectedContract?.metadata)}

+ +
+
*/} + + + ); +} + +export default Verse; diff --git a/src/features/cybernet/ui/pages/Verses/Verses.tsx b/src/features/cybernet/ui/pages/Verses/Verses.tsx new file mode 100644 index 000000000..2d8bea5bf --- /dev/null +++ b/src/features/cybernet/ui/pages/Verses/Verses.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import ContractTable from 'src/containers/wasm/contract/ContractTable'; +import ContractsTable from '../Main/ContractsTable/ContractsTable'; +import Display from 'src/components/containerGradient/Display/Display'; +import { MainContainer } from 'src/components'; +import DisplayTitle from 'src/components/containerGradient/DisplayTitle/DisplayTitle'; + +function Verses() { + return ( + } noPaddingX> + + + ); +} + +export default Verses; diff --git a/src/features/cybernet/ui/routes.ts b/src/features/cybernet/ui/routes.ts new file mode 100644 index 000000000..429c36aa7 --- /dev/null +++ b/src/features/cybernet/ui/routes.ts @@ -0,0 +1,58 @@ +const root = '/cyberver'; + +export const routes = { + verses: { + path: `${root}/verses`, + getLink: () => `${root}/verses`, + }, + verseNetwork: { + path: `${root}/verses/:network`, + getLink: (network: string) => `${root}/verse/${network}`, + }, + verse: { + path: `${root}/verses/:network/:nameOrAddress`, + getLink: (network: string, nameOrAddress: string) => + `${root}/verses/${network}/${nameOrAddress}`, + }, + subnets: { + path: `${root}/verses/:network/:nameOrAddress/faculties`, + getLink: (network: string, nameOrAddress: string) => + `${root}/verses/${network}/${nameOrAddress}/faculties`, + }, + subnet: { + path: `${root}/verses/:network/:nameOrAddress/faculties/:nameOrUid`, + getLink: ( + network: string, + nameOrAddress: string, + nameOrUid: string | number + ) => `${root}/verses/${network}/${nameOrAddress}/faculties/${nameOrUid}`, + }, + delegators: { + path: `${root}/verses/:network/:nameOrAddress/mentors`, + getLink: (network: string, nameOrAddress: string) => + `${root}/verses/${network}/${nameOrAddress}/mentors`, + }, + delegator: { + path: `${root}/verses/:network/:nameOrAddress/faculties/:nameOrUid/mentors/:address`, + getLink: (network: string, nameOrAddress: string, address: string) => + `${root}/verses/${network}/${nameOrAddress}/mentors/${address}`, + }, + myMentor: { + path: `${root}/verses/:network/:nameOrAddress/mentors/my`, + getLink: (network: string, nameOrAddress: string) => + `${root}/verses/${network}/${nameOrAddress}/mentors/my`, + }, + myLearner: { + path: `${root}/verses/:network/:nameOrAddress/learners/my`, + getLink: (network: string, nameOrAddress: string) => + `${root}/verses/${network}/${nameOrAddress}/learners/my`, + }, + sigma: { + path: `${root}/sigma`, + getLink: () => `${root}/sigma`, + }, +}; + +routes.delegate = routes.delegator; + +export const cybernetRoutes = routes; diff --git a/src/features/cybernet/ui/useCybernetTexts.ts b/src/features/cybernet/ui/useCybernetTexts.ts new file mode 100644 index 000000000..97285fdd4 --- /dev/null +++ b/src/features/cybernet/ui/useCybernetTexts.ts @@ -0,0 +1,37 @@ +import { useCybernet } from './cybernet.context'; +import { texts, Texts } from './cybernetTexts'; +import { useCallback } from 'react'; + +function useCybernetTexts() { + const { selectedContract } = useCybernet(); + + const type = selectedContract?.type; + + const getText = useCallback( + (key: Texts, isPlural?: boolean) => { + const t = type === 'graph' ? 'graph' : 'default'; + const t2 = texts[key][t]; + + let text: string; + + // refactor + if (typeof t2 === 'object') { + text = isPlural ? t2.plural || t2 + 's' : t2.single || t2; + } else { + text = t2; + if (isPlural) { + text += 's'; + } + } + + return text; + }, + [type] + ); + + return { + getText, + }; +} + +export default useCybernetTexts; diff --git a/src/features/cybernet/ui/useExecuteCybernetContract.ts b/src/features/cybernet/ui/useExecuteCybernetContract.ts new file mode 100644 index 000000000..edec5939d --- /dev/null +++ b/src/features/cybernet/ui/useExecuteCybernetContract.ts @@ -0,0 +1,25 @@ +import useExecuteContractWithWaitAndAdviser, { + Props as ExecuteContractProps, +} from '../../../hooks/contract/useExecuteContractWithWaitAndAdviser'; +import { useCybernet } from './cybernet.context'; + +type Props = Omit; + +function useExecuteCybernetContract({ + query, + funds, + onSuccess, + successMessage, +}: Props) { + const { selectedContract } = useCybernet(); + + return useExecuteContractWithWaitAndAdviser({ + contractAddress: selectedContract.address, + query, + funds, + onSuccess, + successMessage, + }); +} + +export default useExecuteCybernetContract; diff --git a/src/features/cybernet/ui/useQueryCybernetContract.refactor.ts b/src/features/cybernet/ui/useQueryCybernetContract.refactor.ts new file mode 100644 index 000000000..7e4a30970 --- /dev/null +++ b/src/features/cybernet/ui/useQueryCybernetContract.refactor.ts @@ -0,0 +1,53 @@ +import { useQueryClient } from 'src/contexts/queryClient'; + +import { UseQueryOptions, useQuery } from '@tanstack/react-query'; +import { CybernetContractQuery, queryCybernetContract } from '../api'; +import { useCybernet } from './cybernet.context'; + +type Props = { + query: CybernetContractQuery; + contractAddress?: string; + + // combine to 1 object prop + skip?: boolean; + refetchInterval?: UseQueryOptions['refetchInterval']; +}; + +// TODO: copied from usePassportContract, reuse core logic + +function useQueryCybernetContract({ + query, + contractAddress, + skip, + refetchInterval, +}: Props) { + const queryClient = useQueryClient(); + + const { selectedContract } = useCybernet(); + const contractAddr = contractAddress || selectedContract?.address; + + const { refetch, data, error, isLoading } = useQuery< + unknown, + unknown, + DataType + >( + ['queryCybernetContract', contractAddr, query], + () => { + return queryCybernetContract(contractAddr, query, queryClient!); + }, + { + // @ts-ignore + refetchInterval, + enabled: !skip && Boolean(queryClient && contractAddr), + } + ); + + return { + data, + loading: isLoading, + error, + refetch, + }; +} + +export default useQueryCybernetContract; diff --git a/src/features/cybernet/ui/utils/formatWeight.ts b/src/features/cybernet/ui/utils/formatWeight.ts new file mode 100644 index 000000000..4b5dfb5a7 --- /dev/null +++ b/src/features/cybernet/ui/utils/formatWeight.ts @@ -0,0 +1,13 @@ +export function formatWeightToGrade( + value: number, + maxWeightsLimit: number +): number { + return parseFloat(((value / maxWeightsLimit) * 10).toFixed(0)); +} + +export function formatGradeToWeight( + value: number, + maxWeightsLimit: number +): number { + return parseFloat(((value / 10) * maxWeightsLimit).toFixed(0)); +} diff --git a/src/features/cybernet/ui/utils/verses.ts b/src/features/cybernet/ui/utils/verses.ts new file mode 100644 index 000000000..9c94793a3 --- /dev/null +++ b/src/features/cybernet/ui/utils/verses.ts @@ -0,0 +1,5 @@ +import { ContractTypes } from '../../types'; + +export function checkIsMLVerse(type: ContractTypes) { + return type === ContractTypes.ML; +} diff --git a/src/features/ipfs/Drive/BackendStatus.tsx b/src/features/ipfs/Drive/BackendStatus.tsx index 8679c72ad..d810f8cce 100644 --- a/src/features/ipfs/Drive/BackendStatus.tsx +++ b/src/features/ipfs/Drive/BackendStatus.tsx @@ -6,7 +6,7 @@ import Display from 'src/components/containerGradient/Display/Display'; // import { ServiceStatus, SyncEntryStatus } from 'src/services/backend/types'; import { ProgressTracking, - ServiceStatus, + ServiceStatus as ServiceStatusInfo, SyncEntryName, SyncProgress, } from 'src/services/backend/types/services'; @@ -16,6 +16,10 @@ import styles from './drive.scss'; import { syncEntryNameToReadable } from 'src/services/backend/services/sync/utils'; import { Button } from 'src/components'; import { downloadJson } from 'src/utils/json'; +import { useBackend } from 'src/contexts/backend/backend'; +import { EmbeddinsDbEntity } from 'src/services/CozoDb/types/entities'; +import { isObject } from 'lodash'; +import { openAICompletion } from 'src/services/scripting/services/llmRequests/openai'; const getProgressTrackingInfo = (progress?: ProgressTracking) => { if (!progress) { @@ -29,13 +33,13 @@ const getProgressTrackingInfo = (progress?: ProgressTracking) => { )}% (${estimatedTimeStr})`; }; -function ServiceStatus({ +function ServiceStatusInfo({ name, status, message, }: { name: string; - status: ServiceStatus; + status: ServiceStatusInfo; message?: string; }) { const icon = status === 'error' ? '❌' : status === 'starting' ? '⏳' : ''; @@ -52,12 +56,18 @@ function EntrySatus({ }) { const msg = progress.error || progress.message ? `- ${progress.message}` : ''; const text = `${syncEntryNameToReadable(name)}: ${progress.status} ${msg} - ${getProgressTrackingInfo(progress.progress)}`; + ${ + !isObject(progress.progress) + ? progress.progress + ? `(${progress.progress}%)` + : '' + : getProgressTrackingInfo(progress.progress) + }`; return
{text}
; } function BackendStatus() { - const { syncState, dbPendingWrites, services } = useAppSelector( + const { syncState, dbPendingWrites, services, mlState } = useAppSelector( (store) => store.backend ); @@ -70,17 +80,35 @@ function BackendStatus() {

Backend status

- - - + + + {Object.keys(mlState.entryStatus).map((name) => ( + + ))} + (); const [queryResults, setQueryResults] = useState<{ rows: []; cols: [] }>(); - const { cozoDbRemote, isReady } = useBackend(); - const { syncState, dbPendingWrites, services } = useAppSelector( - (store) => store.backend - ); + const { cozoDbRemote, isReady, ipfsApi } = useBackend(); + const { embeddingApi } = useScripting(); + // const embeddingApi = useEmbeddingApi(); // console.log('-----syncStatus', syncState, dbPendingWrites); @@ -140,7 +155,7 @@ function Drive() { }); saveAs(blob, 'export.json'); } catch (e) { - console.log('CozoDb: Failed to import', e); + console.log('cozoDb: Failed to import', e); } }; @@ -156,6 +171,77 @@ function Drive() { runQuery(value); }; + // const createParticleEmbeddingsClick = async () => { + // const data = await cozoDbRemote?.runCommand( + // '?[cid, text] := *particle{cid, mime, text, blocks, size, size_local, type}, mime="text/plain"', + // true + // ); + + // let index = 0; + // const totalItems = data!.rows.length; + // setEmbeddingsProcessStatus(`Starting... Total particles (0/${totalItems})`); + + // // eslint-disable-next-line no-restricted-syntax + // for await (const row of data!.rows) { + // const [cid, text] = row; + // const vec = await mlApi?.createEmbedding(text as string); + // const res = await cozoDbRemote?.executePutCommand('embeddings', [ + // { + // cid, + // vec, + // } as EmbeddinsDbEntity, + // ]); + // index++; + // setEmbeddingsProcessStatus( + // `Processing particles (${index}/${totalItems})....` + // ); + // } + // setEmbeddingsProcessStatus( + // `Embeddings complete for (0/${totalItems}) particles!` + // ); + // }; + + const searchByEmbeddingsClick = async () => { + const vec = await embeddingApi?.createEmbedding(searchEmbedding); + const queryText = ` + e[dist, cid] := ~embeddings:semantic{cid | query: vec([${vec}]), bind_distance: dist, k: 20, ef: 50} + ?[dist, cid, text] := e[dist, cid], *particle{cid, text} + `; + setQueryText(queryText); + runQuery(queryText); + }; + + // const summarizeClick = async () => { + // const text = (await ipfsApi!.fetchWithDetails(summarizeCid, 'text')) + // ?.content; + // const output = await mlApi?.getSummary(text!); + // setOutputText(output); + + // }; + + // const questionClick = async () => { + // const text = (await ipfsApi!.fetchWithDetails(summarizeCid, 'text')) + // ?.content; + // const output = await mlApi?.getQA(questionText, text!); + // setOutputText(output); + + // }; + + function onSearchEmbeddingChange(event: React.ChangeEvent) { + const { value } = event.target; + setSearchEmbedding(value); + } + + // function onSummarizeCidChange(event: React.ChangeEvent) { + // const { value } = event.target; + // setSummarizeCid(value); + // } + + // function onQuestionChange(event: React.ChangeEvent) { + // const { value } = event.target; + // setQuestionText(value); + // } + return ( <>
@@ -186,7 +272,46 @@ function Drive() {

+ + {/*
+ +
{embeddingsProcessStatus}
+
+
+ onSummarizeCidChange(e)} + placeholder="enter cid:" + /> + +
+
+ onQuestionChange(e)} + placeholder="enter question..." + /> + +
*/} +
{outputText}
+
+ onSearchEmbeddingChange(e)} + placeholder="enter sentence...." + /> + +
+