There’s a lot of advice out there at the moment about using LLMs for code generation, but something I’ve been curious about is using LLMs for learning around code.
Specifically, I don’t want to rely on LLMs to generate the answers for me every time; I want LLMs to help me gain skills so that next time I can generate the right answer myself.
In this blog post I’m going to talk about using an LLM, specifically, ChatGPT to extend my own knowledge in R.
I’m currently collaborating with Charlotte Hadley on an R package called daisyuiwidget, which is a htmlwidgets package.
Charlie and I both really love htmlwidgets, which are used to wrap JavaScript libraries and work with them from R. They can be used in HTML outputs like Quarto documents and Shiny apps.
One example of a htmlwidgets package is DT, which can be used to create interactive tables. You can see an example below.
library(DT)
<- data.frame(
packages package = c("htmlwidgets", "leaflet", "plotly", "DT"),
first_release = as.Date(c("2014-12-09", "2015-06-24", "2015-11-17", "2015-06-09")
)
)
::datatable(packages) DT
One of the excellent features about packages like DT is that they can go beyond just displaying outputs. If you include a DT table in a Shiny app and have enabled “row selection”, then the JavaScript code for the table can pass the index of the user’s selected row back to R. This then can be used in other components of the app, for example, displaying information relevant to the user’s selection.
The goal with daisyuiwidget is to create a new htmlwidgets package, using LLMs - we’ll be talking more about this in our joint presentation at Shiny in Production later on this year.
We decided to create a wrapper for the daisyUI library which has a feature where it can be used to create timelines like the one below.
The project was at the point where the package successfully created a timeline from a data.frame input. The next task for me to do was implement the functionality for timeline event selection, with the goal that when a user clicks on an event in the timeline, an input variable in R is updated, which then can be hooked up to other bits of Shiny reactivity.
This is what I’ll be discussing below - how I got ChatGPT to help with this, but with helping me learn how to do it myself, and not just getting the LLM to write the code.
I’m going to be talking at Posit Conf about increasing productivity with LLMs and a central thesis of my talk is the idea that LLMs are most useful when working on tasks just outside of your comfort zone, sometimes described as your stretch zone.
I’m defining the “stretch zone” in this context as something that isn’t within your current knowledge, but doesn’t seem so challenging that you have doubts about whether you can find a solution.
This task seemed like a great fit for this in terms of my past experience:
✅ Extensive Shiny app development experience
✅ Experience using row selection in DT-based apps to update other components
✅ Some JavaScript experience
❌ Very limited experience of how to implement htmlwidgets package from scratch
❌ Never implemented Shiny reactivity for UI components before
I’d been reading a paper by Dr Xi Lin about the idea of using LLMs in self-directed learning.
Lin, X. (2023). Exploring the Role of ChatGPT as a Facilitator for Motivating Self-Directed Learning Among Adult Learners. Adult Learning, https://doi.org/10.1177/10451595231184928
Dr Lin discusses a self-directed learning framework proposed by Kim et al (2014) with the following aspects:
This framework can be used with ChatGPT to help support adult learners, and Dr Lin goes on to walk through a hypothetical example where an adult learner is using ChatGPT in each of those steps and discusses the kind of help that could be given.
I wanted to try some of this out, and so I created LLM summaries of each of the steps including Dr Lin’s suggestions of how an LLM might implement them, and used these summaries as prompts in a learning session with ChatGPT. I focused solely on steps 1 to 3 as the learning goal was an individual task rather than an ongoing programme.
Duration: 7 minutes
I wrote a little context about what I was trying to achieve, and pasted that and my summary of step 1, into ChatGPT.
ChatGPT asked me five questions:
It took my answers and generated a primary learning goal with supporting objectives.
Primary Learning Goal:
Within the next few hours, I want to understand and implement the necessary code to make my existing
htmlwidget
timeline respond to user interaction in a Shiny app—specifically, capturing a selected event and updating a variable on the server side.Supporting Objectives:
- Identify how event selection can be captured in JavaScript and passed to Shiny via
Shiny.setInputValue()
.- Modify the widget’s JS code to emit a Shiny input when a user selects an event.
- Test and verify that the input value is received in Shiny and reflects the expected event ID or index.
- Write a minimal working Shiny app that uses the widget and displays the selected event value.
I feel that the value I got here from incorporating ChatGPT was clarity. The resulting learning goal was more precise than my original stated aim, and the supporting objectives made for concrete steps to aim for in pursuit of the overall goal.
Expand to view full conversation
I am looking at how I can get LLMs to help me with a coding task as an intelligent tutor and not just solve the problem for me. My first step is establishing a learning goal or goals. Using the brief below, ask me questions one at a time to help me work out a small set of goals in this exercise.
Establish learning goals
When establishing goals for self-directed learning, it’s most important for learners to set SMART goals: Specific, Measurable, Achievable, Relevant, and Time-bound. This approach helps adult learners identify clear learning objectives and stay focused on achieving them.
Goals should be personalized and align with the learner’s interests, motivations, and expected learning outcomes. It’s crucial to break down broader objectives into smaller, manageable tasks to make them more achievable.
Additionally, learners should consider how their goals relate to their personal and professional contexts, ensuring relevance. Setting a realistic timeline for accomplishing these goals is also essential.
While learners can use tools like ChatGPT to assist in goal-setting, it’s important to remember that the quality of recommendations depends on the information provided by the learner. Therefore, clear communication of interests, motivations, and expectations is crucial when using such tools.
Let’s start by grounding this in why you’re doing it.
Question 1: What’s your motivation for using LLMs as a tutor rather than a problem-solver—what do you hope to gain from this approach (e.g., deeper understanding, confidence, skill retention, etc.)?
I want to understand how I can take an R package which creates a htmlwidget and adds the correct code so that I can use it in a shiny app and update a variable based on what the user has selected. I want to learn how to do this as I think it’s a useful skill to have. I don’t want an LLM to do it as I won’t know if the code is correct or not and would only have surface level understanding and so would be hard to repeat the skill in future.
Question 2:
Where are you starting from? For example:
htmlwidget
package from scratch(Just answer what’s true for you—this is to calibrate the scope of your learning goal.)
I have never written an htmlwidget package from scratch although I do have one I’ve written with an LLM that I’d like to add this functionality to. It creates the widget but doesn’t have the shiny functionality yet. I’ve not done much of this connectivity before, though have worked with packages that implement it themselves.
That gives us a baseline. You’ve got a partial htmlwidget
and prior exposure to packages that do this right, but you haven’t yet wired up JS-to-R reactivity yourself.
Question 3:
What exactly do you want the user to select in your widget, and what kind of R-side variable or behavior do you want to update in response? (Try to be concrete—e.g., “user clicks on a bar in a chart, I want to update a reactiveVal()
in the server with the bar’s label.”)
So the widget display a timeline of events and I want the user to select an event, and some variable updates. I’ve seen similar functionality in the DT package where the user clicks on a row in a table, and some variable tracks which row is selected and the data from that row. I can’t remember exactly what data it tracks, it could just be the row number and then the user can write a function to extract the data from an underlying data.frame; this is fine too.
Perfect—this is concrete enough.
You want to let the user select an event from a timeline displayed via your htmlwidget
, and capture that selection in the Shiny server logic—ideally as an index or ID you can work with to retrieve more data.
Question 4:
What’s your ideal end state for this learning exercise? For example:
It’s about getting this one working, although I would like to be able to do it again if I ever make another htmlwidgets package.
Got it. So the primary goal is getting this specific widget working, and a secondary goal is gaining enough understanding to repeat the pattern if needed.
Question 5:
What’s your time horizon? Do you want to finish this within a day, a week, or are you treating it as an open-ended background task?
I’d like to finish this within a few hours and not overcomplicate it
Good. A short, focused goal with an emphasis on practical understanding.
Here’s a draft of your SMART-aligned learning goals based on what you’ve said:
Primary Learning Goal:
Within the next few hours, I want to understand and implement the necessary code to make my existing htmlwidget
timeline respond to user interaction in a Shiny app—specifically, capturing a selected event and updating a variable on the server side.
Supporting Objectives:
Shiny.setInputValue()
.Do you want to adjust or narrow any part of that before we move to designing how the LLM will help you stay in tutor mode for this?
Duration: 11 minutes
The second stage was identifying resources for learning.
It started smoothly - ChatGPT asked me if I preferred to learn by reading, watching, or doing, and when I replied “doing” it assured me that it would find code-first resources with working examples - great!
It gave me some options about kinds of examples I was interested in, and asked which htmlwidgets packages I was more familiar with so we could use those as examples to look at. At this point it suggested some initial links, but 2 out of the 3 didn’t actually work.
When I complained about this, it gave me some code examples to try out, which I pushed back on, stating I wanted resources to learn from and not ChatGPT-generated code,
At this point, it gave me a list of resources, divided into categories and a summary of what it had provided. The links supplied here did all work this time, but there was too much information, and a real risk of overwhelm.
I began to skim through a few articles, before I remembered that the aim of this step was only to identify resources, and not to deep dive into them quite yet, and so instead I moved onto the next step.
Expand to view full conversation
Now the next thing I need to do it find learning resources. Here are the notes I have.
When it comes to locating and accessing resources for self-directed learning, the most important things to know are:
Adult learners often struggle with finding available resources, particularly interactive and multimedia materials. This challenge can be a significant barrier to effective self-directed learning.
Learners should proactively identify a variety of resources beyond those provided by instructors. These can include online articles, videos, multimedia resources, books, journals, publications, blogs, and social media groups related to their learning objectives.
Tools like ChatGPT can assist in recommending relevant external resources based on personalized learning plans. However, learners should be cautious and verify the information provided by such tools.
It’s crucial to develop skills in searching for and accessing information, as insufficient technical skills can hinder this process. Improving these skills is an important part of self-directed learning.
Let’s approach this systematically.
Question 1:
Do you learn better by reading, watching, or doing?
(That’ll help decide whether to focus on docs, code examples, videos, interactive tutorials, etc.)
I learn best by doing
Great—so you’ll want code-first resources, ideally with small working examples you can tinker with.
Question 2:
What kind of prior art do you want to learn from? For example:
htmlwidgets
that support Shiny inputs (like DT
, leaflet
, plotly
)?htmlwidgets
?Shiny.setInputValue()
?Items 1 and 3 on your list look good!
Good. Then your resource categories are:
Shiny.setInputValue()
inside htmlwidgetsLet’s start with category 1.
Question 3:
Pick one or two widgets you’ve used or are familiar with—ideally ones where clicking/selecting something in the widget updates a Shiny input. (e.g., DT::datatable
, plotly::plotlyOutput
). Which ones should we dissect?
I’m mostly familiar with those two you mention, more so DT then plotly tho
Perfect. DT
is a solid choice—it’s well-documented, simple enough to follow, and has clean Shiny integration.
Here’s what I suggest for Category 1 (Prior art):
DT
GitHub repo – especially inst/htmlwidgets/datatables.js
and how it calls Shiny.setInputValue()
when rows are selected.plotly
– check srcjs/
for how it emits click or hover events to Shiny. Slightly more complex but good for broader perspective.Now for Category 3 (Targeted examples of Shiny.setInputValue()
):
Want me to fetch code excerpts or links for any of those right now? Or would you rather search through them yourself and come back with questions?
The Dean Attali post looks great and all, but the other two guides links don’t work.
You’re leaning into learning by doing, which means you need hands‑on code examples where you can modify and test. That’s the right approach.
Here are solid, working resources for your two resource types:
Dean Attali’s “htmlwidgets tips” post explains how timevis
uses JavaScript to detect item selection and sends it to Shiny:
timeline.on('select', properties => {
Shiny.onInputChange(elementId + "_selected", properties.items);
});
This snippet, wrapped in a check for HTMLWidgets.shinyMode
, ensures that Shiny receives the selected IDs via the widget’s container ID (JavaScript for R, Dean Attali).
Shiny.setInputValue()
or .onInputChange()
Communicating with Shiny via JavaScript (Posit article) covers sending values directly from JS to R using Shiny.setInputValue(id, value);
. It’s the modern and preferred method (older Shiny.onInputChange()
is an alias, but can still work) (Shiny).
RyouReady blog post offers a minimal example showing an HTML div
with an onclick handler that sends a random number to R via Shiny.onInputChange("mydata", number)
and displays it reactively server-side (“R” you ready?).
Resource | Why it helps | Focus on | ||||
Dean Attali (“htmlwidgets tips”) | Real widget code invoking JS event + input change | timeline.on('select', ...) and chaining to Shiny |
||||
Posit “Communicating with JS” | Modern API and rationale | Shiny.setInputValue(...) inside a listener |
||||
RyouReady blog | Minimal standalone JS ↔︎ R demo | Using Shiny.onInputChange in UI and reading input$mydata in server |
||||
Pick one real widget example (like timevis or similar) and one minimal demo. Then:
Review the JS code where selection events are handled and Shiny.setInputValue()
(or .onInputChange()
) is called.
Modify your widget JS similarly—hook into user’s event selection and send the event ID (or index) via Shiny.setInputValue()
.
Add reactive code in your server to observe input$<widgetId>_selected
and log or display it.
Build a minimal working Shiny app with your widget and observe whether it captures user clicks.
Want me to pull that specific “page code” from the timevis
repo or the Attali post so you can step through it?
Wait, can you find me more resources? I don’t want to learn from your summaries
Here’s a curated list of solid, hands‑on resources to go beyond summaries and dive into real working code you can adapt:
Dean Attali — “How to write a useful htmlwidgets in R: tips and walk‑through a real example”
Deals directly with timevis and how it handles events: it includes a live example of passing selections back to Shiny via JavaScript callbacks CRAN+8Dean Attali+8Stack Overflow+8Shiny+2r4ds.github.io+2engineering-shiny.org+2.
“Widgets with Shiny” ‑ JavaScript for R book (Chapter 17)
Covers patterns for embedding event handlers in widget JS and using Shiny.setInputValue()
in the context of htmlwidgets JavaScript for Rdatanovia+3engineering-shiny.org+3Stack Overflow+3.
Shiny.setInputValue()
Posit article — “Communicating with Shiny via JavaScript”
Concise and modern: shows core usage of Shiny.setInputValue(id, value)
and the optional {priority: "event"}
flag to treat values as events rstudio.github.io+7Shiny+7r4ds.github.io+7.
“How to build a JavaScript‑based widget” by Posit
Builds an example widget using D3/C3 and includes code on client‑server communication with Shiny (including setInputValue
) Shiny+1JavaScript for R+1.
R for Data Science Bookclub: “JavaScript ↔︎ Shiny Communication”
Offers compact code snippets showing how to send data back using Shiny.setInputValue
and receive it in R with reactive observers htmlwidgets.org+15r4ds.github.io+15datanovia+15.
Datanovia article — “JavaScript Integration and Custom Functionality: Extend Shiny Beyond R” (May 2025)
A recent and comprehensive guide to advanced interaction patterns, custom bindings, and message passing between JS and Shiny JavaScript for R+2datanovia+2CRAN+2.
SO Example — Reactable row click setting input value in Shiny modules
Shows how onClick
handlers call Shiny.setInputValue()
to pass selected row data into R, along with observeEvent responses Stack Overflow+1engineering-shiny.org+1.
SO thread — Reading selection from radialNetworkR htmlwidget in Shiny
Illustrates a real-world case where someone needed to capture user selection in a custom widget, and references using Tip 7a from Dean’s blog for guidance search.r-project.org+10Stack Overflow+10nielsenmark.us+10.
Resource | Use Case | What you’ll get |
---|---|---|
Dean Attali (timevis code) | Real-world widget code | Understand how JS event to Shiny input is implemented |
Posit JS‑Shiny tutorial | Core API | Clean examples of Shiny.setInputValue() usage |
JS for R book (Chapter 17) | In‑widget integration | Full example of wiring JS and htmlwidgets with Shiny |
Datanovia (May 2025) | Advanced patterns | Modern perspectives on custom input and messaging |
SO posts (Reactable, radialNetworkR) | Minimal demos | Concrete handlers showing event → R logic |
Duration: 36 minutes
The third and final step was the most useful by far!
Once again, I pasted in the guide to this step, and ChatGPT created a plan for me containing four items:
This was actually pretty impressive; each step contained a goal, the steps required to achieve it, suggestions for how an LLM could help, and a timebox of how long to spend on each activity.
Here’s the first activity suggested:
🔍 1. Reverse-engineer one working widget
Goal: Understand how an existing widget (e.g.
DT
) captures selection and sends it to Shiny.Activity:
Open the DT GitHub repo.
Locate where
Shiny.setInputValue()
is used in the JS files (inst/htmlwidgets/datatables.js
).Make brief notes:
- What JS event triggers the update?
- How is the input ID constructed?
- What data is passed to Shiny?
LLM Role:
- Ask ChatGPT to walk you through unfamiliar lines or patterns (e.g. “What’s this event handler doing?” or “What does this argument to
setInputValue
mean?”).Timebox: 20–30 minutes.
I walked through each activity, setting a timer on my phone. I found that I completed each in less than the allotted time. I skipped the last activity, as I only required 1-3 to complete the task.
Expand to view learning plan
OK, my next step is below
Adopting and executing learning activities is a crucial task in self-directed learning. This step involves developing a specific plan of action that aligns with established goals and available resources.
Learners should create tailored plans that consider their preferred learning styles and available time for learning. This personalization helps ensure the learning activities are effective and manageable.
Time-management techniques, such as spaced repetition and active recall, are important to incorporate into the learning plan. These strategies can help learners stay on track and optimize their learning process.
Learning activities should be diverse and match the learner’s preferred style. This might include visual aids, podcasts, reading materials, or hands-on activities, depending on whether the learner is visual, auditory, reading/writing, or kinesthetic.
Supplementing core learning activities with additional resources, such as online workshops, can enhance the learning experience and provide different perspectives on the subject matter.
Goal: Understand how an existing widget (e.g. DT
) captures selection and sends it to Shiny.
Activity:
Open the DT GitHub repo.
Locate where Shiny.setInputValue()
is used in the JS files (inst/htmlwidgets/datatables.js
).
Make brief notes:
LLM Role:
setInputValue
mean?”).Timebox: 20–30 minutes.
Goal: Create a toy Shiny app where JS sends a value to R.
Activity:
div
or button that triggers Shiny.setInputValue()
in a browser console or inline JS.LLM Role:
Timebox: 20 minutes.
Goal: Modify your widget to track one user interaction and send something to Shiny.
Activity:
Shiny.setInputValue()
.input$yourwidget_selected
or similar.LLM Role:
Timebox: 45–60 minutes.
Goal: End-to-end test: selection in your timeline → reactive output in Shiny.
Activity:
reactivePrint()
, observeEvent()
, or renderText()
to display the selected item.LLM Role:
Timebox: 30 minutes.
Come back and:
You run the show. Use me like a senior dev standing over your shoulder:
Want a markdown checklist version of this plan to copy into your notes?
There was a lot of interaction between me and ChatGPT here, and I’m not going to paste it all, but essentially, during the time I used it to:
In the last example, ChatGPT didn’t just tell me where to put the code, but walked through the important concepts to reason through the answer. It then asked me questions so that I could work out where in the code the change needed to go. Here’s a snippet from that interaction.
🤔 So… where do you want interactivity?
Let me ask you this:
Out of everything that’s being created here, what part of the DOM would the user actually click on if they were selecting a timeline event?
Once we know that, we can talk about how and where to attach the event handler.
I think they’d click on the timeline-box element
Exactly—that’s the actionable node.
That means you’ll want to attach a JS event handler (e.g.
.onclick
or.addEventListener('click', ...)
) to each.timeline-box
as it’s created.Now a follow-up:
When you’re looping over x.events, what info do you need to send back to Shiny when a .timeline-box is clicked? (Think: what identifier, value, or payload do you want to capture?)
We carried on the back and forth like this for a while, with ChatGPT asking me questions to guide me towards the solution. If I got an answer wrong, it replied with an encouraging tone and explained why I was incorrect, and the specific piece of knowledge I was missing.
Eventually, we go to the end of our interaction.
It’s working!!
Excellent. That means your widget is now:
- Rendering cleanly ✅
- Sending input to Shiny ✅
- Updating on click ✅
You’ve now got the full JS → R feedback loop working in your custom
htmlwidget
.
If you want to improve it later, next steps could include:
- Making the selected element visually highlighted (add/remove a CSS class)
- Sending richer data to Shiny (e.g. event ID, label, timestamp)
- Supporting multiple selection or deselection
- Writing a unit test or snapshot test for the widget rendering
But honestly—you’ve just crossed the hardest barrier. Nice work.
Anything else you want to do with it right now?
It sounds like a lot, but this whole experience took around 1 hour from end to end which I found pretty impressive.
Here is the final PR: https://github.com/thisisnic/daisyuiwidget/pull/11. It’s not particularly complicated, but in the hour it took to write, I learned about some of the internals of the DT package, created a standalone example of sending information between JavaScript and R, and then implemented this in the package.
What made this work wasn’t that ChatGPT giving me the right code but that it helped me ask the right questions. It acted like a more senior engineer, talking me through unfamiliar concepts, helping me debug my code, and nudging me towards the right mental model.
It helped reduce context-switching and help me stay in flow. I didn’t need to spend time refining my overall goal, deciding what steps to take, and skim through partially relevant resources. By instead being able to focusing solely on writing the code, it prevented the cognitive fatigue of having to switch between “coding” and “planning” mindsets.
Could I have done it without the LLM? Yes, but it would have taken longer, and I would have learned less.
Although I was using an LLM, I was an active participant in my own learning and so felt empowered as a learner. ChatGPT provided scaffolding and support but not all of the answers.
A reasonable next step to develop this idea further would be to refine the prompts for each stage - the prompts I included were just summaries of the snippets of the article, rather than specific instructions for ChatGPT. In small tasks like this, I’d treat information gathering as part of the task, and collapse steps 2 and 3.
I found this process really rewarding to try out, and I’d encourage others to give it a go. Let me know how you get on if you do!