Fork me on GitHub
#portal
<
2023-05-23
>
Sam Ritchie15:05:58

Portal Q! I get that I can build a custom viewer by creating a reagent component. But let’s say I want to use some cljs library like Mafs.cljs when building the components — is this possible with portal?

djblue16:05:43

The sci implementation in portal should be able to load cljs code from the classpath so in theory this should be possible 👍

Sam Ritchie16:05:29

Oh got it! I might have to make a custom build that adds all the code I need into the sci context in that case

Sam Ritchie16:05:43

But that’s very comfortable at this point :)

djblue16:05:47

What type of dependencies does your viewer have?

Sam Ritchie16:05:22

Mafs.cljs builds on the mafs npm package

djblue16:05:17

So you can pull js directly from node_modules, https://github.com/djblue/portal/tree/master/examples/plotly-viewer is an example of dynamically loading plotly

Sam Ritchie16:05:17

Oh very cool

djblue16:05:23

I don't think people have used this dependency loading code in portal so it might have issues, but I'm down to fix any issues you run into 👍

Sam Ritchie16:05:46

Okay I’ll take a look!! That’s awesome, I forgot it might be simpler because we’re staying local vs deploying a bundle

👍 2
djblue16:05:28

https://cljdoc.org/d/djblue/portal/0.40.0/doc/guides/custom-viewer is some docs on setting the viewer within the Portal ui runtime

djblue16:05:24

Please let me know if you run into any issues 🙏

Sam Ritchie16:05:36

Will do, thanks for your support!

💯 2
Sam Ritchie22:05:26

@U1G869VNV

