---
title: "Focus Group Analysis"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Focus Group Analysis}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
library(FocusGroup)
```

# Analyze simulated and imported transcripts

How much did each speaker contribute? Which terms and topics distinguished
their turns? How did moderator questions organize the discussion, and where do
readability, recurring themes, or participation patterns differ? FocusGroup
addresses these questions for simulated sessions and imported transcripts.

Descriptive analysis runs offline. Thematic analysis and model summaries are
separate, opt-in model tasks. A live session or requested model analysis uses
LLMR, requires a provider key in its usual environment variable, and receives
an explicit model configuration.

## Run a focus group

```{r create_fg, eval = FALSE}
config <- LLMR::llm_config(
  provider = "openai",
  model = "gpt-4o-mini",
  temperature = 0.7
)

result <- run_focus_group(
  topic = "Impact of Social Media on Mental Health",
  n_participants = 5,
  guide = c(
    Opening = 1,
    Icebreaker = 1,
    Engagement = 2,
    Exploration = 3,
    Closing = 1
  ),
  flow = "desire_based",
  config = config,
  seed = 110,
  message_mode = "roleflip"
)

print(result)
print(result$focus_group)
```

The returned `focus_group_result` has six returned components:
`focus_group`, `transcript`, `summary`, `participants`, `usage`, and
`metadata`.

```{r view_transcript, eval = FALSE}
head(result$transcript)
result$participants
result$usage
```

Each transcript row has a unique `message_id`. `round` identifies the moderator
cycle, so a moderator question and its participant responses share a round.
`phase` retains the guide phase. Generated rows also carry provider response
metadata and token counts; a count the provider did not report is `NA`.

## Run descriptive analysis offline

`analyze_focus_group()` runs descriptive analyses without a model unless the
caller opts in by supplying `config`.

```{r offline_analysis, eval = FALSE}
analysis <- analyze_focus_group(
  result,
  num_topics = 4,
  include_plots = TRUE
)

print(analysis)
analysis$basic_stats
analysis$tfidf
analysis$readability
analysis$issues
```

The `focus_group_analysis` returned components are `basic_stats`, `topics`,
`tfidf`, `readability`, `themes`, `model_summary`, `plots`, and `issues`. A
component that cannot be computed is an empty component with consistent
columns where it is tabular. For example, `plots` is an empty list if ggplot2
is unavailable. `issues` identifies an optional analysis that could not run
and gives its reason.

Individual descriptive analyses remain available as methods on the underlying
`FocusGroup` object:

```{r individual_analysis, eval = FALSE}
readability <- result$focus_group$analyze_readability()
participation <- result$focus_group$analyze_participation_balance()
questions <- result$focus_group$analyze_question_patterns()

readability
participation$participation_stats
questions$question_patterns
```

## Opt in to model analysis

Thematic analysis and model summaries run only when an explicit `config` is
passed to `analyze_focus_group()`.

```{r model_analysis, eval = FALSE}
model_analysis <- analyze_focus_group(
  result,
  num_topics = 4,
  config = config
)

cat(model_analysis$themes)
cat(model_analysis$model_summary)
```

If a requested model analysis fails, the provider condition is raised. The
failure is not converted to a missing field or omitted from the result.

## Import and analyze an existing transcript

Importing a transcript creates no model output. Pass `moderator_id` when the
moderator is known. If it is omitted, a speaker named exactly `mod`,
`moderator`, `facilitator`, or `interviewer` (in any case) is the moderator.

```{r import, eval = FALSE}
transcript <- data.frame(
  speaker = c("Facilitator", "Ana", "Ben", "Ana"),
  text = c(
    "What should change about the library hours?",
    "Evening hours help working parents.",
    "Morning crowding has become difficult.",
    "Both schedules need enough staff."
  )
)

imported <- focus_group_from_transcript(
  transcript,
  topic = "Library hours",
  moderator_id = "Facilitator"
)

imported_analysis <- analyze_focus_group(imported, include_plots = FALSE)
print(imported_analysis)
```

## Summarize a discussion

`summarize_focus_group()` summarizes a session, a set of arms, or an
imported transcript by participant, guide item, disagreement, trajectory,
turn, or the whole session, at one or more of three lengths: `"headline"`,
`"brief"`, and `"detailed"`. The model returns claims as JSON. Each claim
gives its status (`"stated"`, `"implied"`, `"contradictory"`, or
`"not_stated"`), the ids of the messages it rests on, and any words it
quotes. Every claim is checked against the transcript before prose is
rendered from it: the cited messages must exist and be by the speakers the
claim names, each quote must appear in the message it cites, and a claim for
the whole group must cite every participant who spoke.

The chunks below run offline. The scripted runner stands in for the model:
it reads the transcript from the prompt and answers with one claim per
participant, quoting the first five words of that participant's first
message. A live call passes `config` and no runner.

```{r summarize}
transcript <- data.frame(
  speaker = c("Facilitator", "Ana", "Ben", "Ana", "Facilitator", "Ben"),
  text = c(
    "What should change about the library hours?",
    "Evening hours help working parents like me.",
    "I see it differently. Mornings are too crowded now.",
    "Crowded mornings still beat being locked out after work.",
    "What would you give up to pay for longer hours?",
    "I would give up the Sunday story hour before anything else."
  )
)
imported <- focus_group_from_transcript(
  transcript,
  topic = "Library hours",
  moderator_id = "Facilitator"
)

