Livebook is a web application for writing interactive and collaborative code notebooks. It features:
diff --git a/lib/livebook/notebook/explore.ex b/lib/livebook/notebook/explore.ex
new file mode 100644
index 000000000..befde2ee9
--- /dev/null
+++ b/lib/livebook/notebook/explore.ex
@@ -0,0 +1,114 @@
+defmodule Livebook.Notebook.Explore.Utils do
+ @moduledoc false
+
+ @doc """
+ Defines a module attribute `attr` with notebook info.
+ """
+ defmacro defnotebook(attr, props) do
+ quote bind_quoted: [attr: attr, props: props] do
+ {path, notebook_info} = Livebook.Notebook.Explore.Utils.fetch_notebook!(attr, props)
+
+ @external_resource path
+
+ Module.put_attribute(__MODULE__, attr, notebook_info)
+ end
+ end
+
+ def fetch_notebook!(attr, props) do
+ name = Atom.to_string(attr)
+ path = Path.join([__DIR__, "explore", name <> ".livemd"])
+
+ markdown = File.read!(path)
+ # Parse the file to ensure no warnings and read the title.
+ # However, in the info we keep just the file contents to save on memory.
+ {notebook, []} = Livebook.LiveMarkdown.Import.notebook_from_markdown(markdown)
+
+ notebook_info = %{
+ slug: String.replace(name, "_", "-"),
+ livemd: markdown,
+ title: notebook.name,
+ description: Keyword.fetch!(props, :description),
+ image_url: Keyword.fetch!(props, :image_url)
+ }
+
+ {path, notebook_info}
+ end
+end
+
+defmodule Livebook.Notebook.Explore do
+ @moduledoc false
+
+ defmodule NotFoundError do
+ @moduledoc false
+
+ defexception [:slug, plug_status: 404]
+
+ def message(%{slug: slug}) do
+ "could not find an example notebook matching #{inspect(slug)}"
+ end
+ end
+
+ import Livebook.Notebook.Explore.Utils
+
+ defnotebook(:intro_to_livebook,
+ description: "Get to know Livebook, see how it works and explore its features.",
+ image_url: "/images/logo.png"
+ )
+
+ defnotebook(:intro_to_elixir,
+ description: "New to Elixir? Learn about the language and its core concepts.",
+ image_url: "/images/elixir.png"
+ )
+
+ defnotebook(:intro_to_nx,
+ description:
+ "Enter numerical Elixir, experience the power of multi-dimensional arrays of numbers.",
+ image_url: "/images/nx.png"
+ )
+
+ defnotebook(:intro_to_axon,
+ description: "Build Neural Networks in Elixir using a high-level, composable API.",
+ image_url: "/images/axon.png"
+ )
+
+ defnotebook(:intro_to_vega_lite,
+ description: "Learn how to quickly create numerous plots for your data.",
+ image_url: "/images/vega_lite.png"
+ )
+
+ @type notebook_info :: %{
+ slug: String.t(),
+ livemd: String.t(),
+ title: String.t(),
+ description: String.t(),
+ image_url: String.t()
+ }
+
+ @doc """
+ Returns a list of example notebooks with metadata.
+ """
+ @spec notebook_infos() :: list(notebook_info())
+ def notebook_infos() do
+ [
+ @intro_to_livebook
+ # @intro_to_elixir, @intro_to_nx, @intro_to_axon, @intro_to_vega_lite
+ ]
+ end
+
+ @doc """
+ Finds explore notebook by slug and returns the parsed data structure.
+ """
+ @spec notebook_by_slug!(String.t()) :: Livebook.Notebook.t()
+ def notebook_by_slug!(slug) do
+ notebook_infos()
+ |> Enum.find(&(&1.slug == slug))
+ |> case do
+ nil ->
+ raise NotFoundError, slug: slug
+
+ notebook_info ->
+ {notebook, []} = Livebook.LiveMarkdown.Import.notebook_from_markdown(notebook_info.livemd)
+ notebook
+ end
+ end
+end
diff --git a/lib/livebook/notebook/explore/intro_to_axon.livemd b/lib/livebook/notebook/explore/intro_to_axon.livemd
new file mode 100644
index 000000000..24ced0adf
--- /dev/null
+++ b/lib/livebook/notebook/explore/intro_to_axon.livemd
@@ -0,0 +1,3 @@
+# Neural Networks with Axon
+
+TODO: content 🐈
diff --git a/lib/livebook/notebook/explore/intro_to_elixir.livemd b/lib/livebook/notebook/explore/intro_to_elixir.livemd
new file mode 100644
index 000000000..0db126e46
--- /dev/null
+++ b/lib/livebook/notebook/explore/intro_to_elixir.livemd
@@ -0,0 +1,3 @@
+# Introduction to Elixir
+
+TODO: content 🐈
diff --git a/lib/livebook/notebook/explore/intro_to_livebook.livemd b/lib/livebook/notebook/explore/intro_to_livebook.livemd
new file mode 100644
index 000000000..f029ba2e9
--- /dev/null
+++ b/lib/livebook/notebook/explore/intro_to_livebook.livemd
@@ -0,0 +1,213 @@
+# Welcome to Livebook
+
+## Introduction
+
+We are happy you decided to give Livebook a try, hopefully it empowers
+you to build great stuff! 🚀
+
+Livebook is a tool for crafting **interactive** and **collaborative** code notebooks.
+It is primarily meant as a tool for rapid prototyping - think of it as an IEx session
+combined with your editor.
+You can also use it for authoring shareable articles that people can easily
+run and play around with.
+Package authors can write notebooks as interactive tutorials
+and include them in their repository, so that users can easily download
+and run them locally.
+
+## Basic usage
+
+Each notebook consists of a number of cells, which serve as primary building blocks.
+There are **Markdown** cells (such as this one) that allow you to describe your work
+and **Elixir** cells where the magic takes place!
+
+To insert a new cell move your cursor between cells and click one of the revealed buttons. 👇
+
+```elixir
+# This is an Elixir cell - as the name suggests that's where the code goes.
+# To evaluate this cell, you can either press the "Evaluate" button above
+# or use `Ctrl + Enter` (or Cmd + Enter on a Mac)!
+
+message = "hey, grab yourself a cup of 🍵"
+```
+
+Subsequent cells have access to the bindings you've defined:
+
+```elixir
+String.replace(message, "🍵", "☕")
+```
+
+Note however that bindings are not global, so each cell *sees* only stuff that goes
+above itself. This approach helps to keep the notebook clean and predictable
+as you keep working on it!
+
+## Sections
+
+You can leverage so called **sections** to nicely group related cells together.
+Click on the "Book" icon in the sidebar to reveal a list of all sections.
+As you can see, this approach helps to easily jump around the notebook,
+especially once it grows.
+
+Let's make use of this section to see how output is captured!
+
+```elixir
+cats = ~w(😼 😹 😻 😺 😸 😽)
+
+for _ <- 1..3 do
+ cats
+ |> Enum.take_random(3)
+ |> Enum.join(" ")
+ |> IO.puts()
+end
+```
+
+## Notebook files
+
+By default notebooks are kept in memory, which is fine for interactive hacking,
+but oftentimes you will want to save your work for later. Fortunately, notebooks
+can be persisted by clicking on the "Disk" icon in the bottom-right corner
+and selecting the file location.
+
+Notebooks are stored in **live markdown** format, which is essentially the markdown you know,
+with just a few assumptions on how particular elements are represented. Thanks to this
+approach you can easily keep notebooks under version control and get readable diffs.
+You can also easily preview those files, reuse for blog posts, and even edit in a text editor.
+
+## Modules
+
+As we have seen, Elixir cells can be used for working on tiny snippets,
+but you may as well define a module!
+
+```elixir
+defmodule Utils do
+ @doc """
+ Generates a random binary id.
+ """
+ @spec random_id() :: binary()
+ def random_id() do
+ :crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower)
+ end
+end
+```
+
+If you're surprised by the above output, keep in mind that
+every Elixir expression evaluates to some value and as so does module compilation!
+
+Having the module defined, let's take it for a spin.
+
+```elixir
+Utils.random_id()
+```
+
+## Imports
+
+You can import modules as normally to make the imported functions visible
+to all subsequent cells. Usually you want to keep `import`, `alias` and `require`
+in the first section, as part of the notebook setup.
+
+```elixir
+import IEx.Helpers
+```
+
+```elixir
+h(Enum.map())
+```
+
+```elixir
+# Sidenote: http://www.numbat.org.au/thenumbat
+i("I ❤️ Numbats")
+```
+
+## Runtimes
+
+Livebook has a concept of **runtime**, which in practice is an Elixir node responsible
+for evaluating your code.
+
+By default, a new Elixir node is started (similarly to starting `iex`),
+but you can also choose to run inside a Mix project (as you would with `iex -S mix`)
+or even manually attach to an existing distributed node!
+You can configure the runtime by clicking the "Runtime" icon on the sidebar.
+
+## Using packages
+
+Sometimes you need a dependency or two and notebooks are no exception to this.
+
+One way to work with packages is to create a Mix project and configure the notebook
+to run in its context (as pointed out above). This approach makes sense if you already have
+a Mix project that you are working on, especially because this makes all project's
+modules available as well.
+
+But there are cases when you just want to play around with a new package
+or quickly prototype some code that relies on such. Fortunately, Elixir v1.12+ ships with
+[`Mix.install/2`](https://hexdocs.pm/mix/Mix.html#install/2) that allows you to install
+dependencies into your Elixir runtime! This approach is especially useful when sharing notebooks
+because everyone will be able to get the same dependencies. Let's try this out:
+
+**Note:** compiling dependencies may use a reasonable amount of memory. If you are
+hosting Livebook, make sure you have enough memory allocated to the Livebook
+instance, otherwise the command below will fail.
+
+```elixir
+Mix.install([
+ {:jason, "~> 1.2"}
+])
+```
+
+```elixir
+%{elixir: "is awesome"}
+|> Jason.encode!()
+|> IO.puts()
+```
+
+It is a good idea to specify versions of the installed packages,
+so that the notebook is easily reproducible later on.
+
+Also keep in mind that `Mix.install/2` can be called only once
+per runtime, so if you need to modify the dependencies, you should
+go to the notebook runtime configuration and **reconnect** the current runtime.
+
+## Running tests
+
+It is also possible to run tests directly from your notebooks.
+The key is to disable `ExUnit`'s autorun feature and then explicitly
+run the test suite after all test cases have been defined:
+
+```elixir
+ExUnit.start(autorun: false)
+
+defmodule MyTest do
+ use ExUnit.Case, async: true
+
+ test "it works" do
+ assert true
+ end
+end
+
+ExUnit.run()
+```
+
+## Math
+
+Livebook uses $\\TeX$ syntax for math.
+It supports both inline math like $e^{\\pi i} + 1 = 0$, as well as display math:
+
+$$
+S(x) = \\frac{1}{1 + e^{-x}} = \\frac{e^{x}}{e^{x} + 1}
+$$
+
+You can explore all supported expressions [here](https://katex.org/docs/supported.html).
+
+## Stepping up your workflow
+
+Once you start using notebooks more, it's gonna be beneficial
+to optimise how you move around. Livebook leverages the concept of
+**navigation**/**insert** modes and offers many shortcuts for common operations.
+Make sure to check out the shortcuts by clicking the "Keyboard" icon in
+the sidebar or by typing `?`.
+
+## Final notes
+
+Livebook is an open source project, so feel free to look into
+[the repository](https://github.com/elixir-nx/livebook)
+to contribute, report bugs, suggest features or just skim over the codebase.
+
+Now go ahead and build something cool! 🚢
diff --git a/lib/livebook/notebook/explore/intro_to_nx.livemd b/lib/livebook/notebook/explore/intro_to_nx.livemd
new file mode 100644
index 000000000..fcbec5078
--- /dev/null
+++ b/lib/livebook/notebook/explore/intro_to_nx.livemd
@@ -0,0 +1,3 @@
+# Introduction to Nx
+
+ TODO: content 🐈
diff --git a/lib/livebook/notebook/explore/intro_to_vega_lite.livemd b/lib/livebook/notebook/explore/intro_to_vega_lite.livemd
new file mode 100644
index 000000000..587e7cad6
--- /dev/null
+++ b/lib/livebook/notebook/explore/intro_to_vega_lite.livemd
@@ -0,0 +1,3 @@
+# Plotting with VegaLite and Kino
+
+TODO: content 🐈
diff --git a/lib/livebook/notebook/welcome.ex b/lib/livebook/notebook/welcome.ex
deleted file mode 100644
index 16c789c11..000000000
--- a/lib/livebook/notebook/welcome.ex
+++ /dev/null
@@ -1,225 +0,0 @@
-defmodule Livebook.Notebook.Welcome do
- livemd = ~s'''
- # Welcome to Livebook
-
- ## Introduction
-
- We are happy you decided to give Livebook a try, hopefully it empowers
- you to build great stuff! 🚀
-
- Livebook is a tool for crafting **interactive** and **collaborative** code notebooks.
- It is primarily meant as a tool for rapid prototyping - think of it as an IEx session
- combined with your editor.
- You can also use it for authoring shareable articles that people can easily
- run and play around with.
- Package authors can write notebooks as interactive tutorials
- and include them in their repository, so that users can easily download
- and run them locally.
-
- ## Basic usage
-
- Each notebook consists of a number of cells, which serve as primary building blocks.
- There are **Markdown** cells (such as this one) that allow you to describe your work
- and **Elixir** cells where the magic takes place!
-
- To insert a new cell move your cursor between cells and click one of the revealed buttons. 👇
-
- ```elixir
- # This is an Elixir cell - as the name suggests that's where the code goes.
- # To evaluate this cell, you can either press the "Evaluate" button above
- # or use `Ctrl + Enter` (or Cmd + Enter on a Mac)!
-
- message = "hey, grab yourself a cup of 🍵"
- ```
-
- Subsequent cells have access to the bindings you've defined:
-
- ```elixir
- String.replace(message, "🍵", "☕")
- ```
-
- Note however that bindings are not global, so each cell *sees* only stuff that goes
- above itself. This approach helps to keep the notebook clean and predictable
- as you keep working on it!
-
- ## Sections
-
- You can leverage so called **sections** to nicely group related cells together.
- Click on the "Book" icon in the sidebar to reveal a list of all sections.
- As you can see, this approach helps to easily jump around the notebook,
- especially once it grows.
-
- Let's make use of this section to see how output is captured!
-
- ```elixir
- cats = ~w(😼 😹 😻 😺 😸 😽)
-
- for _ <- 1..3 do
- cats
- |> Enum.take_random(3)
- |> Enum.join(" ")
- |> IO.puts()
- end
- ```
-
- ## Notebook files
-
- By default notebooks are kept in memory, which is fine for interactive hacking,
- but oftentimes you will want to save your work for later. Fortunately, notebooks
- can be persisted by clicking on the "Disk" icon in the bottom-right corner
- and selecting the file location.
-
- Notebooks are stored in **live markdown** format, which is essentially the markdown you know,
- with just a few assumptions on how particular elements are represented. Thanks to this
- approach you can easily keep notebooks under version control and get readable diffs.
- You can also easily preview those files, reuse for blog posts, and even edit in a text editor.
-
- ## Modules
-
- As we have seen, Elixir cells can be used for working on tiny snippets,
- but you may as well define a module!
-
- ```elixir
- defmodule Utils do
- @doc """
- Generates a random binary id.
- """
- @spec random_id() :: binary()
- def random_id() do
- :crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower)
- end
- end
- ```
-
- If you're surprised by the above output, keep in mind that
- every Elixir expression evaluates to some value and as so does module compilation!
-
- Having the module defined, let's take it for a spin.
-
- ```elixir
- Utils.random_id()
- ```
-
- ## Imports
-
- You can import modules as normally to make the imported functions visible
- to all subsequent cells. Usually you want to keep `import`, `alias` and `require`
- in the first section, as part of the notebook setup.
-
- ```elixir
- import IEx.Helpers
- ```
-
- ```elixir
- h(Enum.map())
- ```
-
- ```elixir
- # Sidenote: http://www.numbat.org.au/thenumbat
- i("I ❤️ Numbats")
- ```
-
- ## Runtimes
-
- Livebook has a concept of **runtime**, which in practice is an Elixir node responsible
- for evaluating your code.
-
- By default, a new Elixir node is started (similarly to starting `iex`),
- but you can also choose to run inside a Mix project (as you would with `iex -S mix`)
- or even manually attach to an existing distributed node!
- You can configure the runtime by clicking the "Runtime" icon on the sidebar.
-
- ## Using packages
-
- Sometimes you need a dependency or two and notebooks are no exception to this.
-
- One way to work with packages is to create a Mix project and configure the notebook
- to run in its context (as pointed out above). This approach makes sense if you already have
- a Mix project that you are working on, especially because this makes all project's
- modules available as well.
-
- But there are cases when you just want to play around with a new package
- or quickly prototype some code that relies on such. Fortunately, Elixir v1.12+ ships with
- [`Mix.install/2`](https://hexdocs.pm/mix/Mix.html#install/2) that allows you to install
- dependencies into your Elixir runtime! This approach is especially useful when sharing notebooks
- because everyone will be able to get the same dependencies. Let's try this out:
-
- **Note:** compiling dependencies may use a reasonable amount of memory. If you are
- hosting Livebook, make sure you have enough memory allocated to the Livebook
- instance, otherwise the command below will fail.
-
- ```elixir
- Mix.install([
- {:jason, "~> 1.2"}
- ])
- ```
-
- ```elixir
- %{elixir: "is awesome"}
- |> Jason.encode!()
- |> IO.puts()
- ```
-
- It is a good idea to specify versions of the installed packages,
- so that the notebook is easily reproducible later on.
-
- Also keep in mind that `Mix.install/2` can be called only once
- per runtime, so if you need to modify the dependencies, you should
- go to the notebook runtime configuration and **reconnect** the current runtime.
-
- ## Running tests
-
- It is also possible to run tests directly from your notebooks.
- The key is to disable `ExUnit`'s autorun feature and then explicitly
- run the test suite after all test cases have been defined:
-
- ```elixir
- ExUnit.start(autorun: false)
-
- defmodule MyTest do
- use ExUnit.Case, async: true
-
- test "it works" do
- assert true
- end
- end
-
- ExUnit.run()
- ```
-
- ## Math
-
- Livebook uses $\TeX$ syntax for math.
- It supports both inline math like $e^{\\pi i} + 1 = 0$, as well as display math:
-
- $$
- S(x) = \\frac{1}{1 + e^{-x}} = \\frac{e^{x}}{e^{x} + 1}
- $$
-
- You can explore all supported expressions [here](https://katex.org/docs/supported.html).
-
- ## Stepping up your workflow
-
- Once you start using notebooks more, it's gonna be beneficial
- to optimise how you move around. Livebook leverages the concept of
- **navigation**/**insert** modes and offers many shortcuts for common operations.
- Make sure to check out the shortcuts by clicking the "Keyboard" icon in
- the sidebar or by typing `?`.
-
- ## Final notes
-
- Livebook is an open source project, so feel free to look into
- [the repository](https://github.com/elixir-nx/livebook)
- to contribute, report bugs, suggest features or just skim over the codebase.
-
- Now go ahead and build something cool! 🚢
- '''
-
- {notebook, []} = Livebook.LiveMarkdown.Import.notebook_from_markdown(livemd)
-
- @notebook notebook
-
- def new() do
- @notebook
- end
-end
diff --git a/lib/livebook_web/live/explore_live.ex b/lib/livebook_web/live/explore_live.ex
new file mode 100644
index 000000000..4826e97a7
--- /dev/null
+++ b/lib/livebook_web/live/explore_live.ex
@@ -0,0 +1,108 @@
+defmodule LivebookWeb.ExploreLive do
+ use LivebookWeb, :live_view
+
+ import LivebookWeb.UserHelpers
+ import LivebookWeb.SessionHelpers
+
+ alias Livebook.Notebook.Explore
+
+ @impl true
+ def mount(_params, %{"current_user_id" => current_user_id}, socket) do
+ if connected?(socket) do
+ Phoenix.PubSub.subscribe(Livebook.PubSub, "users:#{current_user_id}")
+ end
+
+ current_user = build_current_user(current_user_id, socket)
+
+ [lead_notebook_info | notebook_infos] = Explore.notebook_infos()
+
+ {:ok,
+ assign(socket,
+ current_user: current_user,
+ lead_notebook_info: lead_notebook_info,
+ notebook_infos: notebook_infos
+ )}
+ end
+
+ @impl true
+ def render(assigns) do
+ ~L"""
+
+ <%= live_patch to: Routes.home_path(@socket, :page),
+ class: "hidden md:block absolute top-[50%] left-[-12px] transform -translate-y-1/2 -translate-x-full" do %>
+ <%= remix_icon("arrow-left-line", class: "text-2xl align-middle") %>
+ <% end %>
+
+ Explore
+
+
+
+ Check out a number of examples showcasing various parts of the Elixir ecosystem.
+ Click on any notebook you like and start playing around with it!
+