diff --git a/README.md b/README.md
index 837a6112d..c7625286c 100644
--- a/README.md
+++ b/README.md
@@ -41,10 +41,28 @@ Comment.find_each(&:refresh_search_embedding)
### Tests
+#### AI Requests
+
For testing OpenAI API requests, we use [VCR](https://github.com/vcr/vcr). If you want to test AI features exercising the API, you need to place the `config/credentials/test.key`
that you can get from 1Password in "Fizzy - test.key". Then, when running tests that use Open AI API, you must either set the env variable `VCR_RECORD=1`
or to add `vcr_record!` to the test. See `VcrTestHelper`. Due to Open AI quotas, you may need to limit the parallelization. E.g: `PARALLEL_WORKERS=2`
+You can regenerate all the VCR fixtures with:
+
+```bash
+VCR_RECORD=1 PARALLEL_WORKERS=2 bin/rails test
+```
+
+A typical scenario is making modifications LLM prompts. You need to:
+
+1. Make the prompt changes.
+2. Run the test with `VCR_RECORD=1` set. This will let the test make the actual network requests and record the responses as VCR fixtures.
+3. Make sure that the test passes by running the test normally (without setting VCR_RECORD=1).
+4. Commit the YML fixtures that VCR has generated. That way, the next time you run the test, it will run fast without performing any request.
+
+Notice that if you pass changing data to the prompt, this mechanism won't work. E.g: if you pass data timestamps.
+You need to make sure those timestamps are always the same across executions.
+
## Environments
Fizzy is deployed with Kamal. You'll need to have the 1Password CLI set up in order to access the secrets that are used when deploying. Provided you have that, it should be as simple as `bin/kamal deploy` to the correct environment.
diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css
index 67820e6ab..6f41bc597 100644
--- a/app/assets/stylesheets/_global.css
+++ b/app/assets/stylesheets/_global.css
@@ -48,7 +48,7 @@
/* Z-index */
--z-events-column-header: 1;
- --z-events-day-header: 2;
+ --z-events-day-header: 3;
--z-popup: 10;
--z-terminal: 20;
--z-tray: 21;
@@ -174,9 +174,9 @@
--color-selected-dark: oklch(var(--lch-blue-light));
--color-highlight: oklch(var(--lch-yellow-lighter));
--color-marker: oklch(var(--lch-red-medium));
- --color-terminal-bg: oklch(var(--lch-blue-darkest));
- --color-terminal-text: oklch(var(--lch-green-medium));
- --color-terminal-text-light: oklch(var(--lch-green-light));
+ --color-terminal-bg: var(--color-canvas);
+ --color-terminal-text: oklch(var(--lch-green-dark));
+ --color-terminal-text-light: oklch(var(--lch-green-medium));
/* Colors: Cards */
--color-card-default: oklch(var(--lch-blue-dark));
@@ -204,7 +204,7 @@
--color-gradient-1: oklch(var(--lch-violet-lighter));
--color-gradient-2: oklch(var(--lch-pink-lighter));
--color-gradient-3: oklch(var(--lch-purple-lighter));
- --color-gradient-4: oklch(var(--lch-blue-lighter));
+ --color-gradient-4: oklch(var(--lch-white));
@media (prefers-color-scheme: dark) {
--lch-canvas: 20% 0.0195 232.58;
@@ -298,7 +298,7 @@
--lch-pink-lighter: 30% 0.03 346;
--lch-pink-lightest: 25% 0.011 348;
- --color-terminal-bg: var(--color-black);
+ --color-terminal-bg: var(--color-canvas);
--color-terminal-text-light: oklch(var(--lch-green-dark));
--shadow: 0 0 0 1px oklch(var(--lch-black) / 0.42),
diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css
index 4c97b949a..2128d45e2 100644
--- a/app/assets/stylesheets/events.css
+++ b/app/assets/stylesheets/events.css
@@ -18,17 +18,16 @@
}
.events__activity-summary {
- --border-color: var(--color-selected);
-
- background-color: var(--color-selected-light);
font-style: italic;
inline-size: auto;
line-height: 1.3;
+ margin-block-end: calc(var(--block-space) * -1);
margin-inline: auto;
max-inline-size: 80ch;
padding: 1.3lh 1lh 1lh;
position: relative;
text-align: start;
+ z-index: 2;
&:has(.events__activity--generating) {
--border-color: var(--color-selected-dark);
@@ -52,14 +51,13 @@
}
span {
+ display: block;
font-weight: 500;
+ margin-block: 2lh;
opacity: 0.5;
}
.btn {
- --btn-background: var(--color-selected-light);
- --btn-border-color: var(--color-selected-dark);
-
inset: auto 1em 1em auto;
position: absolute;
}
@@ -104,6 +102,7 @@
.events__columns {
border-block-start: var(--events-border);
position: relative;
+ z-index: 1;
@media (min-width: 640px) {
align-items: end;
diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css
index 70f7c9d56..b0969cc3a 100644
--- a/app/assets/stylesheets/rich-text-content.css
+++ b/app/assets/stylesheets/rich-text-content.css
@@ -22,8 +22,11 @@
:where(p, ul, ol, dl, blockquote, figure, .attachment) {
margin-block: var(--block-margin);
- overflow-wrap: break-word;
- text-wrap: pretty;
+
+ &:not(lexical-editor &) {
+ overflow-wrap: break-word;
+ text-wrap: pretty;
+ }
}
:where(b, strong) {
diff --git a/app/assets/stylesheets/terminals.css b/app/assets/stylesheets/terminals.css
index e944cdef1..e598eb30f 100644
--- a/app/assets/stylesheets/terminals.css
+++ b/app/assets/stylesheets/terminals.css
@@ -3,10 +3,12 @@
--row-gap: 0.2lh;
background-color: var(--color-terminal-bg);
+ border-block-start: 1px solid var(--color-ink-light);
color: var(--color-terminal-text);
font-family: var(--font-mono);
font-size: var(--text-normal);
inset: auto 0 0 0;
+ max-block-size: 100%;
padding: var(--block-space) calc(var(--tray-size) + calc(var(--inline-space) * 3)) calc(var(--block-space) + env(safe-area-inset-bottom));
position: fixed;
z-index: var(--z-terminal);
@@ -155,6 +157,8 @@
}
.terminal__label {
+ font-weight: 600;
+
.terminal--busy:is(:not(.terminal--confirmation)) & {
animation: pulse 1s infinite;
}
@@ -201,6 +205,8 @@
.terminal__output {
display: none;
+ overflow-y: auto;
+ padding-inline: var(--inline-space);
.terminal--confirmation & {
color: var(--color-terminal-text-light);
diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css
index d03dd0952..3b29f4823 100644
--- a/app/assets/stylesheets/trays.css
+++ b/app/assets/stylesheets/trays.css
@@ -118,8 +118,8 @@
.tray__toggle-btn {
--btn-background: transparent;
- --btn-border-color: var(--color-white);
- --btn-color: var(--color-white);
+ --btn-border-color: var(--color-ink);
+ --btn-color: var(--color-ink);
border-style: dashed;
border-radius: 1ch;
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index fa55325cb..969a69e12 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -28,8 +28,8 @@ module NotificationsHelper
tag.div id: dom_id(notification), class: "tray__item" do
concat(
link_to(notification,
- class: [ "card card--notification", { "card--closed": notification.card.closed? } ],
- data: { turbo_frame: "_top" },
+ class: [ "card card--notification", { "card--closed": notification.card.closed? }, { "unread": !notification.read? } ],
+ data: { turbo_frame: "_top", badge_target: "unread", action: "badge#update" },
style: { "--card-color:": notification.card.color },
&)
)
@@ -42,7 +42,7 @@ module NotificationsHelper
method: :delete,
class: "card__notification-unread-indicator btn btn--circle borderless",
title: "Mark as unread",
- data: { action: "form#submit:stop", form_target: "submit" },
+ data: { action: "form#submit:stop badge#update:stop", form_target: "submit" },
form: { data: { controller: "form" } } do
concat(icon_tag("unseen"))
concat(tag.span("Mark as unread", class: "for-screen-reader"))
@@ -51,7 +51,7 @@ module NotificationsHelper
button_to read_notification_path(notification),
class: "card__notification-unread-indicator btn btn--circle borderless",
title: "Mark as read",
- data: { action: "form#submit:stop", form_target: "submit" },
+ data: { action: "form#submit:stop badge#update:stop", form_target: "submit" },
form: { data: { controller: "form" } } do
concat(icon_tag("remove-med"))
concat(tag.span("Mark as read", class: "for-screen-reader"))
diff --git a/app/javascript/controllers/badge_controller.js b/app/javascript/controllers/badge_controller.js
new file mode 100644
index 000000000..ca44f96cb
--- /dev/null
+++ b/app/javascript/controllers/badge_controller.js
@@ -0,0 +1,41 @@
+import { Controller } from "@hotwired/stimulus"
+import { onNextEventLoopTick } from "helpers/timing_helpers"
+
+export default class extends Controller {
+ static targets = [ "unread" ]
+ static classes = [ "unread" ]
+
+ connect() {
+ onNextEventLoopTick(() => this.update())
+ }
+
+ update() {
+ onNextEventLoopTick(() => {
+ if (this.#available) {
+ const unreadCount = this.#unreadCount
+
+ if (unreadCount > 0) {
+ navigator.setAppBadge(unreadCount)
+ } else {
+ navigator.clearAppBadge()
+ }
+ }
+ })
+ }
+
+ clear() {
+ onNextEventLoopTick(() => {
+ if (this.#available) {
+ navigator.clearAppBadge()
+ }
+ })
+ }
+
+ get #unreadCount() {
+ return this.unreadTargets.filter(unreadTarget => unreadTarget.classList.contains(this.unreadClass)).length
+ }
+
+ get #available() {
+ return "setAppBadge" in navigator
+ }
+}
diff --git a/app/models/command/parser/context.rb b/app/models/command/parser/context.rb
index cb5a7e0a4..ee90de458 100644
--- a/app/models/command/parser/context.rb
+++ b/app/models/command/parser/context.rb
@@ -3,6 +3,8 @@ class Command::Parser::Context
MAX_CARDS = 75
+ # TODO: +source_context+, +originally_viewing_card_contents?+ and +original_cards+ are part
+ # of a workaround for the "get insight" experiment. I'll remove once I remove that.
def initialize(user, url:, script_name: "", source_context: nil)
@user = user
@url = url
diff --git a/app/models/event/summarizer.rb b/app/models/event/summarizer.rb
index 8cc76fdbc..4afc41166 100644
--- a/app/models/event/summarizer.rb
+++ b/app/models/event/summarizer.rb
@@ -4,7 +4,7 @@ class Event::Summarizer
attr_reader :events
- MAX_WORDS = 150
+ MAX_WORDS = 80
LLM_MODEL = "chatgpt-4o-latest"
# LLM_MODEL = "gpt-4.1"
@@ -19,21 +19,24 @@ class Event::Summarizer
- How things are looking.
- Newly created cards.
- Draw on top-level comments to enrich each point.
- - Prioritise relevance and meaning over completeness.
+ - Prioritize relevance, interesting observations, and meaning over completeness.
+ - Prefer surfacing insights over restating facts.
+ - Point out people who are performing especially well.
+ - Point out any trends that may be relevant.
## Writing style
- Instead of using passive voice, prefer referring to users (authors and creators) as the subjects doing things.
- Aggregate related items into thematic clusters; avoid repeating card titles verbatim.
* Consider the collection name as a logical grouping unit.
- - Prefer compact paragraphs over bullet lists.
+ - Prefer a compact paragraph over bulleted list.
- Refer to people by first name (or full name if duplicates exist).
- e.g. “Ann closed …”, not “Card 123 was closed by Ann.”
## Formatting rules
- Output **Markdown** only.
- Keep the summary below **#{MAX_WORDS} words**.
- - Prefer paragraphs over bullet points.
- - Write 2 paragraphs at most.
+ - Prefer a paragraph over bullet points.
+ - Write 1 paragraph at most.
- Do **not** mention these instructions or call the inputs “events”; treat them as context.
## Linking rules
diff --git a/app/models/user/day_timeline/summarizable.rb b/app/models/user/day_timeline/summarizable.rb
index 1d493a518..ecbc30a6a 100644
--- a/app/models/user/day_timeline/summarizable.rb
+++ b/app/models/user/day_timeline/summarizable.rb
@@ -10,7 +10,7 @@ module User::DayTimeline::Summarizable
end
def summarizable?
- !day.today? || events.count >= 10
+ day.past? || summarizable_today?
end
def summarize
@@ -28,4 +28,9 @@ module User::DayTimeline::Summarizable
def summarize_later
User::DayTimeline::SummarizeJob.perform_later(self)
end
+
+ private
+ def summarizable_today?
+ day.today? && events.count >= 10
+ end
end
diff --git a/app/views/cards/display/perma/_tags.html.erb b/app/views/cards/display/perma/_tags.html.erb
index 4e870078c..527c579c5 100644
--- a/app/views/cards/display/perma/_tags.html.erb
+++ b/app/views/cards/display/perma/_tags.html.erb
@@ -17,6 +17,8 @@
Add a tag
- <%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card) %>
+
diff --git a/app/views/cards/taggings/new.html.erb b/app/views/cards/taggings/new.html.erb
index 5ff4b3bf0..6f7b1ec1a 100644
--- a/app/views/cards/taggings/new.html.erb
+++ b/app/views/cards/taggings/new.html.erb
@@ -1,7 +1,6 @@
<%= turbo_frame_tag @card, :tagging do %>
-
+
<% end %>
diff --git a/app/views/events/day_timeline/_summary.html.erb b/app/views/events/day_timeline/_summary.html.erb
index 4a9db8479..c73737a53 100644
--- a/app/views/events/day_timeline/_summary.html.erb
+++ b/app/views/events/day_timeline/_summary.html.erb
@@ -1,4 +1,4 @@
-
+
<% if day_timeline.summarized? %>
<%= render "events/day_timeline/activity_summary", summary: day_timeline.summary %>
diff --git a/app/views/notifications/_tray.html.erb b/app/views/notifications/_tray.html.erb
index f87ac6f23..23caa68ea 100644
--- a/app/views/notifications/_tray.html.erb
+++ b/app/views/notifications/_tray.html.erb
@@ -1,6 +1,6 @@
<%= turbo_stream_from Current.user, :notifications %>
-
+