| |
| import type { |
| Artifact, |
| Benchmark, |
| ChatMessage, |
| DatasetSearchResponse, |
| DatasetSearchResult, |
| ModelEntry, |
| ProjectRecord, |
| ResourceKind, |
| ResourceRef, |
| Skill, |
| SyncStatusEntry, |
| TaskRecord, |
| } from "./types"; |
| import { BENCHMARKS, MODELS } from "./data"; |
|
|
| |
| |
| |
| |
| const LEGACY_BASE = process.env.NEXT_PUBLIC_API_URL || ""; |
|
|
| |
| |
| |
| |
| const IS_DEV = |
| typeof window !== "undefined" && |
| (window.location.hostname === "localhost" || |
| window.location.hostname === "127.0.0.1"); |
|
|
| |
| async function fetchJSON<T>(path: string, init?: RequestInit): Promise<T> { |
| return fetchJSONAt(path, init); |
| } |
|
|
| async function fetchJSONAt<T>(path: string, init?: RequestInit, base = ""): Promise<T> { |
| const controller = new AbortController(); |
| const timeout = setTimeout(() => controller.abort(), 4000); |
| const devHeaders: Record<string, string> = IS_DEV |
| ? { "X-Dev-User": "dev@proteinea.local" } |
| : {}; |
| try { |
| const res = await fetch(`${base}${path}`, { |
| ...init, |
| signal: controller.signal, |
| headers: { "Content-Type": "application/json", ...devHeaders, ...init?.headers }, |
| }); |
| if (!res.ok) throw new Error(`API ${res.status}`); |
| return (await res.json()) as T; |
| } finally { |
| clearTimeout(timeout); |
| } |
| } |
|
|
| |
|
|
| export async function listProjects(): Promise<{ projects: ProjectRecord[] }> { |
| return fetchJSON<{ projects: ProjectRecord[] }>("/api/projects"); |
| } |
|
|
| export async function ensureDefaultProject(): Promise<ProjectRecord> { |
| const { projects } = await listProjects(); |
| if (projects.length === 0) { |
| throw new Error("No projects returned from /api/projects"); |
| } |
| return projects[0]; |
| } |
|
|
| |
|
|
| export async function listTasks(projectId: string): Promise<{ tasks: TaskRecord[] }> { |
| return fetchJSON<{ tasks: TaskRecord[] }>( |
| `/api/projects/${encodeURIComponent(projectId)}/tasks`, |
| ); |
| } |
|
|
| export async function createTask( |
| projectId: string, |
| firstMessage: string, |
| ): Promise<TaskRecord> { |
| const { task } = await fetchJSON<{ task: TaskRecord }>( |
| `/api/projects/${encodeURIComponent(projectId)}/tasks`, |
| { |
| method: "POST", |
| body: JSON.stringify({ firstMessage }), |
| }, |
| ); |
| return task; |
| } |
|
|
| export async function getTask(taskId: string): Promise<TaskRecord | null> { |
| const controller = new AbortController(); |
| const timeout = setTimeout(() => controller.abort(), 4000); |
| try { |
| const res = await fetch(`/api/tasks/${encodeURIComponent(taskId)}`, { |
| signal: controller.signal, |
| headers: { "Content-Type": "application/json" }, |
| }); |
| if (res.status === 404) return null; |
| if (!res.ok) throw new Error(`API ${res.status}`); |
| const data = (await res.json()) as { task: TaskRecord }; |
| return data.task; |
| } finally { |
| clearTimeout(timeout); |
| } |
| } |
|
|
| export async function appendMessage( |
| taskId: string, |
| message: ChatMessage, |
| ): Promise<TaskRecord> { |
| const { task } = await fetchJSON<{ task: TaskRecord }>( |
| `/api/tasks/${encodeURIComponent(taskId)}/messages`, |
| { |
| method: "POST", |
| body: JSON.stringify({ message }), |
| }, |
| ); |
| return task; |
| } |
|
|
| export async function patchTask( |
| taskId: string, |
| patch: Partial<TaskRecord>, |
| ): Promise<TaskRecord> { |
| const body: Record<string, unknown> = {}; |
| if (patch.title !== undefined) body.title = patch.title; |
| if (patch.messages !== undefined) body.messages = patch.messages; |
| if (patch.artifacts !== undefined) body.artifacts = patch.artifacts; |
| const { task } = await fetchJSON<{ task: TaskRecord }>( |
| `/api/tasks/${encodeURIComponent(taskId)}`, |
| { |
| method: "PATCH", |
| body: JSON.stringify(body), |
| }, |
| ); |
| return task; |
| } |
|
|
| |
|
|
| export async function listSkillsAPI(): Promise<{ skills: Skill[] }> { |
| return fetchJSON<{ skills: Skill[] }>("/api/skills"); |
| } |
|
|
| |
|
|
| export interface JobSubmitResult { |
| campaign_job_id: string; |
| hub_task_execution_id: string; |
| provider: string; |
| hub_response: Record<string, unknown>; |
| } |
|
|
| export interface BackendArtifact { |
| id: string; |
| hub_output_key: string; |
| phylo_tags?: string[]; |
| scientist_note?: string | null; |
| } |
|
|
| export interface JobStatusResult { |
| campaign_job_id: string; |
| campaign_id: string; |
| role: string; |
| submitted_by: string; |
| hub_task_execution_id: string; |
| hub: Record<string, unknown>; |
| artifacts: BackendArtifact[]; |
| } |
|
|
| export interface CostEstimate { |
| estimated_usd: number; |
| gpu_type: string; |
| minutes_used: number; |
| cost_alert: boolean; |
| } |
|
|
| |
| |
| export async function submitJobAPI( |
| campaignId: string, |
| formData: FormData, |
| ): Promise<JobSubmitResult> { |
| formData.set("campaign_id", campaignId); |
| const controller = new AbortController(); |
| |
| const timeout = setTimeout(() => controller.abort(), 60_000); |
| const devHeaders: Record<string, string> = IS_DEV |
| ? { "X-Dev-User": "dev@proteinea.local" } |
| : {}; |
| try { |
| const res = await fetch("/api/jobs/submit", { |
| method: "POST", |
| body: formData, |
| signal: controller.signal, |
| headers: devHeaders, |
| |
| }); |
| if (!res.ok) throw new Error(`API ${res.status}`); |
| return (await res.json()) as JobSubmitResult; |
| } finally { |
| clearTimeout(timeout); |
| } |
| } |
|
|
| |
| export async function getJobStatusAPI( |
| jobId: string, |
| ): Promise<JobStatusResult> { |
| return fetchJSON<JobStatusResult>(`/api/jobs/${encodeURIComponent(jobId)}`); |
| } |
|
|
| |
| export async function estimateToolCostAPI( |
| toolName: string, |
| gpuType?: string, |
| ): Promise<CostEstimate> { |
| return fetchJSON<CostEstimate>( |
| `/api/tools/${encodeURIComponent(toolName)}/estimate`, |
| { |
| method: "POST", |
| body: JSON.stringify(gpuType ? { gpu_type: gpuType } : {}), |
| }, |
| ); |
| } |
|
|
| |
|
|
| export async function listResourcesAPI(params?: { |
| type?: ResourceKind; |
| q?: string; |
| }): Promise<{ resources: ResourceRef[]; counts: Record<string, number> }> { |
| const qs = new URLSearchParams(); |
| if (params?.type) qs.set("type", params.type); |
| if (params?.q) qs.set("q", params.q); |
| const suffix = qs.toString() ? `?${qs.toString()}` : ""; |
| return fetchJSON<{ resources: ResourceRef[]; counts: Record<string, number> }>( |
| `/api/resources${suffix}`, |
| ); |
| } |
|
|
| |
|
|
| export async function searchDatasetsAPI( |
| query: Record<string, unknown>, |
| ): Promise<DatasetSearchResponse> { |
| return fetchJSON<DatasetSearchResponse>("/api/datasets/search", { |
| method: "POST", |
| body: JSON.stringify(query), |
| }); |
| } |
|
|
| export async function searchDatasetSourceAPI( |
| source: string, |
| query: Record<string, unknown>, |
| ): Promise<DatasetSearchResult[]> { |
| return fetchJSON<DatasetSearchResult[]>( |
| `/api/datasets/${encodeURIComponent(source)}/search`, |
| { |
| method: "POST", |
| body: JSON.stringify(query), |
| }, |
| ); |
| } |
|
|
| export async function importDatasetEntriesAPI( |
| entries: { source: string; entry_id: string }[], |
| ): Promise<{ artifacts: Artifact[]; errors: Record<string, string> }> { |
| return fetchJSON<{ artifacts: Artifact[]; errors: Record<string, string> }>( |
| "/api/datasets/import", |
| { |
| method: "POST", |
| body: JSON.stringify({ entries }), |
| }, |
| ); |
| } |
|
|
| export async function getDatasetSyncStatusAPI(): Promise<SyncStatusEntry[]> { |
| return fetchJSON<SyncStatusEntry[]>("/api/datasets/sync/status"); |
| } |
|
|
| |
|
|
| export interface EnrichmentResult { |
| entities: Record<string, string[]>; |
| context: Record<string, unknown>; |
| enriched_prompt: string; |
| } |
|
|
| export async function enrichContextAPI( |
| message: string, |
| maxResultsPerSource = 5, |
| ): Promise<EnrichmentResult> { |
| return fetchJSON<EnrichmentResult>("/api/context/enrich", { |
| method: "POST", |
| body: JSON.stringify({ message, max_results_per_source: maxResultsPerSource }), |
| }); |
| } |
|
|
| |
|
|
| export async function listBenchmarks(): Promise<{ benchmarks: Benchmark[]; count: number }> { |
| try { |
| return await fetchJSONAt<{ benchmarks: Benchmark[]; count: number }>("/api/benchmarks", undefined, LEGACY_BASE); |
| } catch { |
| return { benchmarks: BENCHMARKS, count: BENCHMARKS.length }; |
| } |
| } |
|
|
| export async function listModels(params?: { |
| capability?: string; |
| }): Promise<{ models: ModelEntry[]; count: number }> { |
| try { |
| const qs = params?.capability ? `?capability=${encodeURIComponent(params.capability)}` : ""; |
| return await fetchJSONAt<{ models: ModelEntry[]; count: number }>(`/api/models${qs}`, undefined, LEGACY_BASE); |
| } catch { |
| let models = MODELS; |
| if (params?.capability) { |
| const cap = params.capability; |
| models = models.filter((m) => m.capabilities.includes(cap)); |
| } |
| return { models, count: models.length }; |
| } |
| } |
|
|
| interface CampaignEntry { |
| campaign_id: string; |
| conversation_id: string; |
| active_jobs: Record<string, string>; |
| } |
|
|
| export async function listCampaigns(): Promise<{ campaigns: CampaignEntry[]; count: number }> { |
| try { |
| return await fetchJSONAt<{ campaigns: CampaignEntry[]; count: number }>("/api/campaigns", undefined, LEGACY_BASE); |
| } catch { |
| return { campaigns: [], count: 0 }; |
| } |
| } |
|
|
| |
|
|
| const PHYLO_BACKEND = process.env.NEXT_PUBLIC_PHYLO_BACKEND_URL || "http://127.0.0.1:8601"; |
|
|
| |
| export async function listPhyloCampaigns(): Promise<PhyloCampaign[]> { |
| return fetchJSONAt<PhyloCampaign[]>("/api/campaigns", undefined, PHYLO_BACKEND); |
| } |
|
|
| export interface PhyloCampaign { |
| id: string; |
| name: string; |
| target: string; |
| modality: string; |
| goal: string; |
| constraints?: Record<string, unknown>; |
| created_by: string; |
| created_at?: string; |
| } |
|
|
| |
| export async function createPhyloCampaign(params: { |
| name: string; |
| target: string; |
| modality: string; |
| goal: string; |
| constraints?: Record<string, unknown>; |
| }): Promise<{ id: string; name: string; target: string; [k: string]: unknown }> { |
| return fetchJSONAt<{ id: string; name: string; target: string }>( |
| "/api/campaigns", |
| { method: "POST", body: JSON.stringify(params) }, |
| PHYLO_BACKEND, |
| ); |
| } |
|
|
| |
| export async function submitPhyloJob( |
| campaignId: string, |
| formData: FormData, |
| ): Promise<{ |
| campaign_job_id: string; |
| hub_task_execution_id: string; |
| provider: string; |
| hub_response: unknown; |
| }> { |
| const controller = new AbortController(); |
| const timeout = setTimeout(() => controller.abort(), 120_000); |
| try { |
| const res = await fetch( |
| `${PHYLO_BACKEND}/api/campaigns/${encodeURIComponent(campaignId)}/jobs`, |
| { method: "POST", body: formData, signal: controller.signal }, |
| ); |
| if (!res.ok) throw new Error(`Phylo API ${res.status}: ${await res.text()}`); |
| return (await res.json()) as { |
| campaign_job_id: string; |
| hub_task_execution_id: string; |
| provider: string; |
| hub_response: unknown; |
| }; |
| } finally { |
| clearTimeout(timeout); |
| } |
| } |
|
|
| |
| export async function getPhyloJobStatus(jobId: string): Promise<{ |
| hub: { status: string; output_data?: unknown; cost_estimate?: number; [k: string]: unknown }; |
| artifacts: { name: string; url: string; mime?: string; bytes?: number }[]; |
| }> { |
| return fetchJSONAt( |
| `/api/jobs/${encodeURIComponent(jobId)}`, |
| undefined, |
| PHYLO_BACKEND, |
| ); |
| } |
|
|
| |
| export async function listPhyloTools(): Promise< |
| { endpoint_name: string; provider: string; [k: string]: unknown }[] |
| > { |
| return fetchJSONAt< |
| { endpoint_name: string; provider: string; [k: string]: unknown }[] |
| >("/api/tools", undefined, PHYLO_BACKEND); |
| } |
|
|
| |
| export async function estimateToolCost( |
| endpointName: string, |
| gpuType = "A100", |
| ): Promise<{ |
| estimated_usd: number; |
| gpu_type: string; |
| minutes_used: number; |
| cost_alert?: string; |
| }> { |
| return fetchJSONAt( |
| `/api/tools/${encodeURIComponent(endpointName)}/estimate`, |
| { method: "POST", body: JSON.stringify({ gpu_type: gpuType }) }, |
| PHYLO_BACKEND, |
| ); |
| } |
|
|
| |
|
|
| |
| |
| |
| export async function createConversation(): Promise<{ conversation_id: string }> { |
| |
| try { |
| const project = await ensureDefaultProject(); |
| const task = await createTask(project.id, "(new conversation)"); |
| return { conversation_id: task.id }; |
| } catch { |
| |
| return { conversation_id: `temp-${Date.now()}` }; |
| } |
| } |
|
|
| |
| |
| export function getStreamUrl(conversationId: string): string { |
| return `${PHYLO_BACKEND}/api/tasks/${encodeURIComponent(conversationId)}/messages/stream`; |
| } |
|
|
| |
|
|
| export async function uploadFiles( |
| taskId: string, |
| files: File[], |
| note?: string, |
| ): Promise<{ artifacts: Artifact[]; message: ChatMessage }> { |
| const form = new FormData(); |
| for (const file of files) { |
| form.append("files", file); |
| } |
| if (note) form.append("note", note); |
|
|
| const controller = new AbortController(); |
| const timeout = setTimeout(() => controller.abort(), 60_000); |
| const devHeaders: Record<string, string> = IS_DEV |
| ? { "X-Dev-User": "dev@proteinea.local" } |
| : {}; |
| try { |
| const res = await fetch( |
| `/api/tasks/${encodeURIComponent(taskId)}/upload`, |
| { |
| method: "POST", |
| body: form, |
| signal: controller.signal, |
| headers: devHeaders, |
| }, |
| ); |
| if (!res.ok) throw new Error(`API ${res.status}`); |
| return (await res.json()) as { artifacts: Artifact[]; message: ChatMessage }; |
| } finally { |
| clearTimeout(timeout); |
| } |
| } |
|
|
| |
|
|
| export interface KnowledgeBaseEntry { |
| id: string; |
| name: string; |
| description: string; |
| entryCount: number; |
| source: "embedded" | "dataset" | "custom"; |
| } |
|
|
| export async function listKnowledgeBases(): Promise<{ knowledgeBases: KnowledgeBaseEntry[] }> { |
| return fetchJSON<{ knowledgeBases: KnowledgeBaseEntry[] }>("/api/knowledge-bases"); |
| } |
|
|
| |
|
|
| export async function listTaskSkills(taskId: string): Promise<{ skills: unknown[]; taskId: string }> { |
| return fetchJSON<{ skills: unknown[]; taskId: string }>( |
| `/api/tasks/${encodeURIComponent(taskId)}/skills`, |
| ); |
| } |
| export interface DatasetListEntry { |
| id: string; |
| name: string; |
| source: string; |
| type: "structure" | "sequence" | "gene" | "benchmark" | "mixed"; |
| description: string; |
| entryCount: number; |
| lastUpdated: string | null; |
| sizeBytes: number; |
| } |
|
|
| export async function listDatasetsAPI(params?: { |
| q?: string; |
| type?: string; |
| }): Promise<{ datasets: DatasetListEntry[]; count: number }> { |
| const qs = new URLSearchParams(); |
| if (params?.q) qs.set("q", params.q); |
| if (params?.type) qs.set("type", params.type); |
| const suffix = qs.toString() ? `?${qs.toString()}` : ""; |
| return fetchJSON<{ datasets: DatasetListEntry[]; count: number }>( |
| `/api/datasets/list${suffix}`, |
| ); |
| } |
|
|