scripted <- function(experiments, ...) {
  prompt <- experiments$messages[[1]][[1]]$content
  unit <- sub(".*one participant, (P[0-9]+)\\..*", "\\1", prompt)
  lines <- regmatches(prompt, gregexpr("\\[#[0-9]+\\] P[0-9]+: [^\n]*", prompt))[[1]]
  own <- lines[grepl(paste0("^\\[#[0-9]+\\] ", unit, ": "), lines)][1]
  id <- as.integer(sub("^\\[#([0-9]+)\\].*", "\\1", own))
  words <- strsplit(sub("^[^:]*: ", "", own), " ")[[1]]
  claim <- list(
    id = 1, unit = unit, kind = "position", status = "stated",
    text = paste(unit, "gave a view on the hours."),
    speakers = list(unit), evidence = list(id), context = list(),
    quotes = list(list(message_id = id, speaker = unit,
                       text = paste(words[1:5], collapse = " ")))
  )
  reply <- list(claims = list(claim),
                levels = list(headline = list(1), brief = list(1)))
  experiments$response_text <- as.character(jsonlite::toJSON(reply, auto_unbox = TRUE))
  experiments
}

summary_config <- LLMR::llm_config("openai", "gpt-4o-mini")
by_person <- summarize_focus_group(
  imported, summary_config,
  compression = c("headline", "brief"),
  .runner = scripted
)
by_person
```

The summarizing model labels the speakers by their handles: Ana and Ben are
labeled P1 and P2, and the facilitator Moderator. Names inside the messages
reach it as written, as the speakers said them. The researcher's columns keep
the names, and each quote is cut from the message as recorded.

```{r summarize_records}
by_person$metadata$handles
by_person$claims[, c("unit", "status", "text")]
by_person$quotes[, c("unit", "message_id", "speaker_id", "verbatim", "start", "end")]
```

A quote passes when it occurs in the cited message once case, whitespace, and
quotation marks are set aside; `verbatim`, `start`, and `end` give it as it
stands in the source. A reply that fails a check is sent back once, with each
failure stated. Here the first reply about P2 misquotes the message:

```{r summarize_reask}
calls <- 0L
misquote_once <- function(experiments, ...) {
  calls <<- calls + 1L
  out <- scripted(experiments, ...)
  if (calls == 2L) {
    out$response_text <- sub("I see it differently.", "I see it quite differently.",
                             out$response_text, fixed = TRUE)
  }
  out
}
reasked <- summarize_focus_group(
  imported, summary_config,
  compression = "headline",
  .runner = misquote_once
)
reasked$calls[, c("call_id", "unit", "attempt")]
cat(reasked$calls$messages[[3]][[3]]$content)
```

When the second reply fails as well, the run stops with an error of class
`focusgroup_summary_check_failure`; its `partial` field holds the units
completed before it, and its `checks` and `replies` fields show what failed.
With `on_fail = "drop"` the failing claims move to `rejected` instead, except
claims of a disagreement, of a position held by one participant alone, or of
a change, which still stop the run.

`by = "item"` sends one guide item at a time, which also keeps a long session
within the prompt window. `by = "turn"` gives the gist of every message.
`by = "trajectory"` follows the discussion through stages fixed in advance:
thirds of the participant turns for the headline, one stage per guide item
for the longer lengths. `by = "disagreements"` lists each disagreement, and
each position held by one participant alone, with how it ended. Private probe
answers reach the model only with `include_probes = TRUE`.

```{r summarize_live, eval = FALSE}
by_item <- summarize_focus_group(
  result, config,
  by = "item",
  compression = c("headline", "detailed")
)
by_item$summaries
```

`fg_turns()` gives the turn table the summaries read, with no model call:
each message's guide item, handle, and length, and how the speaker came to
have the floor. An imported transcript has no routing record, so every link
is `"sequence"`.

```{r turns}
fg_turns(imported)[, c("message_id", "item", "speaker_id", "handle", "words", "link")]
```

## Replay a session

`fg_player()` writes a session to a self-contained HTML file that steps
through it message by message, built from the records the session saved and
without any model call. The package includes a synthetic session made with a
scripted runner, which the chunk below replays; `open = FALSE` keeps the
browser closed while the vignette is built.

```{r player}
session <- readRDS(system.file("extdata", "player-session.rds",
                               package = "FocusGroup"))
page <- fg_player(session, file = tempfile(fileext = ".html"), open = FALSE)
file.exists(page)
table(session$routing$rule)
```

Each speaker selection in `session$routing` appears as a panel before the
turn it chose. Under desire-based selection the panel shows the score of
every participant asked, marks the previous speaker as not asked, and states
whether a tie was drawn at random; a call-on or a participant addressed by
the moderator is marked as such. For a printable page, use `mode = "static"`;
for a study with arms, pass the result of `run_focus_group_arms()` to write
one file per arm.

## Construct the objects directly

The R6 interface exposes the parts assembled by `run_focus_group()`.

```{r direct, eval = FALSE}
agents <- create_agents(
  n_participants = 3,
  demographics = data.frame(
    age = c(25, 35, 45),
    gender = c("male", "female", "non-binary"),
    education = c("high school", "bachelor's", "master's"),
    stringsAsFactors = FALSE
  ),
  config = config
)

flow <- create_conversation_flow(
  mode = "desire_based",
  agents = agents,
  moderator_id = "MOD"
)

fg <- FocusGroup$new(
  topic = "Impact of Social Media on Mental Health",
  purpose = "Explore perspectives and experiences related to social media.",
  agents = agents,
  moderator_id = "MOD",
  turn_taking_flow = flow,
  admin_config = config
)
```

Built-in flows are available only through `create_conversation_flow()`.
`ConversationFlow` is the base class for custom turn-taking rules.

## Interpretation

A generated transcript is evidence about the specified simulation, not about a
population. Analysis should retain the guide, participant records, model
configuration, and message construction recorded with the result.
