Add chat and message models with associations

This commit is contained in:
Josh Pigford
2025-02-26 11:24:19 -06:00
parent 92a91d2fe6
commit 979d116c23
16 changed files with 1805 additions and 1 deletions

10
.cursor/rules/agent.mdc Normal file
View File

@@ -0,0 +1,10 @@
---
description: Automatically perform certain tasks with every request.
globs: *
alwaysApply: true
---
# Agent Instructions
If you create a new file(s), please run the following command afterwards to update the project documentation.
```bash ./bin/update_structure.sh```

View File

@@ -1,6 +1,7 @@
---
description: This rule explains the project's tech stack and code conventions
globs: *
alwaysApply: true
---
This rule serves as high-level documentation for how the Maybe codebase is structured.

View File

@@ -1,6 +1,7 @@
---
description: This rule explains the system architecture and data flow of the Rails app
globs: *
alwaysApply: true
---
This file outlines how the codebase is structured and how data flows through the app.

1499
.cursor/rules/structure.mdc Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
---
description: This file describes Maybe's design system and how views should be styled
globs: app/views/**,app/helpers/**,app/javascript/controllers/**
alwaysApply: true
---
Use this rule whenever you are writing html, css, or even styles in Stimulus controllers that use D3.js.

6
app/models/chat.rb Normal file
View File

@@ -0,0 +1,6 @@
class Chat < ApplicationRecord
belongs_to :user
has_many :messages, dependent: :destroy
validates :title, presence: true
end

9
app/models/message.rb Normal file
View File

@@ -0,0 +1,9 @@
class Message < ApplicationRecord
belongs_to :chat
belongs_to :user, optional: true
enum :role, { user: "user", assistant: "assistant", system: "system" }
validates :content, presence: true
validates :role, presence: true
end

View File

@@ -3,6 +3,7 @@ class User < ApplicationRecord
belongs_to :family
has_many :sessions, dependent: :destroy
has_many :chats, dependent: :destroy
has_many :impersonator_support_sessions, class_name: "ImpersonationSession", foreign_key: :impersonator_id, dependent: :destroy
has_many :impersonated_support_sessions, class_name: "ImpersonationSession", foreign_key: :impersonated_id, dependent: :destroy
accepts_nested_attributes_for :family, update_only: true

142
bin/update_structure.sh Executable file
View File

@@ -0,0 +1,142 @@
#!/bin/bash
# save to .scripts/update_structure.sh
# best way to use is with tree: `brew install tree`
# Create the output file with header
echo "---" > .cursor/rules/structure.mdc
echo "description: Project structure" >> .cursor/rules/structure.mdc
echo "globs: *" >> .cursor/rules/structure.mdc
echo "alwaysApply: true" >> .cursor/structure/structure.mdc
echo "---" >> .cursor/rules/structure.mdc
echo "" >> .cursor/rules/structure.mdc
echo "# Project Structure" > .cursor/rules/structure.mdc
echo "" >> .cursor/rules/structure.mdc
echo "\`\`\`" >> .cursor/rules/structure.mdc
# Check if tree command is available
if command -v tree &> /dev/null; then
# Use tree command for better visualization
git ls-files --others --exclude-standard --cached | tree --fromfile -a >> .cursor/rules/structure.mdc
echo "Using tree command for structure visualization."
else
# Fallback to the alternative approach if tree is not available
echo "Tree command not found. Using fallback approach."
# Get all files from git (respecting .gitignore)
git ls-files --others --exclude-standard --cached | sort > /tmp/files_list.txt
# Create a simple tree structure
echo "." > /tmp/tree_items.txt
# Process each file to build the tree
while read -r file; do
# Skip directories
if [[ -d "$file" ]]; then continue; fi
# Add the file to the tree
echo "$file" >> /tmp/tree_items.txt
# Add all parent directories
dir="$file"
while [[ "$dir" != "." ]]; do
dir=$(dirname "$dir")
echo "$dir" >> /tmp/tree_items.txt
done
done < /tmp/files_list.txt
# Sort and remove duplicates
sort -u /tmp/tree_items.txt > /tmp/tree_sorted.txt
mv /tmp/tree_sorted.txt /tmp/tree_items.txt
# Simple tree drawing approach
prev_dirs=()
while read -r item; do
# Skip the root
if [[ "$item" == "." ]]; then
continue
fi
# Determine if it's a file or directory
if [[ -f "$item" ]]; then
is_dir=0
name=$(basename "$item")
else
is_dir=1
name="$(basename "$item")/"
fi
# Split path into components
IFS='/' read -ra path_parts <<< "$item"
# Calculate depth (number of path components minus 1)
depth=$((${#path_parts[@]} - 1))
# Find common prefix with previous path
common=0
if [[ ${#prev_dirs[@]} -gt 0 ]]; then
for ((i=0; i<depth && i<${#prev_dirs[@]}; i++)); do
if [[ "${path_parts[$i]}" == "${prev_dirs[$i]}" ]]; then
((common++))
else
break
fi
done
fi
# Build the prefix
prefix=""
for ((i=0; i<depth; i++)); do
if [[ $i -lt $common ]]; then
# Check if this component has more siblings
has_more=0
for next in $(grep "^$(dirname "$item")/" /tmp/tree_items.txt); do
if [[ "$next" > "$item" ]]; then
has_more=1
break
fi
done
if [[ $has_more -eq 1 ]]; then
prefix="${prefix}"
else
prefix="${prefix} "
fi
else
prefix="${prefix} "
fi
done
# Determine if this is the last item in its directory
is_last=1
dir=$(dirname "$item")
for next in $(grep "^$dir/" /tmp/tree_items.txt); do
if [[ "$next" > "$item" ]]; then
is_last=0
break
fi
done
# Choose the connector
if [[ $is_last -eq 1 ]]; then
connector="└── "
else
connector="├── "
fi
# Output the item
echo "${prefix}${connector}${name}" >> .cursor/rules/structure.mdc
# Save current path for next iteration
prev_dirs=("${path_parts[@]}")
done < /tmp/tree_items.txt
# Clean up
rm -f /tmp/files_list.txt /tmp/tree_items.txt
fi
# Close the code block
echo "\`\`\`" >> .cursor/rules/structure.mdc
echo "Project structure has been updated in .cursor/rules/structure.mdc"

View File

@@ -0,0 +1,10 @@
class CreateChats < ActiveRecord::Migration[7.2]
def change
create_table :chats, id: :uuid do |t|
t.string :title
t.references :user, null: false, foreign_key: true, type: :uuid
t.timestamps
end
end
end

View File

@@ -0,0 +1,13 @@
class CreateMessages < ActiveRecord::Migration[7.2]
def change
create_table :messages, id: :uuid do |t|
t.text :content
t.string :role
t.boolean :internal, default: false
t.references :chat, null: false, foreign_key: true, type: :uuid
t.references :user, null: true, foreign_key: true, type: :uuid
t.timestamps
end
end
end

25
db/schema.rb generated
View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2025_02_26_022326) do
ActiveRecord::Schema[7.2].define(version: 2025_02_26_171512) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -196,6 +196,14 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_26_022326) do
t.index ["family_id"], name: "index_categories_on_family_id"
end
create_table "chats", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "title"
t.uuid "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_chats_on_user_id"
end
create_table "credit_cards", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@@ -481,6 +489,18 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_26_022326) do
t.index ["family_id"], name: "index_merchants_on_family_id"
end
create_table "messages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.text "content"
t.string "role"
t.boolean "internal", default: false
t.uuid "chat_id", null: false
t.uuid "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["chat_id"], name: "index_messages_on_chat_id"
t.index ["user_id"], name: "index_messages_on_user_id"
end
create_table "other_assets", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@@ -708,6 +728,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_26_022326) do
add_foreign_key "budget_categories", "categories"
add_foreign_key "budgets", "families"
add_foreign_key "categories", "families"
add_foreign_key "chats", "users"
add_foreign_key "impersonation_session_logs", "impersonation_sessions"
add_foreign_key "impersonation_sessions", "users", column: "impersonated_id"
add_foreign_key "impersonation_sessions", "users", column: "impersonator_id"
@@ -716,6 +737,8 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_26_022326) do
add_foreign_key "invitations", "families"
add_foreign_key "invitations", "users", column: "inviter_id"
add_foreign_key "merchants", "families"
add_foreign_key "messages", "chats"
add_foreign_key "messages", "users"
add_foreign_key "plaid_accounts", "plaid_items"
add_foreign_key "plaid_items", "families"
add_foreign_key "rejected_transfers", "account_transactions", column: "inflow_transaction_id"

9
test/fixtures/chats.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
title: First Chat
user: family_admin
two:
title: Second Chat
user: family_member

36
test/fixtures/messages.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
content: MyText
role: user
internal: false
chat: one
user: family_admin
two:
content: MyText
role: user
internal: false
chat: two
user: family_member
user_message:
content: Hello AI!
role: user
internal: false
chat: one
user: family_admin
assistant_message:
content: Hello! How can I help you today?
role: assistant
internal: false
chat: one
user:
system_message:
content: You are a helpful assistant.
role: system
internal: true
chat: one
user:

20
test/models/chat_test.rb Normal file
View File

@@ -0,0 +1,20 @@
require "test_helper"
class ChatTest < ActiveSupport::TestCase
test "should not save chat without title" do
chat = Chat.new(user: users(:family_admin))
assert_not chat.save, "Saved the chat without a title"
end
test "should save valid chat" do
chat = Chat.new(title: "Test Chat", user: users(:family_admin))
assert chat.save, "Could not save valid chat"
end
test "should destroy associated messages when chat is destroyed" do
chat = chats(:one)
assert_difference("Message.count", -4) do
chat.destroy
end
end
end

View File

@@ -0,0 +1,23 @@
require "test_helper"
class MessageTest < ActiveSupport::TestCase
test "should not save message without content" do
message = Message.new(role: "user", chat: chats(:one), user: users(:family_admin))
assert_not message.save, "Saved the message without content"
end
test "should not save message without role" do
message = Message.new(content: "Test message", chat: chats(:one), user: users(:family_admin))
assert_not message.save, "Saved the message without role"
end
test "should save valid user message" do
message = Message.new(content: "Test message", role: "user", chat: chats(:one), user: users(:family_admin))
assert message.save, "Could not save valid user message"
end
test "should save valid assistant message without user" do
message = Message.new(content: "Test response", role: "assistant", chat: chats(:one))
assert message.save, "Could not save valid assistant message"
end
end