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/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..765d42546 --- /dev/null +++ b/docs/scripting.md @@ -0,0 +1,404 @@ +# 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, in developement) + +``` +// Apply prompt OpenAI and get result +cyb::open_ai_prompt(prompt: string; params: json) -> string; +``` + +#### 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 76754ca1e..eb8fedc79 100644 --- a/package.json +++ b/package.json @@ -178,6 +178,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", @@ -186,10 +187,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.82", "datastore-core": "^9.2.3", "datastore-idb": "^2.1.4", "dateformat": "^3.0.3", @@ -227,6 +230,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", @@ -239,6 +243,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/src/components/ContentItem/contentItem.tsx b/src/components/ContentItem/contentItem.tsx index 525c29d81..8819681b2 100644 --- a/src/components/ContentItem/contentItem.tsx +++ b/src/components/ContentItem/contentItem.tsx @@ -1,15 +1,11 @@ // TODO: refactor needed -import React, { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { Link } from 'react-router-dom'; import { $TsFixMe } from 'src/types/tsfix'; -import useQueueIpfsContent from 'src/hooks/useQueueIpfsContent'; -import type { - IpfsContentType, - IPFSContentDetails, -} from 'src/services/ipfs/types'; -import { LinksType } from 'src/containers/Search/types'; +import type { IpfsContentType } from 'src/services/ipfs/types'; +import useParticle from 'src/hooks/useParticle'; -import { parseArrayLikeToDetails } from 'src/services/ipfs/utils/content'; +import { LinksType } from 'src/containers/Search/types'; import SearchItem from '../SearchItem/searchItem'; @@ -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/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/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/contentIpfs/contentIpfs.tsx b/src/components/contentIpfs/contentIpfs.tsx index 0f1fc69bd..f0f027027 100644 --- a/src/components/contentIpfs/contentIpfs.tsx +++ b/src/components/contentIpfs/contentIpfs.tsx @@ -1,4 +1,4 @@ -import { IPFSContentDetails, IPFSContentMaybe } from 'src/services/ipfs/types'; +import { IPFSContent, IPFSContentDetails } from 'src/services/ipfs/types'; import { CYBER_GATEWAY } from 'src/constants/config'; import VideoPlayerGatewayOnly from '../VideoPlayer/VideoPlayerGatewayOnly'; import GatewayContent from './component/gateway'; @@ -7,6 +7,7 @@ import LinkHttp from './component/link'; import Pdf from '../PDF'; import Img from './component/img'; import Audio from './component/Audio/Audio'; +import { Option } from 'src/types'; function OtherItem({ content, @@ -25,6 +26,10 @@ function OtherItem({ return ; } +function HtmlItem({ cid }: { cid: string }) { + return ; +} + function DownloadableItem({ cid, search }: { cid: string; search?: boolean }) { if (search) { return
{`${cid} (gateway)`}
; @@ -34,7 +39,7 @@ function DownloadableItem({ cid, search }: { cid: string; search?: boolean }) { type ContentTabProps = { details: IPFSContentDetails; - content?: IPFSContentMaybe; + content?: Option; cid: string; search?: boolean; }; @@ -77,7 +82,8 @@ function ContentIpfs({ details, content, cid, search }: ContentTabProps) { {contentType === 'link' && ( )} - {contentType === 'other' && ( + {contentType === 'html' && } + {['other', 'cid'].some((i) => i === contentType) && ( )} 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/constants/defaultNetworks.local.ts b/src/constants/defaultNetworks.local.ts new file mode 100644 index 000000000..28da15fad --- /dev/null +++ b/src/constants/defaultNetworks.local.ts @@ -0,0 +1,34 @@ +import { NetworkConfig, Networks } from 'src/types/networks'; + +type NetworksList = { + [key in Networks.BOSTROM | Networks.SPACE_PUSSY]: NetworkConfig; +}; + +const defaultNetworks: NetworksList = { + bostrom: { + CHAIN_ID: Networks.BOSTROM, + BASE_DENOM: 'boot', + DENOM_LIQUID: 'hydrogen', + RPC_URL: 'http://localhost:26657', //'https://rpc.bostrom.cybernode.ai', + LCD_URL: 'http://localhost:1317/', //'https://lcd.bostrom.cybernode.ai', + WEBSOCKET_URL: 'ws://localhost:26657', //'wss://rpc.bostrom.cybernode.ai/websocket', + INDEX_HTTPS: 'http://localhost:8090/v1/graphql', //'https://index.bostrom.cybernode.ai/v1/graphql', + INDEX_WEBSOCKET: 'ws://localhost:8090/v1/graphql', // 'wss://index.bostrom.cybernode.ai/v1/graphql', + BECH32_PREFIX: 'bostrom', + MEMO_KEPLR: '[bostrom] cyb.ai, using keplr', + }, + 'space-pussy': { + CHAIN_ID: Networks.SPACE_PUSSY, + BASE_DENOM: 'pussy', + DENOM_LIQUID: 'liquidpussy', + RPC_URL: 'https://rpc.space-pussy.cybernode.ai/', + LCD_URL: 'https://lcd.space-pussy.cybernode.ai', + WEBSOCKET_URL: 'wss://rpc.space-pussy.cybernode.ai/websocket', + INDEX_HTTPS: 'https://index.space-pussy.cybernode.ai/v1/graphql', + INDEX_WEBSOCKET: 'wss://index.space-pussy.cybernode.ai/v1/graphql', + BECH32_PREFIX: 'pussy', + MEMO_KEPLR: '[space-pussy] cyb.ai, using keplr', + }, +}; + +export default defaultNetworks; diff --git a/src/constants/defaultNetworks.ts b/src/constants/defaultNetworks.ts index 9342a27ca..7283d8e7e 100644 --- a/src/constants/defaultNetworks.ts +++ b/src/constants/defaultNetworks.ts @@ -9,9 +9,15 @@ const defaultNetworks: NetworksList = { CHAIN_ID: Networks.BOSTROM, BASE_DENOM: 'boot', DENOM_LIQUID: 'hydrogen', - RPC_URL: 'https://rpc.bostrom.cybernode.ai', - LCD_URL: 'https://lcd.bostrom.cybernode.ai', - WEBSOCKET_URL: 'wss://rpc.bostrom.cybernode.ai/websocket', + RPC_URL: process.env.IS_DEV + ? 'https://rpc.arch.bostrom.cybernode.ai:443' + : 'https://rpc.bostrom.cybernode.ai', + LCD_URL: process.env.IS_DEV + ? 'https://lcd.arch.bostrom.cybernode.ai:443' + : 'https://lcd.bostrom.cybernode.ai', + WEBSOCKET_URL: process.env.IS_DEV + ? 'wss://rpc.arch.bostrom.cybernode.ai:443/websocket' + : 'wss://rpc.bostrom.cybernode.ai/websocket', INDEX_HTTPS: 'https://index.bostrom.cybernode.ai/v1/graphql', INDEX_WEBSOCKET: 'wss://index.bostrom.cybernode.ai/v1/graphql', BECH32_PREFIX: 'bostrom', diff --git a/src/containers/Search/SearchResults.tsx b/src/containers/Search/SearchResults.tsx index 6dfa91716..6b369f40a 100644 --- a/src/containers/Search/SearchResults.tsx +++ b/src/containers/Search/SearchResults.tsx @@ -4,18 +4,16 @@ 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'; @@ -89,15 +87,9 @@ function SearchResults({ setContentType({}); (async () => { - let keywordHashTemp = ''; + const keywordHash = await getSearchQuery(query); - if (query.match(PATTERN_IPFS_HASH)) { - keywordHashTemp = query; - } else { - keywordHashTemp = await getIpfsHash(encodeSlash(query)); - } - - setKeywordHash(keywordHashTemp); + setKeywordHash(keywordHash); })(); }, [query]); 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/ipfs/components/SoulCompanion/SoulCompanion.tsx b/src/containers/ipfs/components/SoulCompanion/SoulCompanion.tsx new file mode 100644 index 000000000..9a8f4afbb --- /dev/null +++ b/src/containers/ipfs/components/SoulCompanion/SoulCompanion.tsx @@ -0,0 +1,87 @@ +import { Link } from 'react-router-dom'; +import { + ScriptingContextType, + useScripting, +} from 'src/contexts/scripting/scripting'; +import type { ScriptMyCampanion } from 'src/services/scripting/types'; +import styles from './soulCompanion.module.scss'; +import { shortenString } from 'src/utils/string'; +import { ParticleCid } from 'src/types/base'; +import { IPFSContentDetails } from 'src/services/ipfs/types'; +import { Option } from 'src/types'; +import { useEffect, useState } from 'react'; +import { proxy } from 'comlink'; + +type AskCompanionStatus = 'loading' | 'ready' | 'pending' | 'done' | 'error'; + +function SoulCompanion({ + cid, + details, + skip, +}: { + cid: ParticleCid; + details: Option; + skip: boolean; +}) { + const [metaItems, setMetaItems] = useState( + [] + ); + const [status, setStatus] = useState('loading'); + const { rune, isSoulInitialized } = useScripting(); + + useEffect(() => { + if (!skip && isSoulInitialized && details) { + if (details.type && details.type !== 'text' && details.text) { + setStatus('done'); + setMetaItems([ + { type: 'text', text: `Skip companion for '${details.content}'.` }, + ]); + return; + } + + rune + ?.askCompanion( + cid, + details!.type!, + details!.text!.substring(0, 255), + proxy((data = {}) => console.log('CALLBACK')) + ) + .then((result) => { + setMetaItems(result.metaItems); + setStatus('done'); + }); + } + }, [cid, skip, rune, isSoulInitialized, details]); + + useEffect(() => { + setMetaItems([]); + }, [cid]); + + if (status !== 'done' && metaItems) { + return ( +
{`soul companion status: ${status}`}
+ ); + } + return ( +
+
    + {metaItems.map((item, index) => ( +
  • + {item.type === 'text' && ( +

    {item.text}

    + )} + {item.type === 'link' && ( + + {shortenString(item.title, 64)} + + )} +
  • + ))} +
+
+ ); +} + +export default SoulCompanion; diff --git a/src/containers/ipfs/components/SoulCompanion/soulCompanion.module.scss b/src/containers/ipfs/components/SoulCompanion/soulCompanion.module.scss new file mode 100644 index 000000000..d65acd3fa --- /dev/null +++ b/src/containers/ipfs/components/SoulCompanion/soulCompanion.module.scss @@ -0,0 +1,24 @@ +.itemLinks { + display: flex; + margin: -10px 0; + + list-style-type: none; + font-size: 14px; +} + +.itemText { + font-size: 14px; + display: block; +} + +.itemLink { + margin: 0 5px; + display: block; + // white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 158px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} diff --git a/src/containers/ipfs/hooks/useGetAnswers.ts b/src/containers/ipfs/hooks/useGetAnswers.ts index 061a7817a..d59ab85ba 100644 --- a/src/containers/ipfs/hooks/useGetAnswers.ts +++ b/src/containers/ipfs/hooks/useGetAnswers.ts @@ -3,12 +3,11 @@ import { useQueryClient } from 'src/contexts/queryClient'; import { useState } from 'react'; import { reduceParticleArr } from './useGetBackLink'; import { searchByHash } from 'src/utils/search/utils'; -import { useBackend } from 'src/contexts/backend/backend'; import { mapLinkToLinkDto } from 'src/services/CozoDb/mapping'; +import { enqueueLinksSave } from 'src/services/backend/channels/BackendQueueChannel/backendQueueSenders'; function useGetAnswers(hash) { const queryClient = useQueryClient(); - const { defferedDbApi } = useBackend(); const [total, setTotal] = useState(0); const { status, @@ -26,10 +25,7 @@ function useGetAnswers(hash) { const reduceArr = result ? reduceParticleArr(result) : []; setTotal(pageParam === 0 && response.pagination.total); - defferedDbApi?.importCyberlinks( - result.map((l) => mapLinkToLinkDto(hash, l.particle)) - ); - + enqueueLinksSave(result.map((l) => mapLinkToLinkDto(hash, l.particle))); return { data: reduceArr, page: pageParam }; }, { diff --git a/src/containers/ipfs/ipfs.tsx b/src/containers/ipfs/ipfs.tsx index eb79cb8c3..8d5d190ad 100644 --- a/src/containers/ipfs/ipfs.tsx +++ b/src/containers/ipfs/ipfs.tsx @@ -1,27 +1,27 @@ import { useParams } from 'react-router-dom'; import ContentIpfs from 'src/components/contentIpfs/contentIpfs'; -import useQueueIpfsContent from 'src/hooks/useQueueIpfsContent'; import { useEffect, useMemo, useState } from 'react'; import { useAdviser } from 'src/features/adviser/context'; import { encodeSlash } from 'src/utils/utils'; import { PATTERN_IPFS_HASH } from 'src/constants/patterns'; import { getIpfsHash } from 'src/utils/ipfs/helpers'; -import { parseArrayLikeToDetails } from 'src/services/ipfs/utils/content'; -import { IPFSContentDetails } from 'src/services/ipfs/types'; import { useBackend } from 'src/contexts/backend/backend'; + +import useParticle from 'src/hooks/useParticle'; + import { Dots, MainContainer } from '../../components'; import ContentIpfsCid from './components/ContentIpfsCid'; import styles from './IPFS.module.scss'; import SearchResults from '../Search/SearchResults'; import AdviserMeta from './components/AdviserMeta/AdviserMeta'; +import SoulCompanion from './components/SoulCompanion/SoulCompanion'; function Ipfs() { const { query = '' } = useParams(); const [cid, setCid] = useState(''); + const { details, status, content, mutated } = useParticle(cid); - const { fetchParticle, status, content } = useQueueIpfsContent(cid); const { ipfsApi, isIpfsInitialized, isReady } = useBackend(); - const [ipfsDataDetails, setIpfsDatDetails] = useState(); const { setAdviser } = useAdviser(); @@ -36,32 +36,12 @@ function Ipfs() { } else if (isIpfsInitialized) { (async () => { const cidFromQuery = (await getIpfsHash(encodeSlash(query))) as string; - await ipfsApi!.addContent(query); - + ipfsApi!.addContent(query); setCid(cidFromQuery); })(); } }, [isText, isReady, query, ipfsApi, isIpfsInitialized]); - - useEffect(() => { - (async () => { - cid && fetchParticle && (await fetchParticle(cid)); - })(); - }, [cid, fetchParticle]); - - useEffect(() => { - if (status === 'completed') { - (async () => { - const details = await parseArrayLikeToDetails( - content, - cid - // (progress: number) => console.log(`${cid} progress: ${progress}`) - ); - setIpfsDatDetails(details); - })(); - } - }, [content, status, cid]); - + useEffect(() => {}, [details]); useEffect(() => { if (!status) { return; @@ -84,19 +64,19 @@ function Ipfs() { setAdviser( , 'purple' ); } - }, [ipfsDataDetails, setAdviser, cid, content, status]); + }, [details, setAdviser, cid, content, status]); return (
- {status === 'completed' && ipfsDataDetails ? ( - + {status === 'completed' && details ? ( + ) : isText ? ( )}
- +
); diff --git a/src/containers/market/index.jsx b/src/containers/market/index.jsx index 858b5e5f1..0b59fe8aa 100644 --- a/src/containers/market/index.jsx +++ b/src/containers/market/index.jsx @@ -16,6 +16,7 @@ import { coinDecimals } from '../../utils/utils'; import { useQueryClient } from 'src/contexts/queryClient'; import { useBackend } from 'src/contexts/backend/backend'; import { mapLinkToLinkDto } from 'src/services/CozoDb/mapping'; +import { enqueueLinksSave } from 'src/services/backend/channels/BackendQueueChannel/backendQueueSenders'; function ContainerGrid({ children }) { return ( @@ -52,7 +53,6 @@ const reduceSearchResults = (data, query) => { function Market({ defaultAccount }) { const { addressActive } = useSetActiveAddress(defaultAccount); const queryClient = useQueryClient(); - const { defferedDbApi } = useBackend(); const { tab = 'BOOT' } = useParams(); const { gol, cyb, boot, hydrogen, milliampere, millivolt, tocyb } = @@ -82,7 +82,8 @@ function Market({ defaultAccount }) { setLoadingSearch(false); setAllPage(Math.ceil(parseFloat(response.pagination.total) / 10)); setPage((item) => item + 1); - defferedDbApi?.importCyberlinks( + + enqueueLinksSave( response.result.map((l) => mapLinkToLinkDto(hash, l.particle)) ); } else { @@ -95,7 +96,7 @@ function Market({ defaultAccount }) { } }; getFirstItem(); - }, [queryClient, tab, defferedDbApi, update]); + }, [queryClient, tab, update]); const fetchMoreData = async () => { // a fake async api call like which sends @@ -104,7 +105,8 @@ function Market({ defaultAccount }) { const response = await searchByHash(queryClient, keywordHash, page); if (response.result) { links = reduceSearchResults(response, tab); - defferedDbApi?.importCyberlinks( + + enqueueLinksSave( response.result.map((l) => mapLinkToLinkDto(keywordHash, l.particle)) ); } diff --git a/src/containers/portal/PasportMoonCitizenship.tsx b/src/containers/portal/PasportMoonCitizenship.tsx index caa2ec608..2bd7f7934 100644 --- a/src/containers/portal/PasportMoonCitizenship.tsx +++ b/src/containers/portal/PasportMoonCitizenship.tsx @@ -47,9 +47,8 @@ function PassportMoonCitizenship() { const { isMobile: mobile } = useDevice(); const defaultAccount = useAppSelector((state) => state.pocket.defaultAccount); - const p = useAppSelector(selectCurrentPassport); + const citizenship = useAppSelector(selectCurrentPassport); // FIXME: backward compatibility - const citizenship = p.data || null; const queryClient = useQueryClient(); const { addressActive } = useSetActiveAddress(defaultAccount); diff --git a/src/containers/sigma/hooks/useGetPassportByAddress.ts b/src/containers/sigma/hooks/useGetPassportByAddress.ts index 2a528fa3a..a8bc4d1a6 100644 --- a/src/containers/sigma/hooks/useGetPassportByAddress.ts +++ b/src/containers/sigma/hooks/useGetPassportByAddress.ts @@ -12,7 +12,7 @@ function useGetPassportByAddress(accounts: any) { // temp for debug if (typeof address === 'object') { address = ''; - debugger; + // debugger; } const { data, loading, error } = usePassportContract({ 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..aafc11d26 100644 --- a/src/contexts/backend/services/senseApi.ts +++ b/src/contexts/backend/services/senseApi.ts @@ -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/scripting/scripting.tsx b/src/contexts/scripting/scripting.tsx new file mode 100644 index 000000000..418093e7f --- /dev/null +++ b/src/contexts/scripting/scripting.tsx @@ -0,0 +1,135 @@ +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(() => { + const setupObservervable = async () => { + const { isSoulInitialized$ } = runeBackend; + + const soulSubscription = (await isSoulInitialized$).subscribe((v) => { + setIsSoulInitialized(!!v); + if (v) { + runeRef.current = runeBackend; + console.log('👻 soul initalized'); + } + }); + + const embeddingApiSubscription = (await embeddingApi$).subscribe( + proxy((embeddingApi) => { + if (embeddingApi) { + embeddingApiRef.current = embeddingApi; + console.log('+ embedding api initalized', embeddingApi); + } + }) + ); + + return () => { + soulSubscription.unsubscribe(); + embeddingApiSubscription.unsubscribe(); + }; + }; + + setupObservervable(); + }, []); + + const runeEntryPoints = useAppSelector(selectRuneEntypoints); + + const citizenship = useAppSelector(selectCurrentPassport); + + useEffect(() => { + (async () => { + if (citizenship) { + const particleCid = citizenship.extension.particle; + + await runeBackend.pushContext('user', { + address: citizenship.owner, + nickname: citizenship.extension.nickname, + citizenship, + particle: particleCid, + } as UserContext); + } else { + await runeBackend.popContext(['user', 'secrets']); + } + })(); + }, [citizenship, runeBackend]); + + 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/ipfs/Drive/BackendStatus.tsx b/src/features/ipfs/Drive/BackendStatus.tsx index 8679c72ad..d2f155340 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 { promptToOpenAI } 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...." + /> + +
+