Ctrl K
ComunidadGuías…

lib/assistant-api.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"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.
    }
  });
}