lib/assistant-api.ts
Rama main · réplica de lectura del repositorio original.
"use client";
import { createSerialQueue } from "./serial-queue";
import type { NumoConversation, NumoConversationDetail, NumoConversationPatch } from "./assistant-types";
/** A bounded, newest-first Numo history page across every accessible project. */
export async function fetchConversations(limit: number): Promise<{ conversations: NumoConversation[]; hasMore: boolean }> {
const res = await fetch(`/api/assistant/conversations?limit=${encodeURIComponent(limit)}`);
if (!res.ok) return { conversations: [], hasMore: false };
return res.json();
}
/** Read the common timeline, retaining the distinction between chat and work. */
export async function fetchNumoConversation(id: string): Promise<NumoConversationDetail> {
const res = await fetch(`/api/numo/conversations/${encodeURIComponent(id)}`);
if (!res.ok) throw new Error("Unable to read conversation");
return res.json();
}
export async function updateConversation(id: string, patch: NumoConversationPatch): Promise<boolean> {
const res = await fetch(`/api/numo/conversations/${encodeURIComponent(id)}`, {
method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch),
});
return res.ok;
}
/** Update a conversation and retain the server's explicit validation message. */
export async function updateConversationWithResult(
id: string,
patch: NumoConversationPatch,
): Promise<{ ok: true } | { ok: false; error: string }> {
try {
const res = await fetch(`/api/numo/conversations/${encodeURIComponent(id)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
});
if (res.ok) return { ok: true };
const data = await res.json().catch(() => null) as { error?: unknown } | null;
return {
ok: false,
error: typeof data?.error === "string" ? data.error : `HTTP ${res.status}`,
};
} catch {
return { ok: false, error: "Unable to save conversation settings" };
}
}
export async function deleteConversation(
conversationId: string
): Promise<boolean> {
const res = await fetch(
`/api/assistant/conversations?id=${encodeURIComponent(conversationId)}`,
{ method: "DELETE" }
);
return res.ok;
}
/** Open conversation, and its scope (MIN-353). The pointer lives in base:
* it survives reloading, the next tab and the desktop app. */
export interface ActiveConversationRef {
conversationId: string | null;
projectId: string | null;
}
export async function fetchActiveConversation(): Promise<ActiveConversationRef> {
const res = await fetch("/api/assistant/active-conversation");
if (!res.ok) return { conversationId: null, projectId: null };
return res.json();
}
/**
* Pointer writes, END TO END.
*
* They follow each other closely and cancel each other: “new
* conversation” clears, the first message written. Launched in parallel, nothing
* guarantees that they arrive in the order in which they left — a DELETE
* processed after the PUT leaves the base without a pointer while the thread is alive.
* And ignoring the response of a stale write would not change anything: the server
* has already applied it. Only the TRANSMIT order is controlled, hence the queue.
*/
const pointerWrites = createSerialQueue();
/** `null` clears the pointer (“new conversation”). Best-effort: lose
* writing only costs a failed restart, never a message. */
export function setActiveConversation(
conversationId: string | null
): Promise<void> {
return pointerWrites(async () => {
try {
await fetch("/api/assistant/active-conversation", {
method: conversationId ? "PUT" : "DELETE",
...(conversationId
? {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ conversationId }),
}
: {}),
});
} catch {
// Network cut — the pointer will update at the next message.
}
});
}