Organize/lib/actions/group-status-ai.ts

71 lines
3.0 KiB
TypeScript

"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<StatusChatResult> {
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,
});
}