fmegahed commited on
Commit
2bbfee1
·
1 Parent(s): da74468

Updated querychat app with bslib

Browse files
Files changed (4) hide show
  1. .claude/settings.local.json +13 -0
  2. .gitignore +10 -0
  3. Dockerfile +1 -0
  4. app.R +230 -27
.claude/settings.local.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "mcp__r-btw__btw_tool_docs_package_help_topics",
5
+ "mcp__r-btw__btw_tool_docs_available_vignettes",
6
+ "mcp__r-btw__btw_tool_session_check_package_installed",
7
+ "mcp__r-btw__btw_tool_docs_help_page",
8
+ "mcp__r-btw__btw_tool_docs_vignette",
9
+ "mcp__r-btw__btw_tool_search_packages",
10
+ "mcp__r-btw__btw_tool_env_describe_data_frame"
11
+ ]
12
+ }
13
+ }
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .Rproj.user
2
+ .Rhistory
3
+ .RData
4
+ .Ruserdata
5
+ .Renviron
6
+
7
+ CLAUDE.md
8
+ *.Rproj
9
+
10
+ nul
Dockerfile CHANGED
@@ -18,6 +18,7 @@ RUN R -e "remotes::install_version('bslib', version = '0.10.0', repos = 'https:/
18
  RUN R -e "remotes::install_version('DT', version = '0.34.0', repos = 'https://cloud.r-project.org')"
19
  RUN R -e "remotes::install_version('dplyr', version = '1.2.0', repos = 'https://cloud.r-project.org')"
20
  RUN R -e "remotes::install_version('RSQLite', version = '2.4.5', repos = 'https://cloud.r-project.org')"
 
21
 
22
  # Install r-universe packages with pinned versions (querychat and dependencies)
23
  RUN R -e "remotes::install_version('ellmer', version = '0.4.0', repos = c('https://posit-dev.r-universe.dev', 'https://cloud.r-project.org'))"
 
18
  RUN R -e "remotes::install_version('DT', version = '0.34.0', repos = 'https://cloud.r-project.org')"
19
  RUN R -e "remotes::install_version('dplyr', version = '1.2.0', repos = 'https://cloud.r-project.org')"
20
  RUN R -e "remotes::install_version('RSQLite', version = '2.4.5', repos = 'https://cloud.r-project.org')"
21
+ RUN R -e "remotes::install_version('bsicons', version = '0.1.2', repos = 'https://cloud.r-project.org')"
22
 
23
  # Install r-universe packages with pinned versions (querychat and dependencies)
24
  RUN R -e "remotes::install_version('ellmer', version = '0.4.0', repos = c('https://posit-dev.r-universe.dev', 'https://cloud.r-project.org'))"
app.R CHANGED
@@ -1,55 +1,258 @@
1
  # ISA 401 OEWS Jobs Explorer
2
  # A Shiny app for exploring U.S. Occupational Employment and Wage Statistics
 
3
 
4
  library(shiny)
5
- library(bslib)
6
- library(querychat)
7
- library(DT)
8
- library(dplyr)
9
- library(RSQLite)
10
 
11
- # Load preprocessed data
 
 
 
12
  oews <- readRDS("data/oews.rds")
13
 
14
- # Step 1: Create QueryChat object
15
- client = ellmer::chat_openai(
 
 
 
 
16
  model = "gpt-5-mini-2025-08-07",
17
- credentials = function(){return(Sys.getenv("OPENAI_API_KEY"))}
18
  )
19
 
20
- qc = querychat::querychat(
21
  oews,
22
  client = client,
23
  greeting = "Welcome to Our ISA 401 Assistant for Understanding the OEWS Dataset",
24
  extra_instructions = "data/extra_instructions.md",
25
- data_description = 'data/data_desc.md'
26
  )
27
 
