Files
gitmost/apps/client/src/features/ai-chat/services/ai-chat-service.ts
claude code agent 227 001ebe2e53 feat(ai): generate page title from content (#199)
Add an AI button in the page byline that generates a note's title from the
live editor content (including unsaved edits) and applies it immediately.

Server: one-shot, non-streaming POST /ai-chat/generate-page-title mirroring
the chat generateTitle path — gated by settings.ai.generative, throttled via
AI_CHAT_THROTTLER, resolves the workspace chat model and returns { title }.
The endpoint never touches the page; the client applies the title through the
existing /pages/update route (which enforces edit permission).

Client: ai-chat-service.generatePageTitle, a useGeneratePageTitle hook that
converts the editor HTML to markdown, calls the endpoint, applies the title
via updateTitle + updatePageData, reflects it in the unfocused title editor,
and broadcasts the UpdateEvent (mirroring TitleEditor.saveTitle). A sparkles
button (GenerateTitleGroup) renders next to dictation, edit-mode + flag gated.

Tests: pure cleanGeneratedTitle helper + controller gate/delegation/error-map.
i18n: en-US + ru-RU strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 20:39:03 +03:00

115 lines
3.5 KiB
TypeScript

import api from "@/lib/api-client";
import { IPagination } from "@/lib/types.ts";
import {
IAiChat,
IAiChatListParams,
IAiChatMessageRow,
IAiChatMessagesParams,
IAiRole,
IAiRoleCreate,
IAiRoleUpdate,
} from "@/features/ai-chat/types/ai-chat.types.ts";
/**
* Per-user AI chat CRUD. The server uses POST for reads (its convention) and
* wraps every (non-stream) response in `{ data }` via the global transform
* interceptor, which the axios client unwraps to the body — so we read `.data`
* (mirroring `comment-service`). The `/ai-chat/stream` endpoint is consumed by
* the AI SDK `useChat` transport directly, not here.
*/
/** List the current user's chats (most recent first, paginated). */
export async function getAiChats(
params: IAiChatListParams,
): Promise<IPagination<IAiChat>> {
const req = await api.post<IPagination<IAiChat>>("/ai-chat/chats", params);
return req.data;
}
/** Fetch a chat's messages (oldest first, paginated). */
export async function getAiChatMessages(
params: IAiChatMessagesParams,
): Promise<IPagination<IAiChatMessageRow>> {
const req = await api.post<IPagination<IAiChatMessageRow>>(
"/ai-chat/messages",
params,
);
return req.data;
}
/** Rename a chat. */
export async function renameAiChat(data: {
chatId: string;
title: string;
}): Promise<void> {
await api.post("/ai-chat/rename", data);
}
/** Soft-delete a chat. */
export async function deleteAiChat(chatId: string): Promise<void> {
await api.post("/ai-chat/delete", { chatId });
}
/**
* Export a chat to Markdown (#183). The server renders the transcript from the
* persisted rows (the DB is the single source of truth — including an
* interrupted turn's in-progress row, persisted upfront + per step), so the
* client just copies the returned string. `lang` localizes the few fixed
* role/tool labels; defaults to English server-side when omitted.
*/
export async function exportAiChat(
chatId: string,
lang?: string,
): Promise<string> {
const req = await api.post<{ markdown: string }>("/ai-chat/export", {
chatId,
lang,
});
return req.data.markdown;
}
/**
* Generate a page title from note content (markdown). One-shot, non-streaming
* (#199): the server only summarizes the supplied text and returns a suggestion;
* it never writes the page. The caller applies the title via /pages/update.
*/
export async function generatePageTitle(content: string): Promise<string> {
const req = await api.post<{ title: string }>(
"/ai-chat/generate-page-title",
{ content },
);
return req.data.title;
}
/**
* Agent roles API (`/ai-chat/roles`). `list` is available to any workspace
* member (for the chat-creation picker); create/update/delete are admin-only
* (the server enforces this). Same `{ data }` unwrap convention as above.
*/
/** List the workspace's agent roles. */
export async function getAiRoles(): Promise<IAiRole[]> {
const req = await api.post<IAiRole[]>("/ai-chat/roles");
return req.data;
}
/** Create a role (admin). */
export async function createAiRole(data: IAiRoleCreate): Promise<IAiRole> {
const req = await api.post<IAiRole>("/ai-chat/roles/create", data);
return req.data;
}
/** Update a role (admin). */
export async function updateAiRole(data: IAiRoleUpdate): Promise<IAiRole> {
const req = await api.post<IAiRole>("/ai-chat/roles/update", data);
return req.data;
}
/** Soft-delete a role (admin). */
export async function deleteAiRole(id: string): Promise<{ success: true }> {
const req = await api.post<{ success: true }>("/ai-chat/roles/delete", {
id,
});
return req.data;
}