"use server"; import { prisma } from "@/lib/db"; import { requireUserId } from "@/lib/auth-helpers"; import { categoryAccessFilter } from "@/lib/access"; import { formatGroupForPrompt } from "@/lib/ai/context"; import { callChatCompletion } from "@/lib/ai/chat-completion"; export interface StatusChatMessage { role: "user" | "assistant"; content: string; } export type StatusChatResult = { content: string } | { error: string }; /** * Powers the group card's "Status Update" feature: with no prior * messages, produces an initial markdown status summary of the group * (notes + todos); with prior messages, answers a follow-up question * about it. One action rather than two "generate" / "ask" actions -- * both are the same shape of call (group context + message history in, * plain text out), and splitting them would just duplicate the * group-loading and system-prompt code for no behavioral benefit. Unlike * lib/actions/group-ai.ts/todo-ai.ts, nothing here is ever saved, so * there's no ask/finalize JSON contract to parse -- just plain text. */ export async function converseAboutGroupStatus( groupId: string, messages: StatusChatMessage[] ): Promise { const userId = await requireUserId(); const group = await prisma.group.findFirst({ where: { id: groupId, category: categoryAccessFilter(userId) }, select: { title: true, noteContent: true, todos: { orderBy: { order: "asc" } }, }, }); if (!group) return { error: "Group not found." }; const context = formatGroupForPrompt({ title: group.title, noteContent: group.noteContent, todos: group.todos.map((todo) => ({ title: todo.title, details: todo.details, completed: todo.completed, completedAt: todo.completedAt?.toISOString() ?? null, })), }); const systemPrompt = `You are summarizing one group ("${group.title}") from a to-do board for its owner. Here is everything known about it: ${context} If there are no prior messages below, your reply must be a concise markdown status update: a couple of short paragraphs and/or a short list covering what this group is about, what's been done, and what's still outstanding. Do not repeat the group's title as a heading, and don't add commentary like "Here's the status update" -- output only the summary itself. If there are prior messages below, they're a follow-up question about this group -- answer it using only the context above, in plain markdown, and say plainly if the answer isn't in the data.`; // An empty conversation means "generate the initial summary" -- a // synthetic, UI-invisible user turn keeps that a normal chat completion // call instead of a special-cased empty-messages request. const effectiveMessages: StatusChatMessage[] = messages.length > 0 ? messages : [{ role: "user", content: "Generate the status update now." }]; return callChatCompletion([{ role: "system", content: systemPrompt }, ...effectiveMessages], { temperature: 0.3, }); }