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>
115 lines
3.5 KiB
TypeScript
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;
|
|
}
|