(do (portal.api/eval-str
     "(ns portal-present.viewer
        (:require [portal.ui.api :as p]
            [\"mafs\" :as m]    
            [portal.ui.inspector :as ins]
            [reagent.core :as r]))

 (defn view-scene [_]
  [:> m/Mafs {}
     [:> (.-Cartesian m/Coordinates) {}]])

(portal.ui.api/register-viewer!
 {:name ::mafs
  :predicate (constantly true)
  :component view-scene})")
    (tap>
     ^{:portal.viewer/default :portal-present.viewer/mafs}
     []))

Sam Ritchie22:05:13

this is a “hello world”; I am fairly certain I’m going to need to add any new library I want to use to the SCI context

Sam Ritchie22:05:57

yeah, I think I’ll need to extend

djblue22:05:34

The npm support in portal looks like it can import that module

djblue22:05:49

Are you getting a specific error?

Sam Ritchie22:05:01

Show: Project-Only All 
  Hide: Clojure Java REPL Tooling Duplicates  (14 frames hidden)

1. Unhandled clojure.lang.ExceptionInfo
   Could not resolve symbol: m/Mafs
   {:error
    #error {:message "Could not resolve symbol: m/Mafs", :data {:type :sci/error, :line 8, :column 2, :file nil, :phase "analysis"}},
    :message "Could not resolve symbol: m/Mafs",
    :op :portal.rpc/response,
    :portal.rpc/id 39}
              launcher.clj:  117  portal.runtime.jvm.launcher/eval-str
              launcher.clj:  110  portal.runtime.jvm.launcher/eval-str
                  api.cljc:  138  portal.api$eval_str/invokeStatic
                  api.cljc:  122  portal.api$eval_str/invoke
                  api.cljc:  136  portal.api$eval_str/invokeStatic
                  api.cljc:  122  portal.api$eval_str/invoke
                  api.cljc:  134  portal.api$eval_str/invokeStatic
                  api.cljc:  122  portal.api$eval_str/invoke
                      REPL:  743  emmy.mafs.core/eval67631
                      REPL:  743  emmy.mafs.core/eval67631
             Compiler.java: 7194  clojure.lang.Compiler/eval
             Compiler.java: 7183  clojure.lang.Compiler/eval
             Compiler.java: 7149  clojure.lang.Compiler/eval
                  core.clj: 3215  clojure.core/eval
                  core.clj: 3211  clojure.core/eval
    interruptible_eval.clj:   87  nrepl.middleware.interruptible-eval/evaluate/fn/fn
                  AFn.java:  152  clojure.lang.AFn/applyToHelper
                  AFn.java:  144  clojure.lang.AFn/applyTo
                  core.clj:  667  clojure.core/apply
                  core.clj: 1990  clojure.core/with-bindings*
                  core.clj: 1990  clojure.core/with-bindings*
               RestFn.java:  425  clojure.lang.RestFn/invoke
    interruptible_eval.clj:   87  nrepl.middleware.interruptible-eval/evaluate/fn
                  main.clj:  437  clojure.main/repl/read-eval-print/fn
                  main.clj:  437  clojure.main/repl/read-eval-print
                  main.clj:  458  clojure.main/repl/fn
                  main.clj:  458  clojure.main/repl
                  main.clj:  368  clojure.main/repl
               RestFn.java: 1523  clojure.lang.RestFn/invoke
    interruptible_eval.clj:   84  nrepl.middleware.interruptible-eval/evaluate
    interruptible_eval.clj:   56  nrepl.middleware.interruptible-eval/evaluate
    interruptible_eval.clj:  152  nrepl.middleware.interruptible-eval/interruptible-eval/fn/fn
                  AFn.java:   22  clojure.lang.AFn/run
               session.clj:  218  nrepl.middleware.session/session-exec/main-loop/fn
               session.clj:  217  nrepl.middleware.session/session-exec/main-loop
                  AFn.java:   22  clojure.lang.AFn/run
               Thread.java:  833  java.lang.Thread/run

Sam Ritchie22:05:15

I first tried to require a cljs file that was on the classpath. If i do this:

(do (portal.api/eval-str
     "(ns portal-present.viewer
        (:require [portal.ui.api :as p]
            [mafs.core :as m]
            [mafs.coordinate :as c]
            [reagent.core :as r]))

 (defn view-scene [_]
  [m/Mafs [m/Cartesian]])

(p/register-viewer!
 {:name ::mafs
  :predicate (constantly true)
  :component view-scene})")
    (tap>
     ^{:portal.viewer/default :portal-present.viewer/mafs}
     []))
I get
1. Unhandled clojure.lang.ExceptionInfo
   Could not resolve symbol: reagent/with-let
   {:error
    #error {:message "Could not resolve symbol: reagent/with-let", :data {:type :sci/error, :line 190, :column 1, :file "file:/Users/sritchie/code/clj/mafs.cljs/src/mafs/core.cljs", :phase "analysis"}},
    :message "Could not resolve symbol: reagent/with-let",
    :op :portal.rpc/response,
    :portal.rpc/id 40}

djblue22:05:05

What version of Portal are you using?

djblue22:05:06

Here is what I get when I use that viewer

djblue22:05:45

Is there some css that needs to be side loaded?

Sam Ritchie22:05:25

Ah, there is!

Sam Ritchie22:05:40

this is with 0.40.0

Sam Ritchie22:05:01

these are the 3 css files

Sam Ritchie22:05:19

I am puzzled that this is working, but if we can get it going this will be an awesome viewer, I’m really excited

awesome 2
Sam Ritchie22:05:26

to get this going without the ceremony of a Clerk notebook

Sam Ritchie22:05:44

I figured out today how to do a decent job of getting values together that describe interactive scenes

💯 2
Sam Ritchie22:05:49

to get set up, I followed this: https://github.com/djblue/portal#api after adding this dep:

djblue/portal {:mvn/version "0.40.0"}

djblue22:05:13

After I get the css loaded, I get this so this does look promising 👌

Sam Ritchie22:05:30

the actual viewer I will end up writing is something like an eval-viewer… where I am going to pass the reagent component in as my value, something like this built server-side:

[mafs.core/Mafs
 [mafs.coordinates/Cartesian {}]
 [mafs.core/Point {:x 1, :y 2}]]
and then the component will… I guess eval it directly

Sam Ritchie22:05:42

@U1G869VNV so what was the secret??

Sam Ritchie22:05:13

I may have to run shortly to take my daughter to the pool, but I am very motivated here

👍 2
djblue22:05:23

Did you npm install mafs to the root of the repl process? That might be the main difference here if we are using the same version but your Portal is unable to resolve the dependency :thinking_face:

Sam Ritchie22:05:23

I started my repl via CIDER so I wonder if it has a different root

Sam Ritchie22:05:49

It’s npm installed for sure

Sam Ritchie22:05:07

Clearly it was able to find the mafs.cljs dependency and start loading it, so once I get this solved I bet the rest will work

djblue22:05:56

I think for the cljs code to load, I will need to update sci to include with-let 👍

djblue23:05:20

In trying to load mafs.macros, I did run into "No matching clause: require-macros" which I think is a limitation in sci :thinking_face:

Sam Ritchie00:05:47

I have a new version without the macros, I’ll send the branch tonight and more info

👍 2
djblue00:05:13

(ns mafs.macros
  #?(:cljs
     (:require [reagent.core])))

