portal 2023-05-23

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?

@djblue

(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}
     []))

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

yeah, I think I’ll need to extend

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

Are you getting a specific error?

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

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}

What version of Portal are you using?

Here is what I get when I use that viewer

Is there some css that needs to be side loaded?

Ah, there is!

this is with 0.40.0

[""
          ""
          ""]

these are the 3 css files

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

1

to get this going without the ceremony of a Clerk notebook

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

💯 1

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

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

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

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

@djblue so what was the secret??

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

👍 1

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 🤔

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

It’s npm installed for sure

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

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

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

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

👍 1

(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

(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

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

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

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

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.

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

👌 1

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://

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

🙏 1

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

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 🙂

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

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

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

Looks like I broke prep recently 😭

we have liftoff

2
🚀 2

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

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

let me know if you think this should work:

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})")

notice that I have :component eval

then this works:

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

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

(-> '(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>))

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

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>))

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:

💯 1
🎉 1

(-> '(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

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

BTW using eval as your viewer is so good chef_kiss

Also, love the js function definitions as strings

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

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

that sounds great

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

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

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

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

I come bearing reagent fragments!!

😆 1

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

💯 1
2
❤️ 1

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

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

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

But that’s very comfortable at this point :)

What type of dependencies does your viewer have?

Mafs.cljs builds on the mafs npm package

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

Oh very cool

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 👍

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

👍 1

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

Please let me know if you run into any issues 🙏

Will do, thanks for your support!

💯 1

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

👀 1

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