mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
feat(coach): a chat that behaves like one
The composer starts as a single line with send beside it and grows only when the text does; the shift-enter hint and the inline disclaimer are gone. A typing indicator bubbles before the first token. The context rail hides by default behind a toggle and stops scrolling sideways, conversations open in the full view instead of a broken side drawer, and the coach preferences move into Settings where the rest of the configuration lives — context toggles and the exclusion list now derive from one source so they can no longer contradict each other. A floating bubble appears only while a coach-initiated nudge is unread. Clearly stated allergies and conditions are remembered immediately through a deterministic extractor instead of waiting for the twenty-turn summary pass.
This commit is contained in:
1 parent
b55fab5e94
commit
9f2c403f84
19 files changed
+1092
-879
No files matched your search
@@ -57,6 +57,7 @@ import {
|
||||
listConversations,
|
||||
} from "@/lib/ai/coach/persistence";
|
||||
import { enqueueCoachMemoryRefresh } from "@/lib/ai/coach/coach-memory-shared";
|
||||
import { storeDeterministicFacts } from "@/lib/ai/coach/facts";
|
||||
import {
|
||||
buildDateKey,
|
||||
enforceBudget,
|
||||
@@ -331,6 +332,20 @@ async function handleChatRequest(request: NextRequest): Promise<Response> {
|
||||
// rolling summary + extract durable facts off the request path. Fire-and-
|
||||
// forget: this turn uses whatever summary is already on disk; the refresh
|
||||
// makes the next long turn fresh. No-ops without an embedded worker.
|
||||
// v1.16.1 — always-remember categories (allergies, intolerances, explicit
|
||||
// self-reported diagnoses) must not wait for the >TURN_CAP memory refresh:
|
||||
// a health-critical statement in the second message of a short chat used
|
||||
// to never reach the fact store. The deterministic pattern pass is
|
||||
// provider-free and deduped, so it fires on every user turn.
|
||||
void storeDeterministicFacts({
|
||||
conversationId: workingConversationId,
|
||||
userId,
|
||||
message,
|
||||
locale,
|
||||
}).catch(() => {
|
||||
// Fact capture must never break the chat turn; the >TURN_CAP LLM
|
||||
// extraction remains as the catch-all on long conversations.
|
||||
});
|
||||
if (allTurns.length > TURN_CAP) {
|
||||
void enqueueCoachMemoryRefresh({
|
||||
conversationId: workingConversationId,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* GET /api/insights/coach/nudge-status — has the proactive Coach nudge
|
||||
* something the caller has not engaged with yet?
|
||||
*
|
||||
* v1.16.1 — feeds the floating Coach bubble: the bubble renders only
|
||||
* while an unseen Coach-initiated nudge exists, instead of being a
|
||||
* permanent FAB. "Unseen" is derived, not stored: the latest successful
|
||||
* `COACH_NUDGE` dispatch (the `push_attempts` ledger the nudge cron
|
||||
* already anchors its frequency cap on) counts as read once the user
|
||||
* has sent a Coach message after it. The client additionally remembers
|
||||
* a local "seen" stamp so opening the chat without sending also clears
|
||||
* the bubble on that device.
|
||||
*
|
||||
* No new table, no migration: both timestamps come from existing rows,
|
||||
* scoped to the caller.
|
||||
*/
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAssistantSurface } from "@/lib/feature-flags";
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
const { user } = await requireAuth();
|
||||
await requireAssistantSurface("coach");
|
||||
|
||||
const [lastNudge, lastUserMessage] = await Promise.all([
|
||||
prisma.pushAttempt.findFirst({
|
||||
where: { userId: user.id, eventType: "COACH_NUDGE", result: "ok" },
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { createdAt: true },
|
||||
}),
|
||||
prisma.coachMessage.findFirst({
|
||||
where: { role: "user", conversation: { userId: user.id } },
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { createdAt: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const nudgedAt = lastNudge?.createdAt ?? null;
|
||||
const unread =
|
||||
nudgedAt !== null &&
|
||||
(lastUserMessage === null || lastUserMessage.createdAt < nudgedAt);
|
||||
|
||||
return apiSuccess({
|
||||
nudgedAt: nudgedAt ? nudgedAt.toISOString() : null,
|
||||
unread,
|
||||
});
|
||||
});
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
Reference in new issue
Block a user