28
- ## Based on the documentation:
29
- # https://posit-dev.github.io/querychat/r/articles/build.html#programmatic-filtering
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- # Step 2: Add UI component
32
- ui <- bslib::page_sidebar(
33
- sidebar = qc$sidebar(),
34
- bslib::card(
35
- bslib::card_header("Data Table"),
36
- DT::DTOutput("table")
 
 
 
 
 
 
 
 
 
37
  ),
38
- bslib::card(
39
- fill = FALSE,
40
- bslib::card_header("SQL Query"),
41
- shiny::verbatimTextOutput("sql")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  )
43
  )
44
 
45
- # Step 3: Use reactive values in server
 
 
 
46
  server <- function(input, output, session) {
47
  qc_vals <- qc$server()
48
-
 
 
 
 
 
 
 
 
 
49
  output$table <- DT::renderDT({
50
- DT::datatable(qc_vals$df(), fillContainer = TRUE)
 
 
 
 
51
  })
52
-
53
  output$sql <- shiny::renderText({
54
  qc_vals$sql() %||% "SELECT * FROM oews"
55
  })
 
1
  # ISA 401 OEWS Jobs Explorer
2
  # A Shiny app for exploring U.S. Occupational Employment and Wage Statistics
3
+ # Built with querychat for natural language data querying
4
 
5
  library(shiny)
 
 
 
 
 
6
 
7
+ # ---------------------------------------------------------------------------
8
+ # Data & QueryChat setup
9
+ # ---------------------------------------------------------------------------
10
+
11
  oews <- readRDS("data/oews.rds")
12
 
13
+ # Pre-compute static stats for the Home tab
14
+ n_occupations <- length(unique(oews$occ_title[oews$o_group == "detailed"]))
15
+ n_areas <- length(unique(oews$area_title))
16
+ data_period <- "May 2024"
17
+
18
+ client <- ellmer::chat_openai(
19
  model = "gpt-5-mini-2025-08-07",
20
+ credentials = function() { return(Sys.getenv("OPENAI_API_KEY")) }
21
  )
22
 
23
+ qc <- querychat::querychat(
24
  oews,
25
  client = client,
26
  greeting = "Welcome to Our ISA 401 Assistant for Understanding the OEWS Dataset",
27
  extra_instructions = "data/extra_instructions.md",
28
+ data_description = "data/data_desc.md"
29
  )
30
 
31
+ # ---------------------------------------------------------------------------
32
+ # UI
33
+ # ---------------------------------------------------------------------------
34
+
35
+ ui <- bslib::page_navbar(
36
+ id = "navbar",
37
+ title = "OEWS Jobs Explorer",
38
+ theme = bslib::bs_theme(primary = "#C3142D"),
39
+ navbar_options = bslib::navbar_options(bg = "#C3142D"),
40
+ fillable = "Explorer",
41
+
42
+ sidebar = bslib::sidebar(
43
+ id = "sidebar",
44
+ width = 325,
45
+ shiny::conditionalPanel(
46
+ "input.navbar == 'Explorer'",
47
+ qc$ui()
48
+ ),
49
+ shiny::conditionalPanel(
50
+ "input.navbar == 'Home'",
51
+ tags$div(
52
+ class = "p-2",
53
+ tags$h5("Welcome!"),
54
+ tags$p("Use the tabs above to explore the data. The ",
55
+ tags$strong("Explorer"), " tab lets you query the OEWS dataset
56
+ using natural language."),
57
+ tags$hr(),
58
+ tags$p("Try asking things like:"),
59
+ tags$ul(
60
+ tags$li("Show me the top 10 highest paying occupations"),
61
+ tags$li("Filter to software developers in California"),
62
+ tags$li("What are the jobs with more than 1 million employees?")
63
+ )
64
+ )
65
+ ),
66
+ fillable = TRUE
67
+ ),
68
+
69
+ # ---------- Home Tab ----------
70
+ bslib::nav_panel(
71
+ title = "Home",
72
+ value = "Home",
73
+ icon = bsicons::bs_icon("house"),
74
+
75
+ # Hero card
76
+ bslib::card(
77
+ class = "bg-dark text-white border-0",
78
+ style = "background: linear-gradient(135deg, #C3142D 0%, #8B0E20 100%);",
79
+ bslib::card_body(
80
+ class = "text-center py-5",
81
+ tags$h1("OEWS Jobs Explorer", class = "display-4 fw-bold"),
82
+ tags$p(
83
+ class = "lead mt-3 mb-4",
84
+ "Explore U.S. Occupational Employment and Wage Statistics using natural language queries powered by AI"
85
+ ),
86
+ tags$p(class = "text-white-50 mb-4", "A Business Intelligence and Data Visualization Assignment for ISA 401 at Miami University"),
87
+ shiny::actionButton(
88
+ "go_explorer", "Start Exploring",
89
+ class = "btn btn-outline-light btn-lg px-4",
90
+ icon = shiny::icon("magnifying-glass")
91
+ )
92
+ )
93
+ ),
94
+
95
+ # App metadata row
96
+ bslib::layout_column_wrap(
97
+ width = 1 / 4,
98
+ fill = FALSE,
99
+ bslib::value_box(
100
+ title = "Version",
101
+ value = "0.1.0",
102
+ showcase = bsicons::bs_icon("tag"),
103
+ theme = "light"
104
+ ),
105
+ bslib::value_box(
106
+ title = "Last Updated",
107
+ value = "Feb 2026",
108
+ showcase = bsicons::bs_icon("calendar-check"),
109
+ theme = "light"
110
+ ),
111
+ bslib::value_box(
112
+ title = "By",
113
+ value = tags$span("Fadel M. Megahed", style = "font-size: 1em;"),
114
+ p("Miami University"),
115
+ showcase = bsicons::bs_icon("person"),
116
+ theme = "light"
117
+ ),
118
+ bslib::value_box(
119
+ title = "Data Period",
120
+ value = data_period,
121
+ p(format(n_occupations, big.mark = ","), " occupations \u00B7 ",
122
+ format(n_areas, big.mark = ","), " areas"),
123
+ showcase = bsicons::bs_icon("bar-chart-line"),
124
+ theme = "light"
125
+ )
126
+ ),
127
+
128
+ # Developer / About + Tutorial row
129
+ bslib::layout_columns(
130
+ col_widths = c(6, 6),
131
+ fill = FALSE,
132
+
133
+ bslib::card(
134
+ bslib::card_header(class = "fw-bold", bsicons::bs_icon("info-circle"), " About"),
135
+ bslib::card_body(
136
+ tags$p(
137
+ "This app uses the ",
138
+ tags$a("querychat", href = "https://posit-dev.github.io/querychat/",
139
+ target = "_blank"),
140
+ " R package to let you query the ",
141
+ tags$strong("OEWS May 2024"),
142
+ " dataset from the ",
143
+ tags$a("Bureau of Labor Statistics",
144
+ href = "https://www.bls.gov/oes/tables.htm", target = "_blank"),
145
+ " using plain English. Under the hood, your questions are translated
146
+ to SQL by an OpenAI language model."
147
+ ),
148
+ tags$hr(),
149
+ tags$h6(class = "fw-bold mb-2", "Developer"),
150
+ tags$div(
151
+ class = "d-flex align-items-start gap-3",
152
+ bsicons::bs_icon("person-circle", size = "2em"),
153
+ tags$div(
154
+ tags$div(class = "fw-semibold", "Fadel M. Megahed"),
155
+ tags$small(class = "text-muted d-block",
156
+ "Raymond E. Glos Professor, Farmer School of Business"),
157
+ tags$small(class = "text-muted d-block mb-2", "Miami University"),
158
+ tags$div(
159
+ class = "d-flex flex-wrap gap-2",
160
+ tags$a(class = "btn btn-outline-secondary btn-sm",
161
+ href = "mailto:fmegahed@miamioh.edu",
162
+ bsicons::bs_icon("envelope"), " Email"),
163
+ tags$a(class = "btn btn-outline-secondary btn-sm",
164
+ href = "https://www.linkedin.com/in/fmegahed/",
165
+ target = "_blank",
166
+ bsicons::bs_icon("linkedin"), " LinkedIn"),
167
+ tags$a(class = "btn btn-outline-secondary btn-sm",
168
+ href = "https://miamioh.edu/fsb/directory/?up=/directory/megMDef",
169
+ target = "_blank",
170
+ bsicons::bs_icon("globe"), " Website"),
171
+ tags$a(class = "btn btn-outline-secondary btn-sm",
172
+ href = "https://github.com/fmegahed/",
173
+ target = "_blank",
174
+ shiny::icon("github"), " GitHub")
175
+ )
176
+ )
177
+ ),
178
+ tags$hr(),
179
+ tags$h6(class = "fw-bold mb-2", "Data Source"),
180
+ tags$div(
181
+ class = "d-flex flex-wrap gap-2",
182
+ tags$a(class = "btn btn-outline-primary btn-sm",
183
+ href = "https://www.bls.gov/oes/tables.htm", target = "_blank",
184
+ bsicons::bs_icon("table"), " OEWS Tables"),
185
+ tags$a(class = "btn btn-outline-primary btn-sm",
186
+ href = "https://www.bls.gov/oes/special-requests/oesm24all.zip",
187
+ target = "_blank",
188
+ bsicons::bs_icon("download"), " Raw Data (ZIP)")
189
+ )
190
+ )
191
+ ),
192
 
193
+ bslib::card(
194
+ bslib::card_header(class = "fw-bold", bsicons::bs_icon("play-circle"), " Video Tutorial"),
195
+ bslib::card_body(
196
+ fillable = FALSE,
197
+ tags$div(
198
+ class = "ratio ratio-16x9",
199
+ tags$iframe(
200
+ src = "https://www.youtube.com/embed/WHlLehxlzcs",
201
+ allowfullscreen = NA,
202
+ allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
203
+ )
204
+ )
205
+ )
206
+ )
207
+ )
208
  ),
209
+
210
+ # ---------- Explorer Tab ----------
211
+ bslib::nav_panel(
212
+ title = "Explorer",
213
+ value = "Explorer",
214
+ icon = bsicons::bs_icon("search"),
215
+
216
+ bslib::card(
217
+ bslib::card_header(shiny::textOutput("explorer_table_title")),
218
+ DT::DTOutput("table")
219
+ ),
220
+
221
+ bslib::accordion(
222
+ open = FALSE,
223
+ bslib::accordion_panel(
224
+ title = "SQL Query",
225
+ icon = bsicons::bs_icon("code-square"),
226
+ shiny::verbatimTextOutput("sql")
227
+ )
228
+ )
229
  )
230
  )
231
 
232
+ # ---------------------------------------------------------------------------
233
+ # Server
234
+ # ---------------------------------------------------------------------------
235
+
236
  server <- function(input, output, session) {
237
  qc_vals <- qc$server()
238
+
239
+ # Navigate to Explorer tab on button click
240
+ shiny::observeEvent(input$go_explorer, {
241
+ bslib::nav_select("navbar", "Explorer")
242
+ })
243
+
244
+ output$explorer_table_title <- shiny::renderText({
245
+ qc_vals$title() %||% "OEWS Data"
246
+ })
247
+
248
  output$table <- DT::renderDT({
249
+ DT::datatable(
250
+ qc_vals$df(),
251
+ fillContainer = TRUE,
252
+ options = list(scrollX = TRUE, pageLength = 15)
253
+ )
254
  })
255
+
256
  output$sql <- shiny::renderText({
257
  qc_vals$sql() %||% "SELECT * FROM oews"
258
  })