(defmacro defcomponent
  ([sym component]
   `(defcomponent ~sym "" {} ~component))
  ([sym docstring component]
   `(defcomponent ~sym ~docstring {} ~component))
  ([sym docstring attr component]
   {:pre [(string? docstring)
          (map? attr)]}
   (let [m (-> (meta sym)
               (merge (assoc attr :doc docstring)))]
     `(def ~(with-meta sym m)
        (reagent.core/adapt-react-class ~component)))))
worked for me locally

djblue00:05:36

(do (portal.api/eval-str
       "(ns portal-present.viewer
        (:require [portal.ui.api :as p]
            [mafs.core :as m]
            [mafs.coordinates :as c]
            [reagent.core :as r]))

(defn inject [href]
  (let [link (.createElement js/document \"link\")]
    (set! (.-rel link) \"stylesheet\")
    (set! (.-href link) href)
    (js/document.head.appendChild link)))

(inject \"\")
(inject \"\")
(inject \"\")

 (defn view-scene [_]
  [m/Mafs [c/Cartesian]])

(p/register-viewer!
 {:name ::mafs
  :predicate (constantly true)
  :component view-scene})")
      (tap>
       ^{:portal.viewer/default :portal-present.viewer/mafs}
       []))
Works for me with https://github.com/djblue/portal/commit/26188aec167e6e617c5869cbecbcbdba6e9e01a6

djblue00:05:44

Also, if you want to load the code from a file you can

(portal.api/eval-str "(require 'portal-present.viewer :reload)")

djblue00:05:55

Do you have other hello world examples? I would like to try some stuff out before I cut a release 👌

djblue00:05:48

Also, I think it might be nice to have some helpers to get resources from node_modules that aren't js files. Would that be something useful to you? That way the viewer could work entirely offline.

Sam Ritchie00:05:20

Yes I’ll send some this evening, I’m in family maelstrom! I know you’re not pressuring I’m just excited so checking in

👌 2
Sam Ritchie00:05:32

I think it would be helpful to get the CSS out, as I think you’re anticipating. Assuming we’d be able to load those in portal and wouldn’t see an error from loading a file://

Sam Ritchie00:05:48

so anything in https://mafs.mentat.org/ inside of a show-sci form should work. The only change to make is expanding the mafs alias into mafs.core and reagent into reagent.core

🙏 2
Sam Ritchie00:05:28

this branch removes all of the macro code, so pulling this in via git dep should solve that issue

Sam Ritchie02:05:44

I’m running into this error when using portal as a git dep:

(base) [sritchie@wintermute ~/code/clj/emmy-viewers (sritchie/mafs)]$ clj -X:deps prep :aliases '[:nextjournal/clerk :dev]'
Prepping io.github.djblue/portal in /Users/sritchie/.gitlibs/libs/io.github.djblue/portal/26188aec167e6e617c5869cbecbcbdba6e9e01a6
Execution error (FileNotFoundException) at tasks.docs/eval461$loading (docs.clj:1).
Could not locate examples/data__init.class, examples/data.clj or examples/data.cljc on classpath.

Full report at:
/var/folders/xw/0lq56zhn4hb4lknppw_k086c0000gn/T/clojure-4188203586225886789.edn
Execution error (ExceptionInfo) at clojure.tools.deps/prep-libs!$fn (deps.clj:711).
Prep function could not be resolved: tasks.build/prep
I’m going to apply your patch to the clj file inside the 0.40.0 jar 🙂

Sam Ritchie02:05:20

I am trying a local build but running into trouble where I can’t get the prep step to run. I will debug more or maybe wait for a release here, but looks like this is all close to working well

djblue02:05:07

If you have the repo locally, bb dev should get you going

Sam Ritchie02:05:48

ah, very nice, I use those too and should have checked

djblue02:05:31

Looks like I broke prep recently 😭

Sam Ritchie02:05:18

we have liftoff

djblue02:05:36

If you find the need to tweak any of the portal code, try (portal.api/open {:mode :dev})

Sam Ritchie03:05:58

I can get an interactive thing up, but it doesn’t look like reagent atoms are doing their thing inside of portal - probably a mistake on my part

Sam Ritchie03:05:04

let me know if you think this should work:

Sam Ritchie03:05:30

first, this:

(portal.api/eval-str
     "(ns portal-present.viewer
        (:require [portal.ui.api :as p]
            [mafs.debug]
            [mafs.line]
            [mafs.core]
            [mafs.coordinates]
            [mafs.plot]
            [reagent.core :as r]))

(defn inject [href]
  (let [link (.createElement js/document \"link\")]
    (set! (.-rel link) \"stylesheet\")
    (set! (.-href link) href)
    (js/document.head.appendChild link)))

(inject \"\")
(inject \"\")
(inject \"\")


(p/register-viewer!
 {:name ::mafs
  :predicate (constantly true)
  :component eval})")

Sam Ritchie03:05:41

notice that I have :component eval

Sam Ritchie03:05:40

then this works:

(-> '[mafs.core/Mafs [mafs.coordinates/Cartesian]]
     (with-meta {:portal.viewer/default :portal-present.viewer/mafs})
     (tap>))

Sam Ritchie03:05:40

a more complex one also looks great! but the atom state doesn’t seem to update, which means that elements can’t respond reactively. this is obviously some sort of bug on my side, since you have the slideshow example where state does work

Sam Ritchie03:05:30

(-> '(reagent.core/with-let
       [G__68160 (reagent.core/atom [0 0])]
       [:<>
        [:pre (clojure.core/pr-str @G__68160)]
        [mafs.core/Mafs
         [mafs.coordinates/Cartesian {}]
         (reagent.core/with-let
           [G__68161
            (js/Function.
             "[y0001]"
             "[p0002]"
             "  const _0006 = - p0002;\n  const _0008 = _0006 + y0001;\n  const _0013 = Math.sinh(_0008);\n  const _0014 = Math.cosh(_0008);\n  const _0015 = Math.pow(_0014, 4.0);\n  return (- 6.0 * Math.pow(_0013, 4.0) + 8.0 * Math.pow(_0013, 2.0) * Math.pow(_0014, 2.0) - 2.0 * _0015) / _0015;")]
           [mafs.plot/OfX
            {:y
             (let [G__68162 (mapv @G__68160 [0])] (fn [x] (G__68161 [x] G__68162)))}])
         (reagent.core/with-let
           [G__68163
            (js/Function.
             "[y0001]"
             "[p0002]"
             "  const _0006 = - p0002;\n  const _0008 = _0006 + y0001;\n  const _0013 = Math.sinh(_0008);\n  const _0014 = Math.cosh(_0008);\n  const _0015 = Math.pow(_0014, 4.0);\n  return (- 6.0 * Math.pow(_0013, 4.0) + 8.0 * Math.pow(_0013, 2.0) * Math.pow(_0014, 2.0) - 2.0 * _0015) / _0015;")
            G__68165
            (js/Function. "y0001" "  return Math.cos(y0001);")]
           [mafs.plot/Inequality
            {:y
             {:<=
              (let [G__68164 (mapv @G__68160 [0])] (fn [x] (G__68163 [x] G__68164))),
              :> G__68165},
             :color :blue}])
         [mafs.core/MovablePoint {:atom G__68160, :constrain "horizontal"}]]])
    (with-meta {:portal.viewer/default :portal-present.viewer/mafs})
    (tap>))

Sam Ritchie03:05:35

(there’s the code for that example, generated of course, not handwritten)

Sam Ritchie03:05:19

this simpler example seems to fail to update a reagent atom inside of with-let . So maybe with-let is the problem?

(-> '(reagent.core/with-let [!click (reagent.core/atom 0)]
       [:div
        "The atom " [:code "!click"] " has value: "
        @!click ". "
        [:input {:type :button :value "Click me!" :on-click #(swap! !click inc)}]])
    (with-meta {:portal.viewer/default :portal-present.viewer/mafs})
    (tap>))

Sam Ritchie03:05:47

I have to duck out for now, but my next debugging steps are 1. see if this works when I don’t use eval 2. try it without with-let, maybe that has some error curious if you have thoughts too. But this is now looking like it will be trivial for me to ship support for Clerk AND portal with my “emmy-viewers” project, so folks can do 2d stuff like above and also 3d:

💯 2
🎉 2
djblue03:05:29

(-> '(fn []
       (reagent.core/with-let [!click (reagent.core/atom 0)]
         [:div
          "The atom " [:code "!click"] " has value: "
          @!click ". "
          [:input {:type :button
                   :value "Click me!"
                   :on-click
                   (fn [e]
                     (js/console.log e)
                     (.stopPropagation e)
                     (swap! !click inc))}]]))
    (with-meta {:portal.viewer/default :portal-present.viewer/mafs})
    (tap>))
Works for me

djblue03:05:57

I think this is related to it not being a "react" component so wrapping it in a fn produces a functional component 👌

djblue03:05:47

BTW using eval as your viewer is so good chef_kiss

djblue03:05:13

Also, love the js function definitions as strings

djblue03:05:00

I would also be down to ship this type of viewer in portal, something like portal.viewer/reagent :thinking_face:

djblue03:05:37

Then the only thing Emmy needs is the default viewer metadata which could be included unconditionally

Sam Ritchie04:05:16

that sounds great

Sam Ritchie04:05:33

yeah, the eval-style viewer cracked this project open for me

Sam Ritchie04:05:12

the reason it is important is that Emmy can compile these fast JS function definitions down from high level code after running a powerful simplifier on it… I actually defined that function body as

(fn [shift]
  (((cube D) tanh) (- identity shift)))
where the return value is using • function algebra! (- identity shift) is actually a function, (fn [x] (- x shift) , and the whole form is actually (fn [x] (((cube D) tanh) (- x shift))) • automatic differentiation… ((cube D) tanh) is the 3rd derivative of tanh

djblue04:05:40

It makes a lot of sense, you have a specialized optimizer / compiler targeting js 👌

djblue04:05:37

I feel like I can fully appreciate your conj talk now, it's all coming together for me 🙏

Sam Ritchie04:05:54

I come bearing reagent fragments!!

😆 2
Sam Ritchie04:05:46

thanks for all of your help here, I remain happily surprised at how easy it is to fuse together clojure projects that weren’t explicitly designed to fuse

❤️ 2
💯 2
clojure-spin 4
djblue05:05:10

Also, got git deps working again, I guess for a prep builds you don't automatically get your top level :paths/:deps by default :thinking_face:

Sam Ritchie15:05:05

I have an example here in Clerk where (plot/of-x f) returns a reagent fragment, [mafs.plot/OfX {:y <my-compiled-function-body>}], which Clerk then evaluates into an actual reagent component using SCI

👀 2
Sam Ritchie15:05:46

so it feels like this is something I should be able to do in Portal, to let these plot objects render correctly.