disclaimer I'm a statistics major and this is going in my portfolio so please don't actually treat this as anything more than like a fun project
INSTRUCTIONS:
Step 1 open terminal in your macbook (lookup up terminal using the searchbar) and paste each chunk of code separately
sqlite3 ~/Library/Messages/chat.db
Replace the 1234567890 with the persons actual phone number before you paste this ⬇️
.headers on
.mode csv
.output ourmessages.csv
SELECT
datetime(message.date / 1000000000 + 978307200, 'unixepoch', 'localtime') AS timestamp,
CASE
WHEN message.is_from_me = 1 THEN 'Me'
ELSE handle.id
END AS sender,
message.text
FROM message
JOIN chat_message_join
ON message.ROWID = chat_message_join.message_id
JOIN chat_handle_join
ON chat_message_join.chat_id = chat_handle_join.chat_id
JOIN handle
ON chat_handle_join.handle_id = handle.ROWID
WHERE handle.id LIKE '%9723576595%'
AND message.text IS NOT NULL
ORDER BY message.date;
Step 2 open RStudio (you can download here) and paste the following code chunks. Im using a .rmd file (file > new file > R Markdown)
# -----packages and exploration
library(tidyverse)
install.packages("tidytext")
library(tidytext)
library(stringr)
install.packages("text2bvec")
library(text2vec)
chat_data <- read_csv("ourmessages.csv") %>%
mutate(
# labeling me and them
sender = if_else(!is.na(sender) & str_trim(sender) == "Me", "Me", "Them"),
text = str_trim(text)
)
head(chat_data, 50)
nrow(chat_data)
str(chat_data)
chat_data$sender <- factor(chat_data$sender, levels = c("Me", "Them"))
chat_data$timestamp <- as.Date(chat_data$timestamp)
# -----cleaning dataset and create pairs
chat_pairs <- chat_data %>%
# clean whitespace and force sender to trimmed case
mutate(
sender = str_trim(sender),
text = str_trim(text)
) %>%
filter(!is.na(text) & nchar(text) > 0) %>%
# pairing each of my messages with his immediate response
mutate(
next_sender = lead(sender),
next_text = lead(text)
) %>%
filter(tolower(sender) == "me" & tolower(next_sender) != "me") %>%
select(my_prompt = text, their_response = next_text) %>%
filter(!is.na(their_response)) %>%
distinct() %>%
mutate(pair_id = row_number())
# sanity checking to make sure npairs > 0...
nrow(chat_pairs)
# -----step2 unnest words from past prompts
prompt_tokens <- chat_pairs %>%
unnest_tokens(word, my_prompt) %>%
count(pair_id, word) %>%
bind_tf_idf(word, pair_id, n)
# -----step3 prediction function
predict_response <- function(input_msg, pairs_df, tokens_df) {
# tokenize the incoming firsthand user prompt
input_df <- tibble(my_prompt = input_msg) %>%
unnest_tokens(word, my_prompt) %>%
count(word)
matches <- tokens_df %>%
inner_join(input_df, by = "word", suffix = c("_hist", "_input")) %>%
# calculate similarity score based on TF-IDF weight
group_by(pair_id) %>%
summarise(score = sum(tf_idf * n_input), .groups = "drop") %>%
arrange(desc(score))
# handle cases with zero word overlap (fallback to random frequent response)
if (nrow(matches) == 0) {
message("no direct word matches found, returning a common response")
return(sample(pairs_df$their_response, 1))
}
top_pair_id <- matches$pair_id[1]
matched_pair <- pairs_df %>%
filter(pair_id == top_pair_id)
return(matched_pair$their_response)
}
# shiny interface
library(shiny)
library(tidyverse)
library(tidytext)
# minimalist barebones shiny interface
ui <- fluidPage(
tags$head(
tags$style(HTML("
body {
background-color: #ffffff !important;
font-family: monospace;
padding: 50px;
color: #000000;
}
.prompt-container {
display: flex;
align-items: center;
font-size: 20px;
}
.prompt-symbol {
margin-right: 12px;
font-weight: bold;
user-select: none;
}
.shiny-input-container {
margin-bottom: 0 !important;
width: 100% !important;
}
.form-control, .form-control:focus {
border: none !important;
box-shadow: none !important;
outline: none !important;
background-color: transparent !important;
font-family: monospace;
font-size: 20px;
color: #000000;
padding: 0 !important;
height: auto !important;
}
.response-output {
margin-top: 20px;
font-size: 20px;
font-family: monospace;
color: #000000;
white-space: pre-wrap;
}
"))
),
# HTML form traps the Enter key press
tags$form(
id = "prompt_form",
action = "javascript:void(0);",
onsubmit = "Shiny.setInputValue('submit_input', document.getElementById('user_input').value, {priority: 'event'});",
div(class = "prompt-container",
span(">", class = "prompt-symbol"),
tags$input(
id = "user_input",
type = "text",
class = "form-control",
placeholder = "",
autocomplete = "off"
)
)
),
div(class = "response-output",
textOutput("prediction")
)
)
# 3. SERVER LOGIC
server <- function(input, output, session) {
# only triggers when 'submit_input' is fired (via Enter key)
predicted_val <- eventReactive(input$submit_input, {
req(input$submit_input)
predict_response(input$submit_input, chat_pairs, prompt_tokens)
})
output$prediction <- renderText({
predicted_val()
})
}
shinyApp(ui = ui, server = server)