Spaces:
Sleeping
Sleeping
Update app.R
Browse files
app.R
CHANGED
|
@@ -1,58 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
library(shiny)
|
| 2 |
library(bslib)
|
| 3 |
-
library(
|
| 4 |
-
library(
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
title = "
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
sidebar = sidebar(
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
),
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
),
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
)
|
| 26 |
|
|
|
|
| 27 |
server <- function(input, output, session) {
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
)
|
| 43 |
-
|
| 44 |
-
if (input$show_margins) {
|
| 45 |
-
margin_type <- if (input$by_species) "density" else "histogram"
|
| 46 |
-
p <- p |> ggExtra::ggMarginal(
|
| 47 |
-
type = margin_type, margins = "both",
|
| 48 |
-
size = 8, groupColour = input$by_species, groupFill = input$by_species
|
| 49 |
-
)
|
| 50 |
}
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
)
|
| 56 |
}
|
| 57 |
|
| 58 |
-
shinyApp(ui, server)
|
|
|
|
| 1 |
+
# install.packages("shinyWidgets")
|
| 2 |
+
# install.packages("shinycssloaders")
|
| 3 |
+
# install.packages("bslib")
|
| 4 |
+
# install.packages("plotly")
|
| 5 |
+
|
| 6 |
library(shiny)
|
| 7 |
library(bslib)
|
| 8 |
+
library(tidyverse)
|
| 9 |
+
library(DT)
|
| 10 |
+
library(plotly)
|
| 11 |
+
library(shinyWidgets)
|
| 12 |
+
library(shinycssloaders)
|
| 13 |
|
| 14 |
+
# --- カスタムCSS(LINE風の配色とカードデザイン) ---
|
| 15 |
+
custom_css <- "
|
| 16 |
+
.main-title { color: #06C755; font-weight: bold; margin-bottom: 20px; }
|
| 17 |
+
.card { border-radius: 15px; border: none; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
|
| 18 |
+
.btn-primary { background-color: #06C755; border: none; }
|
| 19 |
+
.btn-primary:hover { background-color: #05a346; }
|
| 20 |
+
.nav-tabs .nav-link.active { border-bottom: 3px solid #06C755 !important; color: #06C755 !important; }
|
| 21 |
+
"
|
| 22 |
|
| 23 |
+
# --- UI ---
|
| 24 |
+
ui <- page_navbar(
|
| 25 |
+
title = span("Quali Fit トーク履歴解析", class = "main-title"),
|
| 26 |
+
theme = bs_theme(version = 5, bootswatch = "flatly", primary = "#06C755"),
|
| 27 |
+
header = tags$head(tags$style(custom_css)),
|
| 28 |
+
|
| 29 |
+
# サイドバー(設定・アップロード)
|
| 30 |
sidebar = sidebar(
|
| 31 |
+
title = "操作パネル",
|
| 32 |
+
fileInput("file1", "履歴ファイルをアップロード", accept = c(".csv", ".txt")),
|
| 33 |
+
hr(),
|
| 34 |
+
uiOutput("filter_ui"),
|
| 35 |
+
hr(),
|
| 36 |
+
downloadButton("downloadData", "CSVでエクスポート", class = "btn-primary w-100")
|
| 37 |
+
),
|
| 38 |
+
|
| 39 |
+
# メインパネル(タブ構成)
|
| 40 |
+
nav_panel("📊 ダッシュボード",
|
| 41 |
+
layout_column_wrap(
|
| 42 |
+
width = 1/3,
|
| 43 |
+
value_box(
|
| 44 |
+
title = "総メッセージ数",
|
| 45 |
+
value = textOutput("total_msg"),
|
| 46 |
+
showcase = icon("comment-dots")
|
| 47 |
+
),
|
| 48 |
+
value_box(
|
| 49 |
+
title = "アクティブ人数",
|
| 50 |
+
value = textOutput("total_members"),
|
| 51 |
+
showcase = icon("users")
|
| 52 |
+
),
|
| 53 |
+
value_box(
|
| 54 |
+
title = "最多発言者",
|
| 55 |
+
value = textOutput("top_sender"),
|
| 56 |
+
showcase = icon("award")
|
| 57 |
+
)
|
| 58 |
+
),
|
| 59 |
+
layout_column_wrap(
|
| 60 |
+
width = 1/2,
|
| 61 |
+
card(card_header("発言者別の割合"), withSpinner(plotlyOutput("piePlot"))),
|
| 62 |
+
card(card_header("時系列のアクティビティ"), withSpinner(plotlyOutput("timePlot")))
|
| 63 |
+
)
|
| 64 |
+
),
|
| 65 |
+
|
| 66 |
+
nav_panel("💬 トーク閲覧",
|
| 67 |
+
card(
|
| 68 |
+
full_screen = TRUE,
|
| 69 |
+
card_header("メッセージ履歴"),
|
| 70 |
+
DTOutput("table")
|
| 71 |
+
)
|
| 72 |
),
|
| 73 |
+
|
| 74 |
+
nav_panel("ℹ️ 使い方",
|
| 75 |
+
card(
|
| 76 |
+
markdown("
|
| 77 |
+
### Quali Fit 履歴解析ツール
|
| 78 |
+
1. 左側のサイドバーから、LINEから書き出した`.txt`または`.csv`ファイルをアップロードしてください。
|
| 79 |
+
2. **ダッシュボード**タブで、コミュニケーションの統計を確認できます。
|
| 80 |
+
3. **トーク閲覧**タブで、特定のキーワードや人物で検索が可能です。
|
| 81 |
+
4. フィルタリング後のデータは、CSVとして保存できます。
|
| 82 |
+
")
|
| 83 |
+
)
|
| 84 |
+
)
|
| 85 |
)
|
| 86 |
|
| 87 |
+
# --- Server ---
|
| 88 |
server <- function(input, output, session) {
|
| 89 |
+
|
| 90 |
+
# 解析ロジック
|
| 91 |
+
parsed_df <- reactive({
|
| 92 |
+
req(input$file1)
|
| 93 |
+
lines <- readLines(input$file1$datapath, warn = FALSE, encoding = "UTF-8")
|
| 94 |
+
|
| 95 |
+
chat_list <- list()
|
| 96 |
+
current_date <- NA
|
| 97 |
+
current_thread <- "不明"
|
| 98 |
+
|
| 99 |
+
for (line in lines) {
|
| 100 |
+
if (grepl("\\[LINE\\] (.*?)とのトーク履歴", line)) {
|
| 101 |
+
current_thread <- sub("\\[LINE\\] (.*?)とのトーク履歴.*", "\\1", line)
|
| 102 |
+
next
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
}
|
| 104 |
+
if (grepl("^\\d{4}/\\d{2}/\\d{2}", line)) {
|
| 105 |
+
current_date <- sub("^(\\d{4}/\\d{2}/\\d{2}).*", "\\1", line)
|
| 106 |
+
next
|
| 107 |
+
}
|
| 108 |
+
if (grepl("^\"?\\d{2}:\\d{2}\t", line)) {
|
| 109 |
+
clean_line <- gsub("^\"", "", line)
|
| 110 |
+
parts <- strsplit(clean_line, "\t")[[1]]
|
| 111 |
+
if (length(parts) >= 3) {
|
| 112 |
+
chat_list[[length(chat_list) + 1]] <- data.frame(
|
| 113 |
+
Thread = current_thread,
|
| 114 |
+
Date = as.Date(current_date, format="%Y/%m/%d"),
|
| 115 |
+
Time = parts[1],
|
| 116 |
+
Sender = parts[2],
|
| 117 |
+
Message = gsub("\"$", "", parts[3]),
|
| 118 |
+
stringsAsFactors = FALSE
|
| 119 |
+
)
|
| 120 |
+
}
|
| 121 |
+
} else if (length(chat_list) > 0 && !is.na(current_date)) {
|
| 122 |
+
last_idx <- length(chat_list)
|
| 123 |
+
chat_list[[last_idx]]$Message <- paste(chat_list[[last_idx]]$Message, line)
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
bind_rows(chat_list)
|
| 127 |
+
})
|
| 128 |
+
|
| 129 |
+
# 動的フィルター
|
| 130 |
+
output$filter_ui <- renderUI({
|
| 131 |
+
df <- parsed_df()
|
| 132 |
+
tagList(
|
| 133 |
+
pickerInput("thread_sel", "スレッド選択:", choices = unique(df$Thread), multiple = TRUE, selected = unique(df$Thread), options = list(`actions-box` = TRUE)),
|
| 134 |
+
pickerInput("sender_sel", "送信者選択:", choices = unique(df$Sender), multiple = TRUE, selected = unique(df$Sender), options = list(`actions-box` = TRUE)),
|
| 135 |
+
# 修正点: btnDel を btnReset に変更
|
| 136 |
+
searchInput("keyword", "本文検索:", placeholder = "キーワード入力...", btnSearch = icon("search"), btnReset = icon("times"))
|
| 137 |
+
)
|
| 138 |
+
})
|
| 139 |
+
|
| 140 |
+
# フィルタリング後のデータ
|
| 141 |
+
filtered_df <- reactive({
|
| 142 |
+
df <- parsed_df()
|
| 143 |
+
if (!is.null(input$thread_sel)) df <- df %>% filter(Thread %in% input$thread_sel)
|
| 144 |
+
if (!is.null(input$sender_sel)) df <- df %>% filter(Sender %in% input$sender_sel)
|
| 145 |
+
if (!is.null(input$keyword) && input$keyword != "") {
|
| 146 |
+
df <- df %>% filter(grepl(input$keyword, Message, ignore.case = TRUE))
|
| 147 |
+
}
|
| 148 |
+
df
|
| 149 |
+
})
|
| 150 |
+
|
| 151 |
+
# --- メトリクス ---
|
| 152 |
+
output$total_msg <- renderText({ nrow(filtered_df()) })
|
| 153 |
+
output$total_members <- renderText({ n_distinct(filtered_df()$Sender) })
|
| 154 |
+
output$top_sender <- renderText({
|
| 155 |
+
res <- filtered_df() %>% count(Sender) %>% slice_max(n, n = 1) %>% pull(Sender)
|
| 156 |
+
if(length(res) > 0) res[1] else "なし"
|
| 157 |
+
})
|
| 158 |
+
|
| 159 |
+
# --- グラフ作成 ---
|
| 160 |
+
output$piePlot <- renderPlotly({
|
| 161 |
+
plot_ly(filtered_df() %>% count(Sender), labels = ~Sender, values = ~n, type = 'pie', hole = 0.4) %>%
|
| 162 |
+
layout(showlegend = TRUE)
|
| 163 |
+
})
|
| 164 |
+
|
| 165 |
+
output$timePlot <- renderPlotly({
|
| 166 |
+
p <- filtered_df() %>% count(Date) %>%
|
| 167 |
+
ggplot(aes(x = Date, y = n)) +
|
| 168 |
+
# 修正点: size を linewidth に変更
|
| 169 |
+
geom_line(color = "#06C755", linewidth = 1) +
|
| 170 |
+
geom_point(color = "#06C755") +
|
| 171 |
+
theme_minimal() + labs(x = "", y = "投稿数")
|
| 172 |
+
ggplotly(p)
|
| 173 |
+
})
|
| 174 |
+
|
| 175 |
+
# --- テーブル ---
|
| 176 |
+
output$table <- renderDT({
|
| 177 |
+
datatable(filtered_df(), options = list(pageLength = 15, autoWidth = TRUE), rownames = FALSE)
|
| 178 |
+
})
|
| 179 |
+
|
| 180 |
+
# --- エクスポート ---
|
| 181 |
+
output$downloadData <- downloadHandler(
|
| 182 |
+
filename = function() { paste0("Struct_Chat_", Sys.Date(), ".csv") },
|
| 183 |
+
content = function(file) { write.csv(filtered_df(), file, row.names = FALSE, fileEncoding = "CP932") }
|
| 184 |
)
|
| 185 |
}
|
| 186 |
|
| 187 |
+
shinyApp(ui, server)
|