mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(.gitignore): anchor Python build artifacts; recover panel/src/lib (28 files)
The 'lib/' rule (intended for Python virtualenv at repo root) was matching panel/src/lib/, hiding the entire panel API client + utility tree from git. Anchored Python build-artifact rules to the repo root with a leading slash so they only match at the top level. Adds 28 panel/src/lib files that should have been tracked from day one.
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* A2A (Agent-to-Agent) Protocol API Client
|
||||
*
|
||||
* API functions for agent-to-agent communication protocol.
|
||||
*/
|
||||
|
||||
import api from "./client";
|
||||
import { isMockMode } from "@/lib/mock-data";
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
export interface A2AMessage {
|
||||
id: string;
|
||||
from_agent_id: string;
|
||||
to_agent_id: string;
|
||||
content: string;
|
||||
message_type: string;
|
||||
timestamp: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface A2AMessageSendRequest {
|
||||
to_agent_id: string;
|
||||
content: string;
|
||||
message_type?: string;
|
||||
task_id?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface A2AMessageResponse {
|
||||
message_id: string;
|
||||
status: string;
|
||||
delivered_at?: string;
|
||||
}
|
||||
|
||||
export interface A2ATask {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
assigned_to: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface A2AAgentCard {
|
||||
agent_id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
capabilities: string[];
|
||||
status: string;
|
||||
current_task_id: string | null;
|
||||
}
|
||||
|
||||
export interface A2AStreamChunk {
|
||||
chunk_id: string;
|
||||
content: string;
|
||||
is_final: boolean;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API Client
|
||||
// =============================================================================
|
||||
|
||||
export const a2aApi = {
|
||||
// ===========================================================================
|
||||
// MESSAGE ENDPOINTS
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Send a message to another agent
|
||||
*/
|
||||
sendMessage: async (request: A2AMessageSendRequest): Promise<A2AMessageResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
message_id: `msg-${Date.now()}`,
|
||||
status: "delivered",
|
||||
delivered_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<A2AMessageResponse>("/a2a/message/send", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stream a message to another agent (for long content)
|
||||
*/
|
||||
streamMessage: async (request: A2AMessageSendRequest): Promise<A2AMessageResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
message_id: `msg-${Date.now()}`,
|
||||
status: "streaming",
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<A2AMessageResponse>("/a2a/message/stream", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// TASK ENDPOINTS
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* List tasks visible via A2A protocol
|
||||
*/
|
||||
listTasks: async (): Promise<A2ATask[]> => {
|
||||
if (isMockMode()) {
|
||||
return [];
|
||||
}
|
||||
const { data } = await api.get<A2ATask[]>("/a2a/tasks");
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a specific task via A2A protocol
|
||||
*/
|
||||
getTask: async (taskId: string): Promise<A2ATask> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
id: taskId,
|
||||
title: "Mock Task",
|
||||
status: "in_progress",
|
||||
assigned_to: null,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
const { data } = await api.get<A2ATask>(`/a2a/tasks/${taskId}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Subscribe to task updates (returns SSE stream URL)
|
||||
* Note: This endpoint returns Server-Sent Events, handle appropriately
|
||||
*/
|
||||
subscribeToTask: (taskId: string): string => {
|
||||
// Returns the URL for SSE subscription
|
||||
return `/a2a/tasks/${taskId}/subscribe`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel a task via A2A protocol
|
||||
*/
|
||||
cancelTask: async (taskId: string): Promise<{ status: string; task_id: string }> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
status: "cancelled",
|
||||
task_id: taskId,
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<{ status: string; task_id: string }>(`/a2a/tasks/${taskId}/cancel`);
|
||||
return data;
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// AGENT ENDPOINTS
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* List all agents available via A2A protocol
|
||||
*/
|
||||
listAgents: async (): Promise<A2AAgentCard[]> => {
|
||||
if (isMockMode()) {
|
||||
return [];
|
||||
}
|
||||
const { data } = await api.get<A2AAgentCard[]>("/a2a/agents");
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get agent card (profile/capabilities)
|
||||
*/
|
||||
getAgentCard: async (agentId: string): Promise<A2AAgentCard> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
agent_id: agentId,
|
||||
name: "Mock Agent",
|
||||
role: "developer",
|
||||
capabilities: ["coding", "testing"],
|
||||
status: "idle",
|
||||
current_task_id: null,
|
||||
};
|
||||
}
|
||||
const { data } = await api.get<A2AAgentCard>(`/a2a/agents/${agentId}/card`);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Agents API Client
|
||||
*
|
||||
* Fetches agent data from the backend API.
|
||||
*/
|
||||
|
||||
import api from "./client";
|
||||
import type { AgentRole, Team } from "@/types";
|
||||
import { isMockMode, mockAgents } from "@/lib/mock-data";
|
||||
|
||||
export interface AgentDefinition {
|
||||
id: string; // slug from backend (e.g., "be-dev-1")
|
||||
name: string;
|
||||
role: AgentRole | null;
|
||||
team: Team | null;
|
||||
}
|
||||
|
||||
interface AgentApiResponse {
|
||||
id: string; // UUID (not used for definitions)
|
||||
name: string;
|
||||
slug: string;
|
||||
role: string;
|
||||
team: string | null;
|
||||
}
|
||||
|
||||
export const agentsApi = {
|
||||
/**
|
||||
* Get all agents from the API
|
||||
*/
|
||||
getAll: async (): Promise<AgentDefinition[]> => {
|
||||
if (isMockMode()) {
|
||||
return mockAgents.map((a) => ({
|
||||
id: a.slug || a.id,
|
||||
name: a.name,
|
||||
role: a.role,
|
||||
team: a.team,
|
||||
}));
|
||||
}
|
||||
|
||||
const response = await api.get<AgentApiResponse[]>("/agents");
|
||||
const data = response.data;
|
||||
|
||||
// Handle unexpected response formats
|
||||
if (!data || !Array.isArray(data)) {
|
||||
console.warn("Unexpected agents API response:", data);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.map((a) => ({
|
||||
id: a.slug || a.id, // Use slug as ID, fallback to UUID
|
||||
name: a.name || "Unknown",
|
||||
role: (a.role as AgentRole) || null,
|
||||
team: (a.team as Team) || null,
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single agent by ID or slug
|
||||
*/
|
||||
getOne: async (idOrSlug: string): Promise<AgentDefinition> => {
|
||||
const response = await api.get<AgentApiResponse>(`/agents/${idOrSlug}`);
|
||||
const a = response.data;
|
||||
return {
|
||||
id: a.slug,
|
||||
name: a.name,
|
||||
role: a.role as AgentRole,
|
||||
team: a.team as Team | null,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
import api from "./client";
|
||||
import type { Channel, PaginatedResponse, Group } from "@/types";
|
||||
import { ChannelType } from "@/types";
|
||||
import { isMockMode, mockChannels, mockGroups } from "@/lib/mock-data";
|
||||
|
||||
export interface ChannelFilters {
|
||||
type?: string;
|
||||
is_private?: boolean;
|
||||
}
|
||||
|
||||
export interface ChannelCreate {
|
||||
name: string;
|
||||
slug: string;
|
||||
type?: string;
|
||||
description?: string;
|
||||
topic?: string;
|
||||
is_private?: boolean;
|
||||
}
|
||||
|
||||
export interface ChannelUpdate {
|
||||
name?: string;
|
||||
description?: string;
|
||||
topic?: string;
|
||||
is_archived?: boolean;
|
||||
}
|
||||
|
||||
export const channelsApi = {
|
||||
// List all channels
|
||||
list: async (filters?: ChannelFilters): Promise<Channel[]> => {
|
||||
if (isMockMode()) {
|
||||
let channels = [...mockChannels] as Channel[];
|
||||
if (filters?.type) {
|
||||
channels = channels.filter((c) => c.type === filters.type);
|
||||
}
|
||||
if (filters?.is_private !== undefined) {
|
||||
channels = channels.filter((c) => c.is_private === filters.is_private);
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
const { data } = await api.get<PaginatedResponse<Channel>>("/channels", { params: filters });
|
||||
return data.items;
|
||||
},
|
||||
|
||||
// Get channel by ID
|
||||
get: async (channelId: string): Promise<Channel> => {
|
||||
if (isMockMode()) {
|
||||
const channel = mockChannels.find((c) => c.id === channelId);
|
||||
if (channel) return channel as Channel;
|
||||
throw new Error("Channel not found");
|
||||
}
|
||||
const { data } = await api.get<Channel>("/channels/" + channelId);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get channel by slug
|
||||
getBySlug: async (slug: string): Promise<Channel> => {
|
||||
if (isMockMode()) {
|
||||
const channel = mockChannels.find((c) => c.slug === slug);
|
||||
if (channel) return channel as Channel;
|
||||
throw new Error("Channel not found");
|
||||
}
|
||||
// Backend uses query param filter, not path segment
|
||||
const { data } = await api.get<PaginatedResponse<Channel>>("/channels", {
|
||||
params: { slug },
|
||||
});
|
||||
if (!data.items.length) {
|
||||
throw new Error("Channel not found");
|
||||
}
|
||||
return data.items[0];
|
||||
},
|
||||
|
||||
// Get groups for a channel
|
||||
getGroups: async (channelId: string): Promise<Group[]> => {
|
||||
if (isMockMode()) {
|
||||
return mockGroups as Group[];
|
||||
}
|
||||
const { data } = await api.get<Group[]>("/channels/" + channelId + "/groups");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Create a new channel (PM/CEO only)
|
||||
create: async (channel: ChannelCreate): Promise<Channel> => {
|
||||
if (isMockMode()) {
|
||||
const newChannel: Channel = {
|
||||
id: `channel-${Date.now()}`,
|
||||
name: channel.name,
|
||||
slug: channel.slug,
|
||||
type: (channel.type as ChannelType) || ChannelType.CELL,
|
||||
description: channel.description || null,
|
||||
topic: channel.topic || null,
|
||||
is_private: channel.is_private || false,
|
||||
is_archived: false,
|
||||
member_count: 0,
|
||||
message_count: 0,
|
||||
group_count: 0,
|
||||
can_write: true,
|
||||
};
|
||||
(mockChannels as Channel[]).push(newChannel);
|
||||
return newChannel;
|
||||
}
|
||||
const { data } = await api.post<Channel>("/channels", channel);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Update a channel (PM/CEO only)
|
||||
update: async (channelId: string, updates: ChannelUpdate): Promise<Channel> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockChannels.findIndex((c) => c.id === channelId);
|
||||
if (idx === -1) throw new Error("Channel not found");
|
||||
const updated = { ...mockChannels[idx], ...updates } as Channel;
|
||||
(mockChannels as Channel[])[idx] = updated;
|
||||
return updated;
|
||||
}
|
||||
const { data } = await api.patch<Channel>("/channels/" + channelId, updates);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Add a member to a channel (PM/CEO only)
|
||||
addMember: async (channelId: string, agentId: string): Promise<void> => {
|
||||
if (isMockMode()) {
|
||||
return;
|
||||
}
|
||||
await api.post("/channels/" + channelId + "/add-member", { agent_id: agentId });
|
||||
},
|
||||
|
||||
// Remove a member from a channel (PM/CEO only)
|
||||
removeMember: async (channelId: string, agentId: string): Promise<void> => {
|
||||
if (isMockMode()) {
|
||||
return;
|
||||
}
|
||||
await api.delete("/channels/" + channelId + "/remove-member", {
|
||||
data: { agent_id: agentId },
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import axios, { AxiosInstance, AxiosError } from "axios";
|
||||
import { API_URL, CEO_AGENT_ID, CEO_ROLE } from "@/lib/constants";
|
||||
|
||||
// Create axios instance with default config
|
||||
const api: AxiosInstance = axios.create({
|
||||
baseURL: API_URL,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout: 60000, // Increased to 60s for long operations like reindexing
|
||||
});
|
||||
|
||||
// Request interceptor to add auth headers and logging
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// Add agent context headers for API authorization
|
||||
config.headers["X-Agent-ID"] = CEO_AGENT_ID;
|
||||
config.headers["X-Agent-Role"] = CEO_ROLE;
|
||||
|
||||
// Log request in development
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
console.error("[API] Request setup error:", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor for comprehensive error handling
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
// Log successful responses in development
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(`[API] ✓ ${response.config.method?.toUpperCase()} ${response.config.url}`);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
// Extract error details
|
||||
const status = error.response?.status;
|
||||
const url = error.config?.url;
|
||||
const method = error.config?.method?.toUpperCase();
|
||||
const errorData = error.response?.data as Record<string, unknown> | undefined;
|
||||
const errorDetail = errorData?.detail || error.message;
|
||||
|
||||
// Log comprehensive error info
|
||||
console.error(`[API] ✗ ${method} ${url}`, {
|
||||
status,
|
||||
detail: errorDetail,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
// Specific error handling with helpful messages
|
||||
if (error.code === "ECONNABORTED") {
|
||||
console.error("[API] Request timed out - backend may be overloaded or unavailable");
|
||||
} else if (error.code === "ERR_NETWORK") {
|
||||
console.error("[API] Network error - check if backend is running at", API_URL);
|
||||
} else if (status === 401) {
|
||||
console.error("[API] Unauthorized - check API authentication headers");
|
||||
} else if (status === 403) {
|
||||
console.error("[API] Forbidden - insufficient permissions for this action");
|
||||
} else if (status === 404) {
|
||||
console.error("[API] Not found - endpoint may not exist:", url);
|
||||
} else if (status === 422) {
|
||||
console.error("[API] Validation error - request data is invalid:", errorDetail);
|
||||
} else if (status && status >= 500) {
|
||||
console.error("[API] Server error - backend encountered an internal error");
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper to extract user-friendly error message from API error
|
||||
*/
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const axiosError = error as AxiosError<{ detail?: string }>;
|
||||
|
||||
// Check for specific error codes
|
||||
if (error.code === "ECONNABORTED") {
|
||||
return "Request timed out. The server may be busy.";
|
||||
}
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
return "Cannot connect to server. Check if the backend is running.";
|
||||
}
|
||||
|
||||
// Check for API error response
|
||||
if (axiosError.response?.data?.detail) {
|
||||
return String(axiosError.response.data.detail);
|
||||
}
|
||||
|
||||
// Check for HTTP status
|
||||
const status = axiosError.response?.status;
|
||||
if (status === 401) return "Authentication required. Please refresh the page.";
|
||||
if (status === 403) return "Permission denied for this action.";
|
||||
if (status === 404) return "The requested resource was not found.";
|
||||
if (status === 422) return "Invalid request data.";
|
||||
if (status && status >= 500) return "Server error. Please try again later.";
|
||||
}
|
||||
|
||||
// Generic error
|
||||
return error instanceof Error ? error.message : "An unexpected error occurred";
|
||||
}
|
||||
|
||||
export { api, API_URL };
|
||||
export default api;
|
||||
@@ -0,0 +1,568 @@
|
||||
import api from "./client";
|
||||
import { Team, TaskStatus } from "@/types";
|
||||
import type {
|
||||
KanbanBoard,
|
||||
AuditorDashboard,
|
||||
AuditorFlag,
|
||||
AuditorReport,
|
||||
FlagSeverity,
|
||||
CEOOverview as CEOOverviewType,
|
||||
Task,
|
||||
} from "@/types";
|
||||
import {
|
||||
isMockMode,
|
||||
mockDashboardStats,
|
||||
mockTeamHealth,
|
||||
getMockRecentActivity,
|
||||
mockAuditorDashboard,
|
||||
mockAuditorFlags,
|
||||
mockAuditorReports,
|
||||
mockTasks,
|
||||
mockOrchestratorStatus,
|
||||
} from "@/lib/mock-data";
|
||||
|
||||
// Re-export types from @/types for backward compatibility
|
||||
export type { CEOOverviewType as CEOOverview };
|
||||
|
||||
export interface TeamHealth {
|
||||
team: Team;
|
||||
health_score: number;
|
||||
active_tasks: number;
|
||||
blocked_tasks: number;
|
||||
completed_today: number;
|
||||
}
|
||||
|
||||
export interface MetricsSummary {
|
||||
velocity: VelocityMetric;
|
||||
blockers: BlockerMetric;
|
||||
communication: CommunicationMetric;
|
||||
agents: AgentMetric;
|
||||
}
|
||||
|
||||
export interface VelocityMetric {
|
||||
tasks_completed_today: number;
|
||||
tasks_completed_week: number;
|
||||
average_completion_time_hours: number;
|
||||
}
|
||||
|
||||
export interface BlockerMetric {
|
||||
total_blocked: number;
|
||||
blocked_by_team: Record<string, number>;
|
||||
longest_blocked_hours: number;
|
||||
}
|
||||
|
||||
export interface CommunicationMetric {
|
||||
messages_today: number;
|
||||
active_channels: number;
|
||||
notifications_pending: number;
|
||||
}
|
||||
|
||||
export interface AgentMetric {
|
||||
total_agents: number;
|
||||
running: number;
|
||||
idle: number;
|
||||
waiting: number;
|
||||
errors: number;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUDITOR API TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface CreateFlagRequest {
|
||||
severity: FlagSeverity;
|
||||
category: string;
|
||||
title: string;
|
||||
description: string;
|
||||
related_task_id?: string;
|
||||
related_agent_id?: string;
|
||||
}
|
||||
|
||||
export interface CreateReportRequest {
|
||||
report_type: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
sections: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
// Helper to create mock kanban board from tasks
|
||||
function createMockKanbanBoard(tasks: Task[], team?: Team): KanbanBoard {
|
||||
const filteredTasks = team ? tasks.filter((t) => t.team === team) : tasks;
|
||||
const pendingTasks = filteredTasks.filter((t) => t.status === TaskStatus.PENDING);
|
||||
const inProgressTasks = filteredTasks.filter((t) => t.status === TaskStatus.IN_PROGRESS);
|
||||
const blockedTasks = filteredTasks.filter((t) => t.status === TaskStatus.BLOCKED);
|
||||
const awaitingQaTasks = filteredTasks.filter((t) => t.status === TaskStatus.AWAITING_QA);
|
||||
const completedTasks = filteredTasks.filter((t) => t.status === TaskStatus.COMPLETED);
|
||||
|
||||
return {
|
||||
columns: [
|
||||
{
|
||||
id: "pending",
|
||||
title: "Pending",
|
||||
status: TaskStatus.PENDING,
|
||||
tasks: pendingTasks,
|
||||
count: pendingTasks.length,
|
||||
},
|
||||
{
|
||||
id: "in_progress",
|
||||
title: "In Progress",
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
tasks: inProgressTasks,
|
||||
count: inProgressTasks.length,
|
||||
},
|
||||
{
|
||||
id: "blocked",
|
||||
title: "Blocked",
|
||||
status: TaskStatus.BLOCKED,
|
||||
tasks: blockedTasks,
|
||||
count: blockedTasks.length,
|
||||
},
|
||||
{
|
||||
id: "awaiting_qa",
|
||||
title: "Awaiting QA",
|
||||
status: TaskStatus.AWAITING_QA,
|
||||
tasks: awaitingQaTasks,
|
||||
count: awaitingQaTasks.length,
|
||||
},
|
||||
{
|
||||
id: "completed",
|
||||
title: "Completed",
|
||||
status: TaskStatus.COMPLETED,
|
||||
tasks: completedTasks,
|
||||
count: completedTasks.length,
|
||||
},
|
||||
],
|
||||
total_tasks: filteredTasks.length,
|
||||
};
|
||||
}
|
||||
|
||||
export const dashboardApi = {
|
||||
// Get CEO overview
|
||||
getCeoOverview: async (): Promise<CEOOverviewType> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
health_status: mockTeamHealth,
|
||||
key_metrics: {
|
||||
total_tasks: mockDashboardStats.total_tasks,
|
||||
tasks_in_progress: mockDashboardStats.tasks_in_progress,
|
||||
tasks_blocked: mockDashboardStats.tasks_blocked,
|
||||
tasks_completed_today: mockDashboardStats.tasks_completed_today,
|
||||
active_agents: mockDashboardStats.active_agents,
|
||||
},
|
||||
auditor_alerts: {},
|
||||
roadmap_progress: {
|
||||
phase_1: 100,
|
||||
phase_2: 75,
|
||||
phase_3: 25,
|
||||
},
|
||||
};
|
||||
}
|
||||
const { data } = await api.get<CEOOverviewType>("/dashboard/ceo");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get metrics - individual endpoints
|
||||
getVelocityMetrics: async (): Promise<VelocityMetric> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
tasks_completed_today: mockDashboardStats.tasks_completed_today,
|
||||
tasks_completed_week: 15,
|
||||
average_completion_time_hours: 4.5,
|
||||
};
|
||||
}
|
||||
const { data } = await api.get<VelocityMetric>("/dashboard/metrics/velocity");
|
||||
return data;
|
||||
},
|
||||
|
||||
getBlockerMetrics: async (): Promise<BlockerMetric> => {
|
||||
if (isMockMode()) {
|
||||
const blockedTasks = mockTasks.filter((t) => t.status === TaskStatus.BLOCKED);
|
||||
return {
|
||||
total_blocked: blockedTasks.length,
|
||||
blocked_by_team: {
|
||||
backend: blockedTasks.filter((t) => t.team === Team.BACKEND).length,
|
||||
frontend: blockedTasks.filter((t) => t.team === Team.FRONTEND).length,
|
||||
ux_ui: blockedTasks.filter((t) => t.team === Team.UX_UI).length,
|
||||
marketing: blockedTasks.filter((t) => t.team === Team.MARKETING).length,
|
||||
},
|
||||
longest_blocked_hours: 24,
|
||||
};
|
||||
}
|
||||
const { data } = await api.get<BlockerMetric>("/dashboard/metrics/blockers");
|
||||
return data;
|
||||
},
|
||||
|
||||
getCommunicationMetrics: async (): Promise<CommunicationMetric> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
messages_today: 45,
|
||||
active_channels: 5,
|
||||
notifications_pending: 3,
|
||||
};
|
||||
}
|
||||
const { data } = await api.get<CommunicationMetric>("/dashboard/metrics/communication");
|
||||
return data;
|
||||
},
|
||||
|
||||
getHealthMetrics: async () => {
|
||||
if (isMockMode()) {
|
||||
return mockTeamHealth;
|
||||
}
|
||||
const { data } = await api.get("/dashboard/metrics/health");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get kanban board for a team (dev view)
|
||||
getKanbanDev: async (team: Team): Promise<KanbanBoard> => {
|
||||
if (isMockMode()) {
|
||||
return createMockKanbanBoard(mockTasks, team);
|
||||
}
|
||||
const { data } = await api.get<KanbanBoard>("/kanban/dev/" + team);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get QA kanban board
|
||||
getKanbanQa: async (team: Team): Promise<KanbanBoard> => {
|
||||
if (isMockMode()) {
|
||||
return createMockKanbanBoard(mockTasks, team);
|
||||
}
|
||||
const { data } = await api.get<KanbanBoard>("/kanban/qa/" + team);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get PM kanban board (cross-team view)
|
||||
getKanbanPm: async (): Promise<KanbanBoard> => {
|
||||
if (isMockMode()) {
|
||||
return createMockKanbanBoard(mockTasks);
|
||||
}
|
||||
const { data } = await api.get<KanbanBoard>("/kanban/main-pm");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get agent status
|
||||
getAgentStatus: async () => {
|
||||
if (isMockMode()) {
|
||||
return mockOrchestratorStatus;
|
||||
}
|
||||
const { data } = await api.get("/dashboard/agents/status");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get recent activity
|
||||
getRecentActivity: async (hours: number = 24, limit: number = 50) => {
|
||||
if (isMockMode()) {
|
||||
return getMockRecentActivity().slice(0, limit);
|
||||
}
|
||||
const { data } = await api.get<{ period_hours: number; activity: unknown[] }>("/dashboard/activity/recent", {
|
||||
params: { hours, limit },
|
||||
});
|
||||
// Backend returns { period_hours, activity }, extract the activity array
|
||||
return data.activity ?? [];
|
||||
},
|
||||
|
||||
// =============================================================================
|
||||
// AUDITOR DASHBOARD ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
// Get complete auditor dashboard
|
||||
getAuditorDashboard: async (): Promise<AuditorDashboard> => {
|
||||
if (isMockMode()) {
|
||||
return mockAuditorDashboard as AuditorDashboard;
|
||||
}
|
||||
const { data } = await api.get<AuditorDashboard>("/dashboard/auditor");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get auditor flags
|
||||
getAuditorFlags: async (params?: {
|
||||
severity?: FlagSeverity;
|
||||
resolved?: boolean;
|
||||
}): Promise<AuditorFlag[]> => {
|
||||
if (isMockMode()) {
|
||||
let flags = [...mockAuditorFlags] as AuditorFlag[];
|
||||
if (params?.severity) {
|
||||
flags = flags.filter((f) => f.severity === params.severity);
|
||||
}
|
||||
if (params?.resolved !== undefined) {
|
||||
flags = flags.filter((f) => (params.resolved ? f.resolved_at : !f.resolved_at));
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
const { data } = await api.get<AuditorFlag[]>("/dashboard/auditor/flags", {
|
||||
params,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// Create an auditor flag
|
||||
createAuditorFlag: async (
|
||||
request: CreateFlagRequest
|
||||
): Promise<AuditorFlag> => {
|
||||
if (isMockMode()) {
|
||||
const newFlag: AuditorFlag = {
|
||||
id: `flag-${Date.now()}`,
|
||||
severity: request.severity,
|
||||
category: request.category,
|
||||
title: request.title,
|
||||
description: request.description,
|
||||
related_task_id: request.related_task_id ?? null,
|
||||
related_agent_id: request.related_agent_id ?? null,
|
||||
created_at: new Date().toISOString(),
|
||||
resolved_at: null,
|
||||
notes: null,
|
||||
};
|
||||
(mockAuditorFlags as AuditorFlag[]).push(newFlag);
|
||||
return newFlag;
|
||||
}
|
||||
const { data } = await api.post<AuditorFlag>(
|
||||
"/dashboard/auditor/flags",
|
||||
request
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Resolve an auditor flag
|
||||
resolveAuditorFlag: async (
|
||||
flagId: string,
|
||||
notes?: string
|
||||
): Promise<{ status: string; flag_id: string }> => {
|
||||
if (isMockMode()) {
|
||||
const flags = mockAuditorFlags as AuditorFlag[];
|
||||
const idx = flags.findIndex((f) => f.id === flagId);
|
||||
if (idx !== -1) {
|
||||
flags[idx] = {
|
||||
...flags[idx],
|
||||
resolved_at: new Date().toISOString(),
|
||||
notes: notes ?? null,
|
||||
};
|
||||
return { status: "resolved", flag_id: flagId };
|
||||
}
|
||||
throw new Error("Flag not found");
|
||||
}
|
||||
const { data } = await api.put(`/dashboard/auditor/flags/${flagId}/resolve`, null, {
|
||||
params: { notes },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get auditor reports
|
||||
getAuditorReports: async (params?: {
|
||||
report_type?: string;
|
||||
limit?: number;
|
||||
}): Promise<AuditorReport[]> => {
|
||||
if (isMockMode()) {
|
||||
let reports = [...mockAuditorReports] as AuditorReport[];
|
||||
if (params?.report_type) {
|
||||
reports = reports.filter((r) => r.report_type === params.report_type);
|
||||
}
|
||||
if (params?.limit) {
|
||||
reports = reports.slice(0, params.limit);
|
||||
}
|
||||
return reports;
|
||||
}
|
||||
const { data } = await api.get<AuditorReport[]>(
|
||||
"/dashboard/auditor/reports",
|
||||
{ params }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Create an auditor report
|
||||
createAuditorReport: async (
|
||||
request: CreateReportRequest
|
||||
): Promise<AuditorReport> => {
|
||||
if (isMockMode()) {
|
||||
const newReport: AuditorReport = {
|
||||
id: `report-${Date.now()}`,
|
||||
report_type: request.report_type,
|
||||
title: request.title,
|
||||
summary: request.summary,
|
||||
sections: request.sections,
|
||||
created_at: new Date().toISOString(),
|
||||
sent_at: null,
|
||||
};
|
||||
(mockAuditorReports as AuditorReport[]).push(newReport);
|
||||
return newReport;
|
||||
}
|
||||
const { data } = await api.post<AuditorReport>(
|
||||
"/dashboard/auditor/reports",
|
||||
request
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Send a report to CEO
|
||||
sendAuditorReport: async (
|
||||
reportId: string
|
||||
): Promise<{ status: string; report_id: string }> => {
|
||||
if (isMockMode()) {
|
||||
const reports = mockAuditorReports as AuditorReport[];
|
||||
const idx = reports.findIndex((r) => r.id === reportId);
|
||||
if (idx !== -1) {
|
||||
reports[idx] = {
|
||||
...reports[idx],
|
||||
sent_at: new Date().toISOString(),
|
||||
};
|
||||
return { status: "sent", report_id: reportId };
|
||||
}
|
||||
throw new Error("Report not found");
|
||||
}
|
||||
const { data } = await api.post(
|
||||
`/dashboard/auditor/reports/${reportId}/send`
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
// =============================================================================
|
||||
// CEO DETAIL ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
// Get detailed team metrics
|
||||
getCeoTeamDetails: async () => {
|
||||
if (isMockMode()) {
|
||||
return mockTeamHealth.map((th) => ({
|
||||
...th,
|
||||
velocity_7d: 10,
|
||||
avg_completion_time: 4.5,
|
||||
agent_count: 5,
|
||||
}));
|
||||
}
|
||||
const { data } = await api.get("/dashboard/ceo/teams");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get blocker details for CEO
|
||||
getCeoBlockerDetails: async () => {
|
||||
if (isMockMode()) {
|
||||
const blockedTasks = mockTasks.filter((t) => t.status === TaskStatus.BLOCKED);
|
||||
return {
|
||||
total_blocked: blockedTasks.length,
|
||||
blockers: blockedTasks.map((t) => ({
|
||||
task_id: t.id,
|
||||
title: t.title,
|
||||
team: t.team,
|
||||
blocked_hours: 24,
|
||||
blocker_reason: "Dependency not resolved",
|
||||
})),
|
||||
};
|
||||
}
|
||||
const { data } = await api.get("/dashboard/ceo/blockers");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get velocity metrics for CEO
|
||||
getCeoVelocity: async (days: number = 7) => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
period_days: days,
|
||||
total_completed: 15,
|
||||
by_team: {
|
||||
backend: 6,
|
||||
frontend: 5,
|
||||
ux_ui: 4,
|
||||
marketing: 0,
|
||||
},
|
||||
daily_breakdown: Array.from({ length: days }, (_, i) => ({
|
||||
date: new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString().split("T")[0],
|
||||
completed: Math.floor(Math.random() * 5) + 1,
|
||||
})),
|
||||
};
|
||||
}
|
||||
const { data } = await api.get("/dashboard/ceo/velocity", {
|
||||
params: { days },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// =============================================================================
|
||||
// KANBAN ENDPOINTS (Role-specific views)
|
||||
// =============================================================================
|
||||
|
||||
// Get documenter kanban board
|
||||
getKanbanDocumenter: async (team: Team): Promise<KanbanBoard> => {
|
||||
if (isMockMode()) {
|
||||
return createMockKanbanBoard(mockTasks, team);
|
||||
}
|
||||
const { data } = await api.get<KanbanBoard>(`/kanban/documenter/${team}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get cell PM kanban board
|
||||
getKanbanCellPm: async (team: Team): Promise<KanbanBoard> => {
|
||||
if (isMockMode()) {
|
||||
return createMockKanbanBoard(mockTasks, team);
|
||||
}
|
||||
const { data } = await api.get<KanbanBoard>(`/kanban/pm/${team}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get board-level roadmap kanban
|
||||
getKanbanBoard: async (): Promise<KanbanBoard> => {
|
||||
if (isMockMode()) {
|
||||
return createMockKanbanBoard(mockTasks);
|
||||
}
|
||||
const { data } = await api.get<KanbanBoard>("/kanban/board");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get kanban statistics
|
||||
getKanbanStats: async (team?: Team) => {
|
||||
if (isMockMode()) {
|
||||
const tasks = team ? mockTasks.filter((t) => t.team === team) : mockTasks;
|
||||
return {
|
||||
total: tasks.length,
|
||||
by_status: {
|
||||
pending: tasks.filter((t) => t.status === TaskStatus.PENDING).length,
|
||||
in_progress: tasks.filter((t) => t.status === TaskStatus.IN_PROGRESS).length,
|
||||
blocked: tasks.filter((t) => t.status === TaskStatus.BLOCKED).length,
|
||||
awaiting_qa: tasks.filter((t) => t.status === TaskStatus.AWAITING_QA).length,
|
||||
completed: tasks.filter((t) => t.status === TaskStatus.COMPLETED).length,
|
||||
},
|
||||
};
|
||||
}
|
||||
const { data } = await api.get("/kanban/stats", { params: { team } });
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get metrics for a specific agent
|
||||
getAgentMetrics: async (agentId: string) => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
agent_id: agentId,
|
||||
tasks_completed: 10,
|
||||
tasks_in_progress: 2,
|
||||
avg_completion_time_hours: 8.5,
|
||||
quality_score: 0.92,
|
||||
};
|
||||
}
|
||||
const { data } = await api.get(`/dashboard/metrics/agent/${agentId}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get metrics for a specific team
|
||||
getTeamMetrics: async (team: Team) => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
team,
|
||||
tasks_total: 25,
|
||||
tasks_completed: 15,
|
||||
tasks_in_progress: 5,
|
||||
tasks_blocked: 2,
|
||||
velocity: 8.5,
|
||||
quality_score: 0.88,
|
||||
};
|
||||
}
|
||||
const { data } = await api.get(`/dashboard/metrics/team/${team}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get kanban board for any team (generic endpoint)
|
||||
getKanbanForTeam: async (team: Team): Promise<KanbanBoard> => {
|
||||
if (isMockMode()) {
|
||||
const teamTasks = mockTasks.filter((t) => t.team === team);
|
||||
return createMockKanbanBoard(teamTasks);
|
||||
}
|
||||
const { data } = await api.get<KanbanBoard>(`/dashboard/kanban/${team}`);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Git API Client
|
||||
*
|
||||
* API functions for Git operations (branches, commits, PRs).
|
||||
*/
|
||||
|
||||
import api from "./client";
|
||||
import { isMockMode } from "@/lib/mock-data";
|
||||
import type {
|
||||
GitStatusResponse,
|
||||
GitLogResponse,
|
||||
GitBranchListResponse,
|
||||
GitDiffResponse,
|
||||
GitCommitRequest,
|
||||
GitCommitResponse,
|
||||
GitPushRequest,
|
||||
GitPushResponse,
|
||||
GitCreateBranchRequest,
|
||||
GitCreateBranchResponse,
|
||||
GitCheckoutRequest,
|
||||
GitCheckoutResponse,
|
||||
GitCreatePRRequest,
|
||||
GitCreatePRResponse,
|
||||
GitMergePRRequest,
|
||||
GitMergePRResponse,
|
||||
} from "@/types/git";
|
||||
|
||||
// =============================================================================
|
||||
// Mock Data
|
||||
// =============================================================================
|
||||
|
||||
const mockGitStatus: GitStatusResponse = {
|
||||
project_slug: "roboco",
|
||||
current_branch: "feature/backend/abc12345",
|
||||
has_changes: true,
|
||||
staged_files: ["src/api/routes/tasks.py"],
|
||||
unstaged_files: ["src/services/task_service.py"],
|
||||
untracked_files: [],
|
||||
ahead: 2,
|
||||
behind: 0,
|
||||
};
|
||||
|
||||
const mockGitLog: GitLogResponse = {
|
||||
project_slug: "roboco",
|
||||
branch: "feature/backend/abc12345",
|
||||
commits: [
|
||||
{
|
||||
hash: "abc123456789abcdef",
|
||||
short_hash: "abc1234",
|
||||
message: "[abc12345] Add task status endpoint",
|
||||
author: "backend-dev",
|
||||
date: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockBranches: GitBranchListResponse = {
|
||||
project_slug: "roboco",
|
||||
current_branch: "feature/backend/abc12345",
|
||||
branches: [
|
||||
{ name: "main", is_current: false, is_remote: false, last_commit: "def456" },
|
||||
{ name: "feature/backend/abc12345", is_current: true, is_remote: false, last_commit: "abc123" },
|
||||
],
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// API Client
|
||||
// =============================================================================
|
||||
|
||||
export const gitApi = {
|
||||
// ===========================================================================
|
||||
// READ-ONLY OPERATIONS
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Get git status for a project
|
||||
*/
|
||||
getStatus: async (projectSlug: string, taskId?: string): Promise<GitStatusResponse> => {
|
||||
if (isMockMode()) {
|
||||
return { ...mockGitStatus, project_slug: projectSlug };
|
||||
}
|
||||
const { data } = await api.get<GitStatusResponse>("/git/status", {
|
||||
params: { project_slug: projectSlug, _task_id: taskId },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get git log for a project
|
||||
*/
|
||||
getLog: async (
|
||||
projectSlug: string,
|
||||
limit: number = 10,
|
||||
branch?: string
|
||||
): Promise<GitLogResponse> => {
|
||||
if (isMockMode()) {
|
||||
return { ...mockGitLog, project_slug: projectSlug };
|
||||
}
|
||||
const { data } = await api.get<GitLogResponse>("/git/log", {
|
||||
params: { project_slug: projectSlug, limit, branch },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get git branches for a project
|
||||
*/
|
||||
getBranches: async (
|
||||
projectSlug: string,
|
||||
includeRemote: boolean = false
|
||||
): Promise<GitBranchListResponse> => {
|
||||
if (isMockMode()) {
|
||||
return { ...mockBranches, project_slug: projectSlug };
|
||||
}
|
||||
const { data } = await api.get<GitBranchListResponse>("/git/branches", {
|
||||
params: { project_slug: projectSlug, include_remote: includeRemote },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get git diff for a project
|
||||
*/
|
||||
getDiff: async (
|
||||
projectSlug: string,
|
||||
staged: boolean = false,
|
||||
filePath?: string
|
||||
): Promise<GitDiffResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
project_slug: projectSlug,
|
||||
staged,
|
||||
file_path: filePath || null,
|
||||
diff: "- old line\n+ new line",
|
||||
files_changed: 1,
|
||||
};
|
||||
}
|
||||
const { data } = await api.get<GitDiffResponse>("/git/diff", {
|
||||
params: { project_slug: projectSlug, staged, file_path: filePath },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// WRITE OPERATIONS
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Create a git commit
|
||||
*/
|
||||
commit: async (request: GitCommitRequest): Promise<GitCommitResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
commit_hash: "abc123456789abcdef",
|
||||
message: `[${request.task_id.slice(0, 8)}] ${request.message}`,
|
||||
files_changed: request.files?.length || 1,
|
||||
insertions: 10,
|
||||
deletions: 5,
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<GitCommitResponse>("/git/commit", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Push commits to remote
|
||||
*/
|
||||
push: async (request: GitPushRequest): Promise<GitPushResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
branch: "feature/backend/abc12345",
|
||||
commits_pushed: 2,
|
||||
remote: "origin",
|
||||
ready_for_pr: true,
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<GitPushResponse>("/git/push", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a task branch (PM only)
|
||||
*/
|
||||
createBranch: async (request: GitCreateBranchRequest): Promise<GitCreateBranchResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
branch_name: `${request.branch_type}/backend/${request.task_id.slice(0, 8)}`,
|
||||
created_from: request.parent_branch || "main",
|
||||
project_slug: request.project_slug,
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<GitCreateBranchResponse>("/git/branch/create", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checkout a branch
|
||||
*/
|
||||
checkout: async (request: GitCheckoutRequest): Promise<GitCheckoutResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
branch: request.branch,
|
||||
project_slug: request.project_slug,
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<GitCheckoutResponse>("/git/checkout", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a pull request
|
||||
*/
|
||||
createPR: async (request: GitCreatePRRequest): Promise<GitCreatePRResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
pr_number: 42,
|
||||
pr_url: "https://github.com/org/repo/pull/42",
|
||||
title: request.title,
|
||||
source_branch: "feature/backend/abc12345",
|
||||
target_branch: "main",
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<GitCreatePRResponse>("/git/pr/create", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Merge a pull request (PM only)
|
||||
*/
|
||||
mergePR: async (request: GitMergePRRequest): Promise<GitMergePRResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
pr_number: request.pr_number,
|
||||
merged: true,
|
||||
merge_commit: "def456789abcdef",
|
||||
target_branch: "main",
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<GitMergePRResponse>("/git/pr/merge", request);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Groups API Client
|
||||
*
|
||||
* API functions for group management within channels.
|
||||
*/
|
||||
|
||||
import api from "./client";
|
||||
import { isMockMode } from "@/lib/mock-data";
|
||||
import type { Group } from "@/types";
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
export interface GroupCreate {
|
||||
channel_id: string;
|
||||
name: string;
|
||||
hierarchy_level?: number;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API Client
|
||||
// =============================================================================
|
||||
|
||||
export const groupsApi = {
|
||||
/**
|
||||
* Create a new group within a channel
|
||||
*/
|
||||
create: async (group: GroupCreate): Promise<Group> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
id: `group-${Date.now()}`,
|
||||
name: group.name,
|
||||
hierarchy_level: group.hierarchy_level ?? 0,
|
||||
is_active: true,
|
||||
total_messages: 0,
|
||||
active_session_id: null,
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<Group>("/groups", group);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a group by ID
|
||||
*/
|
||||
get: async (groupId: string): Promise<Group> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
id: groupId,
|
||||
name: "Mock Group",
|
||||
hierarchy_level: 0,
|
||||
is_active: true,
|
||||
total_messages: 0,
|
||||
active_session_id: null,
|
||||
};
|
||||
}
|
||||
const { data } = await api.get<Group>(`/groups/${groupId}`);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export { api, API_URL } from "./client";
|
||||
export { tasksApi } from "./tasks";
|
||||
export { orchestratorApi } from "./orchestrator";
|
||||
export { channelsApi } from "./channels";
|
||||
export { notificationsApi } from "./notifications";
|
||||
export { dashboardApi } from "./dashboard";
|
||||
export { knowledgeBaseApi } from "./knowledge-base";
|
||||
export { projectsApi } from "./projects";
|
||||
export { workSessionsApi } from "./work-sessions";
|
||||
export { gitApi } from "./git";
|
||||
export { a2aApi } from "./a2a";
|
||||
export { streamApi } from "./stream";
|
||||
export { groupsApi } from "./groups";
|
||||
@@ -0,0 +1,426 @@
|
||||
/**
|
||||
* Journals API Client
|
||||
*
|
||||
* Agent personal journals for reflection, growth tracking, and debugging.
|
||||
* Matches backend: roboco/api/routes/journals.py
|
||||
*/
|
||||
|
||||
import api from "./client";
|
||||
import {
|
||||
Journal,
|
||||
JournalEntry,
|
||||
JournalEntryCreate,
|
||||
JournalEntryType,
|
||||
JournalStats,
|
||||
GrowthMetrics,
|
||||
} from "@/types";
|
||||
import {
|
||||
isMockMode,
|
||||
mockJournals,
|
||||
mockJournalEntries,
|
||||
} from "@/lib/mock-data";
|
||||
|
||||
// =============================================================================
|
||||
// JOURNAL ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get the current agent's journal (CEO's journal in this case)
|
||||
*/
|
||||
async function getMyJournal(): Promise<Journal> {
|
||||
if (isMockMode()) {
|
||||
return mockJournals[0] as Journal;
|
||||
}
|
||||
const response = await api.get<Journal>("/journals/me");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a journal by agent ID or slug (e.g., "be-dev-1")
|
||||
*/
|
||||
async function getJournalByAgent(agentIdOrSlug: string): Promise<Journal> {
|
||||
if (isMockMode()) {
|
||||
const journal = mockJournals.find((j) => j.agent_id === agentIdOrSlug);
|
||||
if (journal) return journal as Journal;
|
||||
// Return first journal as fallback
|
||||
return mockJournals[0] as Journal;
|
||||
}
|
||||
const response = await api.get<Journal>(`/journals/${agentIdOrSlug}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* List entries for a specific agent by ID or slug
|
||||
*/
|
||||
async function listAgentEntries(
|
||||
agentIdOrSlug: string,
|
||||
params?: {
|
||||
entry_type?: JournalEntryType;
|
||||
task_id?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
): Promise<JournalEntry[]> {
|
||||
if (isMockMode()) {
|
||||
let entries = [...mockJournalEntries] as JournalEntry[];
|
||||
if (params?.entry_type) {
|
||||
entries = entries.filter((e) => e.type === params.entry_type);
|
||||
}
|
||||
if (params?.task_id) {
|
||||
entries = entries.filter((e) => e.task_id === params.task_id);
|
||||
}
|
||||
const offset = params?.offset ?? 0;
|
||||
const limit = params?.limit ?? 50;
|
||||
return entries.slice(offset, offset + limit);
|
||||
}
|
||||
const response = await api.get<JournalEntry[]>(
|
||||
`/journals/${agentIdOrSlug}/entries`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ENTRY CRUD ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Create a new journal entry
|
||||
*/
|
||||
async function createEntry(data: JournalEntryCreate): Promise<JournalEntry> {
|
||||
if (isMockMode()) {
|
||||
const newEntry: JournalEntry = {
|
||||
id: `entry-${Date.now()}`,
|
||||
journal_id: mockJournals[0].id,
|
||||
type: data.type,
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
task_id: data.task_id ?? null,
|
||||
session_id: data.session_id ?? null,
|
||||
timestamp: new Date().toISOString(),
|
||||
tags: data.tags ?? [],
|
||||
sentiment: data.sentiment ?? null,
|
||||
is_private: data.is_private ?? false,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: null,
|
||||
};
|
||||
(mockJournalEntries as JournalEntry[]).push(newEntry);
|
||||
return newEntry;
|
||||
}
|
||||
const response = await api.post<JournalEntry>("/journals/me/entries", data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* List current agent's journal entries
|
||||
*/
|
||||
async function listMyEntries(params?: {
|
||||
entry_type?: JournalEntryType;
|
||||
task_id?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<JournalEntry[]> {
|
||||
if (isMockMode()) {
|
||||
let entries = [...mockJournalEntries] as JournalEntry[];
|
||||
if (params?.entry_type) {
|
||||
entries = entries.filter((e) => e.type === params.entry_type);
|
||||
}
|
||||
if (params?.task_id) {
|
||||
entries = entries.filter((e) => e.task_id === params.task_id);
|
||||
}
|
||||
const offset = params?.offset ?? 0;
|
||||
const limit = params?.limit ?? 50;
|
||||
return entries.slice(offset, offset + limit);
|
||||
}
|
||||
const response = await api.get<JournalEntry[]>("/journals/me/entries", {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific journal entry by ID
|
||||
*/
|
||||
async function getEntry(entryId: string): Promise<JournalEntry> {
|
||||
if (isMockMode()) {
|
||||
const entry = mockJournalEntries.find((e) => e.id === entryId);
|
||||
if (entry) return entry as JournalEntry;
|
||||
throw new Error("Entry not found");
|
||||
}
|
||||
const response = await api.get<JournalEntry>(`/journals/entries/${entryId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a journal entry
|
||||
*/
|
||||
async function deleteEntry(entryId: string): Promise<void> {
|
||||
if (isMockMode()) {
|
||||
const idx = mockJournalEntries.findIndex((e) => e.id === entryId);
|
||||
if (idx !== -1) mockJournalEntries.splice(idx, 1);
|
||||
return;
|
||||
}
|
||||
await api.delete(`/journals/entries/${entryId}`);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONVENIENCE ENTRY ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Add a task reflection entry
|
||||
*/
|
||||
async function addTaskReflection(data: {
|
||||
task_id: string;
|
||||
title: string;
|
||||
what_done: string;
|
||||
what_learned: string;
|
||||
what_struggled?: string;
|
||||
next_steps?: string[];
|
||||
tags?: string[];
|
||||
}): Promise<JournalEntry> {
|
||||
if (isMockMode()) {
|
||||
return createEntry({
|
||||
type: JournalEntryType.TASK_REFLECTION,
|
||||
title: data.title,
|
||||
content: JSON.stringify({
|
||||
what_done: data.what_done,
|
||||
what_learned: data.what_learned,
|
||||
what_struggled: data.what_struggled,
|
||||
next_steps: data.next_steps,
|
||||
}),
|
||||
task_id: data.task_id,
|
||||
tags: data.tags,
|
||||
});
|
||||
}
|
||||
const response = await api.post<JournalEntry>(
|
||||
"/journals/me/reflections",
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a decision log entry
|
||||
*/
|
||||
async function addDecisionLog(data: {
|
||||
title: string;
|
||||
context: string;
|
||||
options: string[];
|
||||
chosen: string;
|
||||
rationale: string;
|
||||
consequences?: string;
|
||||
task_id?: string;
|
||||
tags?: string[];
|
||||
}): Promise<JournalEntry> {
|
||||
if (isMockMode()) {
|
||||
return createEntry({
|
||||
type: JournalEntryType.DECISION_LOG,
|
||||
title: data.title,
|
||||
content: JSON.stringify({
|
||||
context: data.context,
|
||||
options: data.options,
|
||||
chosen: data.chosen,
|
||||
rationale: data.rationale,
|
||||
consequences: data.consequences,
|
||||
}),
|
||||
task_id: data.task_id,
|
||||
tags: data.tags,
|
||||
});
|
||||
}
|
||||
const response = await api.post<JournalEntry>("/journals/me/decisions", data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a learning entry
|
||||
*/
|
||||
async function addLearning(data: {
|
||||
title: string;
|
||||
what_learned: string;
|
||||
how_applied?: string;
|
||||
source?: string;
|
||||
task_id?: string;
|
||||
tags?: string[];
|
||||
}): Promise<JournalEntry> {
|
||||
if (isMockMode()) {
|
||||
return createEntry({
|
||||
type: JournalEntryType.LEARNING,
|
||||
title: data.title,
|
||||
content: JSON.stringify({
|
||||
what_learned: data.what_learned,
|
||||
how_applied: data.how_applied,
|
||||
source: data.source,
|
||||
}),
|
||||
task_id: data.task_id,
|
||||
tags: data.tags,
|
||||
});
|
||||
}
|
||||
const response = await api.post<JournalEntry>("/journals/me/learnings", data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a struggle entry
|
||||
*/
|
||||
async function addStruggle(data: {
|
||||
title: string;
|
||||
what_struggled: string;
|
||||
attempted_solutions: string[];
|
||||
resolution?: string;
|
||||
help_needed?: string;
|
||||
task_id?: string;
|
||||
tags?: string[];
|
||||
}): Promise<JournalEntry> {
|
||||
if (isMockMode()) {
|
||||
return createEntry({
|
||||
type: JournalEntryType.STRUGGLE,
|
||||
title: data.title,
|
||||
content: JSON.stringify({
|
||||
what_struggled: data.what_struggled,
|
||||
attempted_solutions: data.attempted_solutions,
|
||||
resolution: data.resolution,
|
||||
help_needed: data.help_needed,
|
||||
}),
|
||||
task_id: data.task_id,
|
||||
tags: data.tags,
|
||||
});
|
||||
}
|
||||
const response = await api.post<JournalEntry>("/journals/me/struggles", data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a general journal entry
|
||||
*/
|
||||
async function addNote(data: {
|
||||
title: string;
|
||||
content: string;
|
||||
task_id?: string;
|
||||
session_id?: string;
|
||||
tags?: string[];
|
||||
is_private?: boolean;
|
||||
}): Promise<JournalEntry> {
|
||||
if (isMockMode()) {
|
||||
return createEntry({
|
||||
type: JournalEntryType.GENERAL,
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
task_id: data.task_id,
|
||||
session_id: data.session_id,
|
||||
tags: data.tags,
|
||||
is_private: data.is_private,
|
||||
});
|
||||
}
|
||||
const response = await api.post<JournalEntry>("/journals/me/notes", data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ANALYTICS ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get statistics for the current agent's journal
|
||||
*/
|
||||
async function getMyStats(): Promise<JournalStats> {
|
||||
if (isMockMode()) {
|
||||
const entries = mockJournalEntries as JournalEntry[];
|
||||
return {
|
||||
total_entries: entries.length,
|
||||
entries_by_type: {
|
||||
task_reflection: entries.filter((e) => e.type === JournalEntryType.TASK_REFLECTION).length,
|
||||
decision_log: entries.filter((e) => e.type === JournalEntryType.DECISION_LOG).length,
|
||||
learning: entries.filter((e) => e.type === JournalEntryType.LEARNING).length,
|
||||
struggle: entries.filter((e) => e.type === JournalEntryType.STRUGGLE).length,
|
||||
general: entries.filter((e) => e.type === JournalEntryType.GENERAL).length,
|
||||
},
|
||||
last_entry_at: entries[0]?.created_at ?? null,
|
||||
has_summary: false,
|
||||
};
|
||||
}
|
||||
const response = await api.get<JournalStats>("/journals/me/stats");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get growth metrics for the current agent
|
||||
*/
|
||||
async function getMyGrowthMetrics(): Promise<GrowthMetrics> {
|
||||
if (isMockMode()) {
|
||||
const entries = mockJournalEntries as JournalEntry[];
|
||||
return {
|
||||
total_reflections: entries.filter((e) => e.type === JournalEntryType.TASK_REFLECTION).length,
|
||||
total_learnings: entries.filter((e) => e.type === JournalEntryType.LEARNING).length,
|
||||
total_struggles: entries.filter((e) => e.type === JournalEntryType.STRUGGLE).length,
|
||||
total_decisions: entries.filter((e) => e.type === JournalEntryType.DECISION_LOG).length,
|
||||
struggle_resolution_rate: 0.7,
|
||||
learning_frequency: 3.5,
|
||||
sentiment_trend: "improving",
|
||||
};
|
||||
}
|
||||
const response = await api.get<GrowthMetrics>("/journals/me/growth");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SEARCH ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Semantic search through journal entries
|
||||
*/
|
||||
async function searchEntries(
|
||||
query: string,
|
||||
topK: number = 10
|
||||
): Promise<JournalEntry[]> {
|
||||
if (isMockMode()) {
|
||||
// Simple text search for mock mode
|
||||
const queryLower = query.toLowerCase();
|
||||
return (mockJournalEntries as JournalEntry[])
|
||||
.filter(
|
||||
(e) =>
|
||||
e.title.toLowerCase().includes(queryLower) ||
|
||||
e.content.toLowerCase().includes(queryLower)
|
||||
)
|
||||
.slice(0, topK);
|
||||
}
|
||||
const response = await api.post<JournalEntry[]>("/journals/me/search", {
|
||||
query,
|
||||
top_k: topK,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT API
|
||||
// =============================================================================
|
||||
|
||||
export const journalsApi = {
|
||||
// Journal
|
||||
getMyJournal,
|
||||
getJournalByAgent,
|
||||
|
||||
// Entry CRUD
|
||||
createEntry,
|
||||
listMyEntries,
|
||||
listAgentEntries,
|
||||
getEntry,
|
||||
deleteEntry,
|
||||
|
||||
// Convenience entry creators
|
||||
addTaskReflection,
|
||||
addDecisionLog,
|
||||
addLearning,
|
||||
addStruggle,
|
||||
addNote,
|
||||
|
||||
// Analytics
|
||||
getMyStats,
|
||||
getMyGrowthMetrics,
|
||||
|
||||
// Search
|
||||
searchEntries,
|
||||
};
|
||||
|
||||
export default journalsApi;
|
||||
@@ -0,0 +1,653 @@
|
||||
/**
|
||||
* Knowledge Base API Client
|
||||
*
|
||||
* Semantic search and RAG queries against indexed knowledge.
|
||||
* Matches backend: roboco/api/routes/optimal.py
|
||||
*/
|
||||
|
||||
import api from "./client";
|
||||
import {
|
||||
KBIndexType,
|
||||
type KBSearchRequest,
|
||||
type KBSearchResponse,
|
||||
type KBSearchResult,
|
||||
type RAGQueryRequest,
|
||||
type RAGQueryResponse,
|
||||
type KBStats,
|
||||
type KBIndexStats,
|
||||
type RAGHealthResponse,
|
||||
type MentorAskRequest,
|
||||
type MentorAskResponse,
|
||||
type ErrorSearchRequest,
|
||||
type ErrorSearchResponse,
|
||||
type ErrorRecordRequest,
|
||||
type ErrorRecordResponse,
|
||||
type DecisionCheckRequest,
|
||||
type DecisionCheckResponse,
|
||||
type DecisionRecordRequest,
|
||||
type DecisionRecordResponse,
|
||||
type StandardsGetRequest,
|
||||
type StandardsGetResponse,
|
||||
type ValidateActionRequest,
|
||||
type ValidateActionResponse,
|
||||
type CodeReviewRequest,
|
||||
type CodeReviewResponse,
|
||||
type LearningRecordRequest,
|
||||
type LearningRecordResponse,
|
||||
type LearningSearchRequest,
|
||||
type ProactiveContextRequest,
|
||||
type ProactiveContextResponse,
|
||||
type TokenEstimateRequest,
|
||||
type TokenEstimateResponse,
|
||||
type RefreshIndexRequest,
|
||||
type RefreshIndexResponse,
|
||||
type ClearIndexResponse,
|
||||
type ReindexResponse,
|
||||
type ReindexRequest,
|
||||
type IndexStalenessResponse,
|
||||
} from "@/types";
|
||||
import { isMockMode } from "@/lib/mock-data";
|
||||
|
||||
// =============================================================================
|
||||
// MOCK DATA
|
||||
// =============================================================================
|
||||
|
||||
const mockSearchResults: KBSearchResult[] = [
|
||||
{
|
||||
content: "The TaskService handles all task lifecycle operations including creation, assignment, status transitions, and completion tracking...",
|
||||
source: "roboco/services/tasks.py",
|
||||
score: 0.92,
|
||||
index_type: KBIndexType.CODE,
|
||||
metadata: { language: "python", lines: "45-120" },
|
||||
},
|
||||
{
|
||||
content: "## Task Lifecycle\n\nTasks follow a strict state machine from PENDING through IN_PROGRESS to COMPLETED. Each transition requires specific conditions...",
|
||||
source: "docs/architecture/task-lifecycle.md",
|
||||
score: 0.88,
|
||||
index_type: KBIndexType.DOCUMENTATION,
|
||||
metadata: { section: "Architecture" },
|
||||
},
|
||||
{
|
||||
content: "BE-DEV-1: I've completed the task validation logic. The acceptance criteria now require at least one item before a task can be created.",
|
||||
source: "channel:backend/session:abc123",
|
||||
score: 0.75,
|
||||
index_type: KBIndexType.CONVERSATIONS,
|
||||
metadata: { agent: "be-dev-1", timestamp: "2024-01-15T10:30:00Z" },
|
||||
},
|
||||
{
|
||||
content: "Learned that proper error boundaries in the task form prevent cascading failures. Applied this pattern to all form components.",
|
||||
source: "journal:be-dev-1/entry:xyz789",
|
||||
score: 0.71,
|
||||
index_type: KBIndexType.JOURNALS,
|
||||
metadata: { agent: "be-dev-1", type: "learning" },
|
||||
},
|
||||
];
|
||||
|
||||
const mockStats: KBStats = {
|
||||
indexes: [
|
||||
{ index_type: KBIndexType.CODE, document_count: 1250, chunk_count: 8500, last_updated: "2024-01-15T12:00:00Z" },
|
||||
{ index_type: KBIndexType.DOCUMENTATION, document_count: 45, chunk_count: 320, last_updated: "2024-01-15T11:30:00Z" },
|
||||
{ index_type: KBIndexType.CONVERSATIONS, document_count: 890, chunk_count: 4200, last_updated: "2024-01-15T12:15:00Z" },
|
||||
{ index_type: KBIndexType.JOURNALS, document_count: 156, chunk_count: 780, last_updated: "2024-01-15T10:00:00Z" },
|
||||
{ index_type: KBIndexType.ERRORS, document_count: 45, chunk_count: 180, last_updated: "2024-01-15T10:00:00Z" },
|
||||
{ index_type: KBIndexType.STANDARDS, document_count: 12, chunk_count: 60, last_updated: "2024-01-15T10:00:00Z" },
|
||||
{ index_type: KBIndexType.DECISIONS, document_count: 78, chunk_count: 390, last_updated: "2024-01-15T10:00:00Z" },
|
||||
{ index_type: KBIndexType.REVIEWS, document_count: 234, chunk_count: 1170, last_updated: "2024-01-15T10:00:00Z" },
|
||||
{ index_type: KBIndexType.LEARNINGS, document_count: 89, chunk_count: 445, last_updated: "2024-01-15T10:00:00Z" },
|
||||
],
|
||||
total_documents: 2799,
|
||||
total_chunks: 15245,
|
||||
};
|
||||
|
||||
// Backend returns stats as dict, we need to transform to array
|
||||
interface BackendIndexStats {
|
||||
initialized: boolean;
|
||||
indexes: Record<string, { document_count: number; chunk_count: number; last_updated: string | null }>;
|
||||
}
|
||||
|
||||
function transformStatsResponse(backendStats: BackendIndexStats): KBStats {
|
||||
const indexes: KBIndexStats[] = Object.entries(backendStats.indexes || {}).map(([indexType, stats]) => ({
|
||||
index_type: indexType as KBIndexType,
|
||||
document_count: stats.document_count ?? 0,
|
||||
chunk_count: stats.chunk_count ?? 0,
|
||||
last_updated: stats.last_updated ?? null,
|
||||
}));
|
||||
|
||||
const total_documents = indexes.reduce((sum, idx) => sum + idx.document_count, 0);
|
||||
const total_chunks = indexes.reduce((sum, idx) => sum + idx.chunk_count, 0);
|
||||
|
||||
return { indexes, total_documents, total_chunks };
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SEARCH ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Semantic search across indexed content
|
||||
*/
|
||||
async function search(params: KBSearchRequest): Promise<KBSearchResponse> {
|
||||
if (isMockMode()) {
|
||||
// Filter by index types if specified
|
||||
let results = [...mockSearchResults];
|
||||
if (params.index_types && params.index_types.length > 0) {
|
||||
results = results.filter((r) => params.index_types!.includes(r.index_type));
|
||||
}
|
||||
// Filter by min score
|
||||
if (params.min_score) {
|
||||
results = results.filter((r) => r.score >= params.min_score!);
|
||||
}
|
||||
// Limit results
|
||||
const topK = params.top_k ?? 10;
|
||||
results = results.slice(0, topK);
|
||||
|
||||
return {
|
||||
results,
|
||||
total: results.length,
|
||||
query: params.query,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await api.post<KBSearchResponse>("/optimal/kb/search", params);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RAG ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* RAG query - ask a question, get AI answer with citations
|
||||
*/
|
||||
async function ragQuery(params: RAGQueryRequest): Promise<RAGQueryResponse> {
|
||||
if (isMockMode()) {
|
||||
// Simulate RAG response with mock data
|
||||
return {
|
||||
answer: `Based on the indexed knowledge, here's what I found about "${params.question}":\n\nThe system uses a structured approach where tasks follow a defined lifecycle. Each task starts in PENDING state and can transition through various states like IN_PROGRESS, BLOCKED, and eventually COMPLETED or CANCELLED.\n\nKey points:\n- Tasks require acceptance criteria before creation\n- State transitions are validated by the TaskService\n- Agents can claim and work on tasks based on their role and team`,
|
||||
citations: mockSearchResults.slice(0, 3).map((r) => ({
|
||||
content: r.content,
|
||||
source: r.source,
|
||||
score: r.score,
|
||||
index_type: r.index_type,
|
||||
metadata: r.metadata,
|
||||
})),
|
||||
query: params.question,
|
||||
context_used: 3,
|
||||
};
|
||||
}
|
||||
|
||||
// Transform frontend params to backend format
|
||||
const backendParams = {
|
||||
query: params.question,
|
||||
index_types: params.index_types,
|
||||
top_k: params.max_context_chunks ?? 5,
|
||||
};
|
||||
|
||||
const response = await api.post<RAGQueryResponse>("/optimal/rag/query", backendParams);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context for a query without generating an answer
|
||||
*/
|
||||
async function getContext(params: RAGQueryRequest): Promise<KBSearchResult[]> {
|
||||
if (isMockMode()) {
|
||||
return mockSearchResults.slice(0, params.max_context_chunks ?? 5);
|
||||
}
|
||||
|
||||
// Transform frontend params to backend format
|
||||
const backendParams = {
|
||||
query: params.question,
|
||||
index_types: params.index_types,
|
||||
top_k: params.max_context_chunks ?? 5,
|
||||
};
|
||||
|
||||
const response = await api.post<{ context: KBSearchResult[] }>("/optimal/rag/context", backendParams);
|
||||
return response.data.context;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATS ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get index statistics
|
||||
*/
|
||||
async function getStats(): Promise<KBStats> {
|
||||
if (isMockMode()) {
|
||||
return mockStats;
|
||||
}
|
||||
|
||||
const response = await api.get<BackendIndexStats>("/optimal/stats");
|
||||
return transformStatsResponse(response.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stats for a specific index type
|
||||
*/
|
||||
async function getIndexStats(indexType: KBIndexType): Promise<KBIndexStats> {
|
||||
if (isMockMode()) {
|
||||
const stats = mockStats.indexes.find((i) => i.index_type === indexType);
|
||||
if (stats) return stats;
|
||||
return {
|
||||
index_type: indexType,
|
||||
document_count: 0,
|
||||
chunk_count: 0,
|
||||
last_updated: null,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await api.get<KBIndexStats>(`/optimal/stats/${indexType}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BROWSE ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* List documents in a specific index (for browsing)
|
||||
*/
|
||||
async function listDocuments(
|
||||
indexType: KBIndexType,
|
||||
params?: { limit?: number; offset?: number }
|
||||
): Promise<{ documents: Array<{ id: string; source: string; indexed_at: string; metadata?: Record<string, unknown> }>; total: number }> {
|
||||
if (isMockMode()) {
|
||||
// Generate mock document list
|
||||
const docs = mockSearchResults
|
||||
.filter((r) => r.index_type === indexType)
|
||||
.map((r, idx) => ({
|
||||
id: `doc-${indexType}-${idx}`,
|
||||
source: r.source,
|
||||
indexed_at: "2024-01-15T12:00:00Z",
|
||||
}));
|
||||
return { documents: docs, total: docs.length };
|
||||
}
|
||||
|
||||
const response = await api.get<{ documents: Array<{ id: string; source: string; indexed_at: string; metadata?: Record<string, unknown> }>; total: number; index_type: string }>(
|
||||
`/optimal/kb/${indexType}/documents`,
|
||||
{ params }
|
||||
);
|
||||
return { documents: response.data.documents, total: response.data.total };
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HEALTH ENDPOINT
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get RAG system health status
|
||||
*/
|
||||
async function getHealth(): Promise<RAGHealthResponse> {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
healthy: true,
|
||||
embedding_status: "healthy",
|
||||
llm_status: "healthy",
|
||||
vector_store_status: "healthy",
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
const response = await api.get<RAGHealthResponse>("/optimal/health");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INDEX MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Delete/clear an index
|
||||
*/
|
||||
async function deleteIndex(indexType: KBIndexType): Promise<ClearIndexResponse> {
|
||||
if (isMockMode()) {
|
||||
return { status: "cleared", index_type: indexType };
|
||||
}
|
||||
const response = await api.delete<ClearIndexResponse>(`/optimal/kb/${indexType}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh an index with updated sources
|
||||
*/
|
||||
async function refreshIndex(request: RefreshIndexRequest): Promise<RefreshIndexResponse> {
|
||||
if (isMockMode()) {
|
||||
return { status: "refreshed", index_type: request.index_type, sources: request.sources };
|
||||
}
|
||||
const response = await api.post<RefreshIndexResponse>("/optimal/kb/refresh", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger full reindex with detailed reporting
|
||||
*
|
||||
* @param request - Optional parameters for reindexing
|
||||
* @param request.force - Force reindex even if indexes have content
|
||||
* @param request.timeout_seconds - Max time to wait (default: 300s)
|
||||
* @returns Detailed report of what was indexed, failed, and skipped
|
||||
*/
|
||||
async function reindexAll(request?: ReindexRequest): Promise<ReindexResponse> {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
status: "reindexed",
|
||||
code: {
|
||||
index_type: "code",
|
||||
total_attempted: 100,
|
||||
successful: 98,
|
||||
failed: 2,
|
||||
skipped: 0,
|
||||
success_rate: 98.0,
|
||||
has_failures: true,
|
||||
failed_sources: [
|
||||
["/path/to/file1.py", "UTF-8 decode error"],
|
||||
["/path/to/file2.py", "File too large"],
|
||||
],
|
||||
duration_seconds: 45.2,
|
||||
},
|
||||
documentation: {
|
||||
index_type: "documentation",
|
||||
total_attempted: 50,
|
||||
successful: 50,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
success_rate: 100.0,
|
||||
has_failures: false,
|
||||
failed_sources: [],
|
||||
duration_seconds: 12.5,
|
||||
},
|
||||
overall_success: true,
|
||||
warnings: ["Code indexing had 2 failures"],
|
||||
// Legacy fields
|
||||
code_count: 98,
|
||||
docs_count: 50,
|
||||
};
|
||||
}
|
||||
const response = await api.post<ReindexResponse>("/optimal/kb/reindex", null, {
|
||||
params: {
|
||||
force: request?.force ?? false,
|
||||
timeout_seconds: request?.timeout_seconds ?? 300,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if indexes are stale (source files modified after last indexing)
|
||||
*
|
||||
* @returns Staleness info for each index type
|
||||
*/
|
||||
async function checkStaleness(): Promise<IndexStalenessResponse> {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
needs_reindex: true,
|
||||
stale_indexes: ["code"],
|
||||
details: {
|
||||
code: {
|
||||
status: "stale",
|
||||
last_indexed: new Date(Date.now() - 86400000).toISOString(),
|
||||
stale_file_count: 5,
|
||||
stale_files_sample: [
|
||||
"/roboco/services/task.py",
|
||||
"/roboco/services/optimal.py",
|
||||
],
|
||||
recommendation: "Run /kb/reindex?force=true to update",
|
||||
},
|
||||
documentation: {
|
||||
status: "current",
|
||||
last_indexed: new Date().toISOString(),
|
||||
indexed_sources_count: 50,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const response = await api.get<IndexStalenessResponse>(
|
||||
"/optimal/stats/staleness"
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MENTOR ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Ask the mentor for help
|
||||
*/
|
||||
async function askMentor(request: MentorAskRequest): Promise<MentorAskResponse> {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
answer: `Here's what I found about "${request.question}":\n\nBased on our organizational knowledge, I recommend following the established patterns and consulting the relevant documentation.`,
|
||||
sources: mockSearchResults.slice(0, 2),
|
||||
conversation_id: "conv-" + Date.now(),
|
||||
suggested_followups: ["Can you elaborate?", "What are the alternatives?"],
|
||||
};
|
||||
}
|
||||
const response = await api.post<MentorAskResponse>("/optimal/mentor/ask", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ERROR ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Search for known error solutions
|
||||
*/
|
||||
async function searchErrors(request: ErrorSearchRequest): Promise<ErrorSearchResponse> {
|
||||
if (isMockMode()) {
|
||||
return { results: [], total: 0 };
|
||||
}
|
||||
const response = await api.post<ErrorSearchResponse>("/optimal/errors/search", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an error solution
|
||||
*/
|
||||
async function recordError(request: ErrorRecordRequest): Promise<ErrorRecordResponse> {
|
||||
if (isMockMode()) {
|
||||
return { error_id: "err-" + Date.now(), status: "recorded" };
|
||||
}
|
||||
const response = await api.post<ErrorRecordResponse>("/optimal/errors/record", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DECISION ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Check if a similar decision was made before
|
||||
*/
|
||||
async function checkDecision(request: DecisionCheckRequest): Promise<DecisionCheckResponse> {
|
||||
if (isMockMode()) {
|
||||
return { has_precedent: false, decisions: [], recommendation: "No similar decisions found" };
|
||||
}
|
||||
const response = await api.post<DecisionCheckResponse>("/optimal/decisions/check", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a decision for future reference
|
||||
*/
|
||||
async function recordDecision(request: DecisionRecordRequest): Promise<DecisionRecordResponse> {
|
||||
if (isMockMode()) {
|
||||
return { decision_id: "dec-" + Date.now(), status: "recorded" };
|
||||
}
|
||||
const response = await api.post<DecisionRecordResponse>("/optimal/decisions/record", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STANDARDS ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get standards for a domain
|
||||
*/
|
||||
async function getStandards(request: StandardsGetRequest): Promise<StandardsGetResponse> {
|
||||
if (isMockMode()) {
|
||||
return { standards: [], total: 0 };
|
||||
}
|
||||
const response = await api.post<StandardsGetResponse>("/optimal/standards/get", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an action against standards
|
||||
*/
|
||||
async function validateAction(request: ValidateActionRequest): Promise<ValidateActionResponse> {
|
||||
if (isMockMode()) {
|
||||
return { allowed: true, violations: [], warnings: [], relevant_standards: [] };
|
||||
}
|
||||
const response = await api.post<ValidateActionResponse>("/optimal/standards/validate", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CODE REVIEW ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Review code against standards
|
||||
*/
|
||||
async function reviewCode(request: CodeReviewRequest): Promise<CodeReviewResponse> {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
file_path: request.file_path,
|
||||
approved: true,
|
||||
score: 85,
|
||||
comments: [],
|
||||
standards_checked: ["coding-standards"],
|
||||
similar_reviews: [],
|
||||
};
|
||||
}
|
||||
const response = await api.post<CodeReviewResponse>("/optimal/review/code", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LEARNING ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Record a learning for knowledge sharing
|
||||
*/
|
||||
async function recordLearning(request: LearningRecordRequest): Promise<LearningRecordResponse> {
|
||||
if (isMockMode()) {
|
||||
return { learning_id: "learn-" + Date.now(), status: "recorded" };
|
||||
}
|
||||
const response = await api.post<LearningRecordResponse>("/optimal/learnings/record", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for relevant learnings
|
||||
*/
|
||||
async function searchLearnings(request: LearningSearchRequest): Promise<KBSearchResponse> {
|
||||
if (isMockMode()) {
|
||||
return { results: [], total: 0, query: request.query };
|
||||
}
|
||||
const response = await api.post<KBSearchResponse>("/optimal/learnings/search", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROACTIVE CONTEXT ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get proactive context for a task
|
||||
*/
|
||||
async function getProactiveContext(request: ProactiveContextRequest): Promise<ProactiveContextResponse> {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
task_id: request.task_id,
|
||||
similar_tasks: [],
|
||||
relevant_learnings: [],
|
||||
code_patterns: [],
|
||||
applicable_standards: [],
|
||||
recent_decisions: [],
|
||||
known_issues: [],
|
||||
summary: "No relevant context found for this task.",
|
||||
};
|
||||
}
|
||||
const response = await api.post<ProactiveContextResponse>("/optimal/context/proactive", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TOKEN ESTIMATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Estimate token count for content
|
||||
*/
|
||||
async function estimateTokens(request: TokenEstimateRequest): Promise<TokenEstimateResponse> {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
token_count: Math.ceil(request.content.length / 4),
|
||||
model: request.model || "claude-sonnet-4-20250514",
|
||||
content_length: request.content.length,
|
||||
};
|
||||
}
|
||||
const response = await api.post<TokenEstimateResponse>("/optimal/tokens/estimate", request);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT API
|
||||
// =============================================================================
|
||||
|
||||
export const knowledgeBaseApi = {
|
||||
// Search
|
||||
search,
|
||||
|
||||
// RAG
|
||||
ragQuery,
|
||||
getContext,
|
||||
|
||||
// Stats & Health
|
||||
getStats,
|
||||
getIndexStats,
|
||||
getHealth,
|
||||
|
||||
// Browse
|
||||
listDocuments,
|
||||
|
||||
// Index Management
|
||||
deleteIndex,
|
||||
refreshIndex,
|
||||
reindexAll,
|
||||
checkStaleness,
|
||||
|
||||
// Mentor
|
||||
askMentor,
|
||||
|
||||
// Errors
|
||||
searchErrors,
|
||||
recordError,
|
||||
|
||||
// Decisions
|
||||
checkDecision,
|
||||
recordDecision,
|
||||
|
||||
// Standards
|
||||
getStandards,
|
||||
validateAction,
|
||||
|
||||
// Code Review
|
||||
reviewCode,
|
||||
|
||||
// Learnings
|
||||
recordLearning,
|
||||
searchLearnings,
|
||||
|
||||
// Proactive Context
|
||||
getProactiveContext,
|
||||
|
||||
// Token Estimation
|
||||
estimateTokens,
|
||||
};
|
||||
|
||||
export default knowledgeBaseApi;
|
||||
@@ -0,0 +1,123 @@
|
||||
import api from "./client";
|
||||
import type { Message, MessageType } from "@/types";
|
||||
import { isMockMode, getMockMessages, AGENT_IDS, CHANNEL_IDS } from "@/lib/mock-data";
|
||||
|
||||
// Store for mock messages (persists during session)
|
||||
let mockMessagesStore: Message[] | null = null;
|
||||
const getMessages = (): Message[] => {
|
||||
if (!mockMessagesStore) {
|
||||
mockMessagesStore = getMockMessages() as Message[];
|
||||
}
|
||||
return mockMessagesStore;
|
||||
};
|
||||
|
||||
export const messagesApi = {
|
||||
// List messages for a session
|
||||
listBySession: async (
|
||||
sessionId: string,
|
||||
limit: number = 50,
|
||||
before?: string,
|
||||
after?: string
|
||||
): Promise<{ items: Message[]; has_more: boolean }> => {
|
||||
if (isMockMode()) {
|
||||
let messages = getMessages().filter(
|
||||
(m) => m.session_id === sessionId
|
||||
);
|
||||
if (before) {
|
||||
messages = messages.filter(
|
||||
(m) => new Date(m.timestamp) < new Date(before)
|
||||
);
|
||||
}
|
||||
if (after) {
|
||||
messages = messages.filter(
|
||||
(m) => new Date(m.timestamp) > new Date(after)
|
||||
);
|
||||
}
|
||||
return {
|
||||
items: messages.slice(0, limit),
|
||||
has_more: messages.length > limit,
|
||||
};
|
||||
}
|
||||
const { data } = await api.get<{ items: Message[]; has_more: boolean }>("/messages", {
|
||||
params: { session_id: sessionId, limit, before, after },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get message by ID
|
||||
get: async (messageId: string): Promise<Message> => {
|
||||
if (isMockMode()) {
|
||||
const message = getMessages().find((m) => m.id === messageId);
|
||||
if (message) return message;
|
||||
throw new Error("Message not found");
|
||||
}
|
||||
const { data } = await api.get<Message>("/messages/" + messageId);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Send a message
|
||||
send: async (sessionId: string, content: string, type: string = "dialogue"): Promise<Message> => {
|
||||
if (isMockMode()) {
|
||||
const newMessage: Message = {
|
||||
id: `msg-${Date.now()}`,
|
||||
agent_id: AGENT_IDS.ceo,
|
||||
channel_id: CHANNEL_IDS.backendCell,
|
||||
group_id: `msg-${Date.now()}`,
|
||||
session_id: sessionId,
|
||||
type: type as MessageType,
|
||||
content,
|
||||
content_length: content.length,
|
||||
is_reply: false,
|
||||
reply_to: null,
|
||||
mentions: [],
|
||||
task_id: null,
|
||||
commit_ref: null,
|
||||
timestamp: new Date().toISOString(),
|
||||
edited_at: null,
|
||||
was_edited: false,
|
||||
};
|
||||
getMessages().push(newMessage);
|
||||
return newMessage;
|
||||
}
|
||||
const { data } = await api.post<Message>("/messages", {
|
||||
session_id: sessionId,
|
||||
content,
|
||||
type,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// Edit a message
|
||||
edit: async (messageId: string, content: string): Promise<Message> => {
|
||||
if (isMockMode()) {
|
||||
const messages = getMessages();
|
||||
const idx = messages.findIndex((m) => m.id === messageId);
|
||||
if (idx !== -1) {
|
||||
const message = messages[idx];
|
||||
const editedMessage: Message = {
|
||||
...message,
|
||||
content,
|
||||
content_length: content.length,
|
||||
edited_at: new Date().toISOString(),
|
||||
was_edited: true,
|
||||
};
|
||||
messages[idx] = editedMessage;
|
||||
return editedMessage;
|
||||
}
|
||||
throw new Error("Message not found");
|
||||
}
|
||||
const { data } = await api.patch<Message>("/messages/" + messageId, { content });
|
||||
return data;
|
||||
},
|
||||
|
||||
// Delete a message
|
||||
delete: async (messageId: string): Promise<void> => {
|
||||
if (isMockMode()) {
|
||||
const messages = getMessages();
|
||||
const idx = messages.findIndex((m) => m.id === messageId);
|
||||
if (idx !== -1) messages.splice(idx, 1);
|
||||
return;
|
||||
}
|
||||
await api.delete("/messages/" + messageId);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import api from "./client";
|
||||
import type { Notification, NotificationListResponse, NotificationType, NotificationPriority } from "@/types";
|
||||
import { isMockMode, mockNotifications } from "@/lib/mock-data";
|
||||
|
||||
export interface NotificationFilters {
|
||||
type?: NotificationType;
|
||||
priority?: NotificationPriority;
|
||||
unread_only?: boolean;
|
||||
pending_ack_only?: boolean;
|
||||
}
|
||||
|
||||
export const notificationsApi = {
|
||||
// List notifications for CEO
|
||||
list: async (filters?: NotificationFilters): Promise<NotificationListResponse> => {
|
||||
if (isMockMode()) {
|
||||
let notifications = [...mockNotifications] as Notification[];
|
||||
if (filters?.type) {
|
||||
notifications = notifications.filter((n) => n.type === filters.type);
|
||||
}
|
||||
if (filters?.priority) {
|
||||
notifications = notifications.filter((n) => n.priority === filters.priority);
|
||||
}
|
||||
if (filters?.unread_only) {
|
||||
notifications = notifications.filter((n) => !n.is_read);
|
||||
}
|
||||
if (filters?.pending_ack_only) {
|
||||
notifications = notifications.filter((n) => n.requires_ack && !n.is_acknowledged);
|
||||
}
|
||||
return {
|
||||
items: notifications,
|
||||
total: notifications.length,
|
||||
unread_count: notifications.filter((n) => !n.is_read).length,
|
||||
pending_ack_count: notifications.filter((n) => n.requires_ack && !n.is_acknowledged).length,
|
||||
};
|
||||
}
|
||||
// Backend uses type_filter, priority_filter parameter names
|
||||
const params: Record<string, unknown> = {};
|
||||
if (filters?.type) params.type_filter = filters.type;
|
||||
if (filters?.priority) params.priority_filter = filters.priority;
|
||||
if (filters?.unread_only) params.unread_only = filters.unread_only;
|
||||
if (filters?.pending_ack_only) params.pending_ack_only = filters.pending_ack_only;
|
||||
|
||||
const { data } = await api.get<NotificationListResponse>("/notifications", { params });
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get notification by ID
|
||||
get: async (notificationId: string): Promise<Notification> => {
|
||||
if (isMockMode()) {
|
||||
const notification = mockNotifications.find((n) => n.id === notificationId);
|
||||
if (notification) return notification as Notification;
|
||||
throw new Error("Notification not found");
|
||||
}
|
||||
const { data } = await api.get<Notification>("/notifications/" + notificationId);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Mark notification as read (backend returns 204 No Content)
|
||||
markRead: async (notificationId: string): Promise<void> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockNotifications.findIndex((n) => n.id === notificationId);
|
||||
if (idx !== -1) {
|
||||
const notification = mockNotifications[idx];
|
||||
mockNotifications[idx] = { ...notification, is_read: true };
|
||||
}
|
||||
return;
|
||||
}
|
||||
await api.post("/notifications/" + notificationId + "/read");
|
||||
},
|
||||
|
||||
// Acknowledge notification (backend uses /ack)
|
||||
acknowledge: async (notificationId: string): Promise<Notification> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockNotifications.findIndex((n) => n.id === notificationId);
|
||||
if (idx !== -1) {
|
||||
const notification = mockNotifications[idx];
|
||||
const ackedNotification = {
|
||||
...notification,
|
||||
is_acknowledged: true,
|
||||
is_fully_acknowledged: true,
|
||||
};
|
||||
mockNotifications[idx] = ackedNotification;
|
||||
return ackedNotification as Notification;
|
||||
}
|
||||
throw new Error("Notification not found");
|
||||
}
|
||||
const { data } = await api.post<Notification>(
|
||||
"/notifications/" + notificationId + "/ack"
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Mark all as read
|
||||
markAllRead: async (): Promise<void> => {
|
||||
if (isMockMode()) {
|
||||
for (let i = 0; i < mockNotifications.length; i++) {
|
||||
mockNotifications[i] = { ...mockNotifications[i], is_read: true };
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Get all unread notifications
|
||||
const { data } = await api.get<{ items: Notification[] }>("/notifications", {
|
||||
params: { unread_only: true },
|
||||
});
|
||||
// Mark each as read
|
||||
await Promise.all(
|
||||
data.items.map((n) =>
|
||||
api.post("/notifications/" + n.id + "/read").catch(() => {
|
||||
// Ignore individual failures
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import api from "./client";
|
||||
import type { OrchestratorStatus, AgentStatusResponse, WaitingAgent } from "@/types";
|
||||
import { isMockMode, mockOrchestratorStatus, mockWaitingAgents } from "@/lib/mock-data";
|
||||
|
||||
export interface SpawnAgentRequest {
|
||||
task_id?: string;
|
||||
initial_prompt?: string;
|
||||
}
|
||||
|
||||
export const orchestratorApi = {
|
||||
// Get orchestrator status
|
||||
getStatus: async (): Promise<OrchestratorStatus> => {
|
||||
if (isMockMode()) {
|
||||
return mockOrchestratorStatus as OrchestratorStatus;
|
||||
}
|
||||
const { data } = await api.get<OrchestratorStatus>("/orchestrator/status");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get specific agent status
|
||||
getAgentStatus: async (agentId: string): Promise<AgentStatusResponse> => {
|
||||
if (isMockMode()) {
|
||||
const found = mockOrchestratorStatus.agents.find(a => a.agent_id === agentId);
|
||||
if (found) return found as AgentStatusResponse;
|
||||
throw new Error("Agent not found");
|
||||
}
|
||||
const { data } = await api.get<AgentStatusResponse>("/orchestrator/agents/" + agentId);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get waiting agents
|
||||
getWaitingAgents: async (): Promise<WaitingAgent[]> => {
|
||||
if (isMockMode()) {
|
||||
return mockWaitingAgents as WaitingAgent[];
|
||||
}
|
||||
const { data } = await api.get<WaitingAgent[]>("/orchestrator/waiting");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Spawn an agent
|
||||
spawn: async (agentId: string, request?: SpawnAgentRequest): Promise<AgentStatusResponse> => {
|
||||
if (isMockMode()) {
|
||||
// Find or create agent in mock status
|
||||
const existing = mockOrchestratorStatus.agents.find(a => a.agent_id === agentId);
|
||||
if (existing) {
|
||||
existing.state = "running";
|
||||
// Mock data uses string, so use empty string for no task
|
||||
(existing as { task_id: string }).task_id = request?.task_id ?? "";
|
||||
return existing as AgentStatusResponse;
|
||||
}
|
||||
const newAgent = {
|
||||
agent_id: agentId,
|
||||
state: "running",
|
||||
task_id: request?.task_id ?? "",
|
||||
error_count: 0,
|
||||
started_at: new Date().toISOString(),
|
||||
waiting_for: null as string | null,
|
||||
};
|
||||
mockOrchestratorStatus.agents.push(newAgent);
|
||||
return newAgent as AgentStatusResponse;
|
||||
}
|
||||
const { data } = await api.post<AgentStatusResponse>(
|
||||
"/orchestrator/agents/" + agentId + "/spawn",
|
||||
{ agent_id: agentId, ...request }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Stop an agent
|
||||
stop: async (agentId: string, graceful: boolean = true): Promise<void> => {
|
||||
if (isMockMode()) {
|
||||
const agent = mockOrchestratorStatus.agents.find(a => a.agent_id === agentId);
|
||||
if (agent) {
|
||||
agent.state = "idle";
|
||||
(agent as { task_id: string }).task_id = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
await api.post("/orchestrator/agents/" + agentId + "/stop", null, {
|
||||
params: { graceful },
|
||||
});
|
||||
},
|
||||
|
||||
// Resolve a waiting agent
|
||||
resolveWait: async (agentId: string, resolution: string): Promise<AgentStatusResponse> => {
|
||||
if (isMockMode()) {
|
||||
// Remove from waiting agents
|
||||
const idx = mockWaitingAgents.findIndex(a => a.agent_id === agentId);
|
||||
if (idx !== -1) mockWaitingAgents.splice(idx, 1);
|
||||
// Update agent status
|
||||
const agent = mockOrchestratorStatus.agents.find(a => a.agent_id === agentId);
|
||||
if (agent) {
|
||||
agent.state = "running";
|
||||
return agent as AgentStatusResponse;
|
||||
}
|
||||
throw new Error("Agent not found");
|
||||
}
|
||||
const { data } = await api.post<AgentStatusResponse>(
|
||||
"/orchestrator/agents/" + agentId + "/resolve-wait",
|
||||
{ resolution }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
import api from "./client";
|
||||
import type {
|
||||
Project,
|
||||
ProjectCreate,
|
||||
ProjectUpdate,
|
||||
ProjectSummary,
|
||||
Team,
|
||||
} from "@/types";
|
||||
import { isMockMode } from "@/lib/mock-data";
|
||||
|
||||
// Mock data for offline mode
|
||||
const mockProjects: Project[] = [];
|
||||
|
||||
export interface ProjectFilters {
|
||||
assigned_cell?: Team;
|
||||
active_only?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export const projectsApi = {
|
||||
// List projects with optional filters
|
||||
list: async (filters?: ProjectFilters): Promise<ProjectSummary[]> => {
|
||||
if (isMockMode()) {
|
||||
let projects = [...mockProjects];
|
||||
if (filters?.assigned_cell) {
|
||||
projects = projects.filter((p) => p.assigned_cell === filters.assigned_cell);
|
||||
}
|
||||
if (filters?.active_only) {
|
||||
projects = projects.filter((p) => p.is_active);
|
||||
}
|
||||
return projects.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
slug: p.slug,
|
||||
git_url: p.git_url,
|
||||
assigned_cell: p.assigned_cell,
|
||||
is_active: p.is_active,
|
||||
has_workspace: !!p.workspace_path,
|
||||
has_git_token: false, // Mock mode has no tokens
|
||||
}));
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.assigned_cell) params.append("assigned_cell", filters.assigned_cell);
|
||||
if (filters?.active_only !== undefined) params.append("active_only", String(filters.active_only));
|
||||
if (filters?.limit) params.append("limit", String(filters.limit));
|
||||
if (filters?.offset) params.append("offset", String(filters.offset));
|
||||
|
||||
const url = "/projects?" + params.toString();
|
||||
const { data } = await api.get<ProjectSummary[]>(url);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get single project
|
||||
get: async (projectId: string): Promise<Project> => {
|
||||
if (isMockMode()) {
|
||||
const project = mockProjects.find((p) => p.id === projectId);
|
||||
if (!project) throw new Error("Project not found");
|
||||
return project;
|
||||
}
|
||||
|
||||
const { data } = await api.get<Project>("/projects/" + projectId);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Create project (PM only)
|
||||
create: async (project: ProjectCreate): Promise<Project> => {
|
||||
if (isMockMode()) {
|
||||
const now = new Date().toISOString();
|
||||
const newProject: Project = {
|
||||
id: `project-${Date.now()}`,
|
||||
name: project.name,
|
||||
slug: project.slug,
|
||||
git_url: project.git_url,
|
||||
default_branch: project.default_branch ?? "main",
|
||||
protected_branches: project.protected_branches ?? ["main", "master"],
|
||||
assigned_cell: project.assigned_cell,
|
||||
has_git_token: !!project.git_token, // Mock token status
|
||||
is_active: true,
|
||||
test_command: project.test_command ?? null,
|
||||
lint_command: project.lint_command ?? null,
|
||||
format_command: project.format_command ?? null,
|
||||
typecheck_command: project.typecheck_command ?? null,
|
||||
build_command: project.build_command ?? null,
|
||||
workspace_path: null,
|
||||
last_synced_at: null,
|
||||
head_commit: null,
|
||||
created_by: "mock-user",
|
||||
created_at: now,
|
||||
updated_at: null,
|
||||
};
|
||||
mockProjects.push(newProject);
|
||||
return newProject;
|
||||
}
|
||||
const { data } = await api.post<Project>("/projects", project);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Update project (PM only)
|
||||
update: async (projectId: string, updates: ProjectUpdate): Promise<Project> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockProjects.findIndex((p) => p.id === projectId);
|
||||
if (idx === -1) throw new Error("Project not found");
|
||||
const now = new Date().toISOString();
|
||||
mockProjects[idx] = { ...mockProjects[idx], ...updates, updated_at: now };
|
||||
return mockProjects[idx];
|
||||
}
|
||||
const { data } = await api.patch<Project>("/projects/" + projectId, updates);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Set workspace path for local development
|
||||
setWorkspace: async (projectId: string, workspacePath: string): Promise<Project> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockProjects.findIndex((p) => p.id === projectId);
|
||||
if (idx === -1) throw new Error("Project not found");
|
||||
const now = new Date().toISOString();
|
||||
mockProjects[idx] = { ...mockProjects[idx], workspace_path: workspacePath, updated_at: now };
|
||||
return mockProjects[idx];
|
||||
}
|
||||
const { data } = await api.post<Project>("/projects/" + projectId + "/workspace", { workspace_path: workspacePath });
|
||||
return data;
|
||||
},
|
||||
|
||||
// Update sync state (for tracking git status)
|
||||
updateSyncState: async (
|
||||
projectId: string,
|
||||
headCommit: string
|
||||
): Promise<Project> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockProjects.findIndex((p) => p.id === projectId);
|
||||
if (idx === -1) throw new Error("Project not found");
|
||||
const now = new Date().toISOString();
|
||||
mockProjects[idx] = {
|
||||
...mockProjects[idx],
|
||||
head_commit: headCommit,
|
||||
last_synced_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
return mockProjects[idx];
|
||||
}
|
||||
const { data } = await api.post<Project>("/projects/" + projectId + "/sync-state", {
|
||||
head_commit: headCommit,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// Deactivate project (soft delete)
|
||||
deactivate: async (projectId: string): Promise<Project> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockProjects.findIndex((p) => p.id === projectId);
|
||||
if (idx === -1) throw new Error("Project not found");
|
||||
const now = new Date().toISOString();
|
||||
mockProjects[idx] = { ...mockProjects[idx], is_active: false, updated_at: now };
|
||||
return mockProjects[idx];
|
||||
}
|
||||
const { data } = await api.patch<Project>("/projects/" + projectId, { is_active: false });
|
||||
return data;
|
||||
},
|
||||
|
||||
// Delete project permanently
|
||||
delete: async (projectId: string): Promise<void> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockProjects.findIndex((p) => p.id === projectId);
|
||||
if (idx !== -1) mockProjects.splice(idx, 1);
|
||||
return;
|
||||
}
|
||||
await api.delete("/projects/" + projectId);
|
||||
},
|
||||
|
||||
// Grant agent access to project
|
||||
grantAccess: async (projectId: string, agentId: string): Promise<void> => {
|
||||
if (isMockMode()) {
|
||||
return;
|
||||
}
|
||||
await api.post("/projects/" + projectId + "/access/" + agentId);
|
||||
},
|
||||
|
||||
// Revoke agent access from project
|
||||
revokeAccess: async (projectId: string, agentId: string): Promise<void> => {
|
||||
if (isMockMode()) {
|
||||
return;
|
||||
}
|
||||
await api.delete("/projects/" + projectId + "/access/" + agentId);
|
||||
},
|
||||
|
||||
// Trigger git sync for project
|
||||
sync: async (projectId: string): Promise<Project> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockProjects.findIndex((p) => p.id === projectId);
|
||||
if (idx === -1) throw new Error("Project not found");
|
||||
const now = new Date().toISOString();
|
||||
mockProjects[idx] = {
|
||||
...mockProjects[idx],
|
||||
last_synced_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
return mockProjects[idx];
|
||||
}
|
||||
const { data } = await api.post<Project>("/projects/" + projectId + "/sync");
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import api from "./client";
|
||||
import type {
|
||||
AssignmentScope,
|
||||
ModelProvider,
|
||||
} from "@/types";
|
||||
|
||||
// Matches the backend's CatalogEntryResponse.
|
||||
export interface CatalogEntry {
|
||||
model_name: string;
|
||||
provider_type: ModelProvider;
|
||||
display_name: string;
|
||||
}
|
||||
|
||||
export interface OllamaKeyStatus {
|
||||
has_key: boolean;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface ModelAssignment {
|
||||
id: string;
|
||||
scope: AssignmentScope;
|
||||
scope_value: string | null;
|
||||
provider_type: ModelProvider;
|
||||
model_name: string;
|
||||
}
|
||||
|
||||
export type RoutingMode = "anthropic" | "ollama" | "mix";
|
||||
|
||||
export interface ModeSnapshot {
|
||||
mode: RoutingMode;
|
||||
assignments: ModelAssignment[];
|
||||
}
|
||||
|
||||
export interface ApplyModePayload {
|
||||
mode: RoutingMode;
|
||||
default_model?: string;
|
||||
// Required when mode=mix: map of agent_slug → model_name.
|
||||
per_agent?: Record<string, string>;
|
||||
}
|
||||
|
||||
export const providersApi = {
|
||||
catalog: async (): Promise<CatalogEntry[]> => {
|
||||
const { data } = await api.get<CatalogEntry[]>("/providers/catalog");
|
||||
return data;
|
||||
},
|
||||
|
||||
getOllamaKey: async (): Promise<OllamaKeyStatus> => {
|
||||
const { data } = await api.get<OllamaKeyStatus>("/providers/ollama-key");
|
||||
return data;
|
||||
},
|
||||
|
||||
setOllamaKey: async (apiKey: string): Promise<OllamaKeyStatus> => {
|
||||
const { data } = await api.put<OllamaKeyStatus>("/providers/ollama-key", {
|
||||
api_key: apiKey,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
getMode: async (): Promise<ModeSnapshot> => {
|
||||
const { data } = await api.get<ModeSnapshot>("/providers");
|
||||
return data;
|
||||
},
|
||||
|
||||
applyMode: async (payload: ApplyModePayload): Promise<ModeSnapshot> => {
|
||||
const { data } = await api.post<ModeSnapshot>("/providers", payload);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,182 @@
|
||||
import api from "./client";
|
||||
import type { Session } from "@/types";
|
||||
import { SessionStatus, SessionScope } from "@/types";
|
||||
import { isMockMode, mockSessions } from "@/lib/mock-data";
|
||||
|
||||
// Session-Task link response from API
|
||||
export interface SessionTaskLinkResponse {
|
||||
id: string;
|
||||
session_id: string;
|
||||
task_id: string;
|
||||
is_primary: boolean;
|
||||
relationship_type: string;
|
||||
added_at: string;
|
||||
added_by: string | null;
|
||||
}
|
||||
|
||||
export interface SessionCreate {
|
||||
group_id: string;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
export const sessionsApi = {
|
||||
// List sessions for a group
|
||||
listByGroup: async (groupId: string, limit: number = 50): Promise<Session[]> => {
|
||||
if (isMockMode()) {
|
||||
return (mockSessions as Session[]).slice(0, limit);
|
||||
}
|
||||
const { data } = await api.get<{ items: Session[]; total: number }>("/sessions", {
|
||||
params: { group_id: groupId, limit },
|
||||
});
|
||||
return data.items;
|
||||
},
|
||||
|
||||
// Get session by ID
|
||||
get: async (sessionId: string): Promise<Session> => {
|
||||
if (isMockMode()) {
|
||||
const session = mockSessions.find((s) => s.id === sessionId);
|
||||
if (session) return session as Session;
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
const { data } = await api.get<Session>("/sessions/" + sessionId);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get sessions linked to a task
|
||||
getForTask: async (taskId: string): Promise<SessionTaskLinkResponse[]> => {
|
||||
if (isMockMode()) {
|
||||
return []; // No mock session-task links
|
||||
}
|
||||
const { data } = await api.get<SessionTaskLinkResponse[]>("/sessions/for-task/" + taskId);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Close a session
|
||||
close: async (sessionId: string): Promise<Session> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockSessions.findIndex((s) => s.id === sessionId);
|
||||
if (idx !== -1) {
|
||||
const session = mockSessions[idx] as Session;
|
||||
const closedSession: Session = {
|
||||
...session,
|
||||
status: "closed" as SessionStatus,
|
||||
closed_at: new Date().toISOString(),
|
||||
};
|
||||
(mockSessions as Session[])[idx] = closedSession;
|
||||
return closedSession;
|
||||
}
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
const { data } = await api.post<Session>("/sessions/" + sessionId + "/close");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Link a task to a session (PM only)
|
||||
linkTask: async (
|
||||
sessionId: string,
|
||||
taskId: string,
|
||||
isPrimary: boolean = false,
|
||||
relationshipType: string = "discussion"
|
||||
): Promise<SessionTaskLinkResponse> => {
|
||||
const { data } = await api.post<SessionTaskLinkResponse>(
|
||||
"/sessions/" + sessionId + "/add-task",
|
||||
{
|
||||
task_id: taskId,
|
||||
is_primary: isPrimary,
|
||||
relationship_type: relationshipType,
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Unlink a task from a session (PM only)
|
||||
unlinkTask: async (sessionId: string, taskId: string): Promise<void> => {
|
||||
await api.delete("/sessions/" + sessionId + "/remove-task", {
|
||||
data: { task_id: taskId },
|
||||
});
|
||||
},
|
||||
|
||||
// Get tasks linked to a session
|
||||
getTasksForSession: async (sessionId: string): Promise<SessionTaskLinkResponse[]> => {
|
||||
// Note: Backend doesn't have a dedicated GET endpoint for session tasks
|
||||
// Task links are returned with session detail via get()
|
||||
const session = await sessionsApi.get(sessionId);
|
||||
// Return empty array if session doesn't have task_links
|
||||
return (session as Session & { task_links?: SessionTaskLinkResponse[] }).task_links || [];
|
||||
},
|
||||
|
||||
// Create a session for tasks (PM only)
|
||||
createForTasks: async (
|
||||
taskIds: string[],
|
||||
channelSlug: string,
|
||||
relationshipType: string = "discussion"
|
||||
): Promise<{ session: Session; links: SessionTaskLinkResponse[] }> => {
|
||||
const { data } = await api.post<{ session: Session; links: SessionTaskLinkResponse[] }>(
|
||||
"/sessions/for-tasks",
|
||||
{
|
||||
task_ids: taskIds,
|
||||
channel_slug: channelSlug,
|
||||
relationship_type: relationshipType,
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Create a new session directly
|
||||
create: async (session: SessionCreate): Promise<Session> => {
|
||||
if (isMockMode()) {
|
||||
const now = new Date().toISOString();
|
||||
const newSession: Session = {
|
||||
id: `session-${Date.now()}`,
|
||||
group_id: session.group_id,
|
||||
status: SessionStatus.ACTIVE,
|
||||
scope: (session.scope as SessionScope) || SessionScope.CELL,
|
||||
message_count: 0,
|
||||
total_content_length: 0,
|
||||
started_at: now,
|
||||
last_activity_at: now,
|
||||
closed_at: null,
|
||||
task_links: [],
|
||||
};
|
||||
(mockSessions as Session[]).push(newSession);
|
||||
return newSession;
|
||||
}
|
||||
const { data } = await api.post<Session>("/sessions", session);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Update a task link in a session
|
||||
updateTaskLink: async (
|
||||
sessionId: string,
|
||||
taskId: string,
|
||||
updates: { is_primary?: boolean; relationship_type?: string }
|
||||
): Promise<SessionTaskLinkResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
id: `link-${Date.now()}`,
|
||||
session_id: sessionId,
|
||||
task_id: taskId,
|
||||
is_primary: updates.is_primary ?? false,
|
||||
relationship_type: updates.relationship_type ?? "discussion",
|
||||
added_at: new Date().toISOString(),
|
||||
added_by: null,
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<SessionTaskLinkResponse>(
|
||||
"/sessions/" + sessionId + "/update-task",
|
||||
{ task_id: taskId, ...updates }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
|
||||
export const groupsApi = {
|
||||
// Get groups for a channel
|
||||
listByChannel: async (channelId: string): Promise<unknown[]> => {
|
||||
if (isMockMode()) {
|
||||
return [];
|
||||
}
|
||||
const { data } = await api.get<unknown[]>("/channels/" + channelId + "/groups");
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Stream API Client
|
||||
*
|
||||
* API functions for transcription and extraction operations.
|
||||
*/
|
||||
|
||||
import api from "./client";
|
||||
import { isMockMode } from "@/lib/mock-data";
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
export interface StreamChunkRequest {
|
||||
channel_name: string;
|
||||
audio_data: string; // Base64 encoded
|
||||
chunk_index: number;
|
||||
is_final?: boolean;
|
||||
}
|
||||
|
||||
export interface StreamChunkResponse {
|
||||
status: string;
|
||||
chunk_index: number;
|
||||
queued: boolean;
|
||||
}
|
||||
|
||||
export interface StreamCompleteRequest {
|
||||
channel_name: string;
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
export interface StreamCompleteResponse {
|
||||
status: string;
|
||||
channel_name: string;
|
||||
total_chunks: number;
|
||||
transcription_id?: string;
|
||||
}
|
||||
|
||||
export interface ExtractionRequest {
|
||||
content: string;
|
||||
extraction_type: "tasks" | "decisions" | "action_items" | "summary";
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export interface ExtractionResponse {
|
||||
extraction_type: string;
|
||||
results: Record<string, unknown>[];
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
export interface TranscriptionStats {
|
||||
total_transcriptions: number;
|
||||
active_channels: number;
|
||||
chunks_processed: number;
|
||||
avg_processing_time_ms: number;
|
||||
}
|
||||
|
||||
export interface ChannelPermissions {
|
||||
channel_name: string;
|
||||
can_transcribe: boolean;
|
||||
can_extract: boolean;
|
||||
rate_limit?: number;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API Client
|
||||
// =============================================================================
|
||||
|
||||
export const streamApi = {
|
||||
// ===========================================================================
|
||||
// TRANSCRIPTION ENDPOINTS
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Submit an audio chunk for transcription
|
||||
*/
|
||||
submitChunk: async (request: StreamChunkRequest): Promise<StreamChunkResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
status: "queued",
|
||||
chunk_index: request.chunk_index,
|
||||
queued: true,
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<StreamChunkResponse>("/stream/chunk", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Complete a transcription session
|
||||
*/
|
||||
complete: async (request: StreamCompleteRequest): Promise<StreamCompleteResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
status: "completed",
|
||||
channel_name: request.channel_name,
|
||||
total_chunks: 10,
|
||||
transcription_id: `trans-${Date.now()}`,
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<StreamCompleteResponse>("/stream/complete", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// EXTRACTION ENDPOINTS
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Extract structured information from content
|
||||
*/
|
||||
extract: async (request: ExtractionRequest): Promise<ExtractionResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
extraction_type: request.extraction_type,
|
||||
results: [],
|
||||
confidence: 0.85,
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<ExtractionResponse>("/stream/extract", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// STATS & PERMISSIONS
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Get transcription statistics
|
||||
*/
|
||||
getStats: async (): Promise<TranscriptionStats> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
total_transcriptions: 100,
|
||||
active_channels: 5,
|
||||
chunks_processed: 1500,
|
||||
avg_processing_time_ms: 250,
|
||||
};
|
||||
}
|
||||
const { data } = await api.get<TranscriptionStats>("/stream/stats");
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all channel permissions
|
||||
*/
|
||||
getPermissions: async (): Promise<ChannelPermissions[]> => {
|
||||
if (isMockMode()) {
|
||||
return [];
|
||||
}
|
||||
const { data } = await api.get<ChannelPermissions[]>("/stream/permissions");
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get permissions for a specific channel
|
||||
*/
|
||||
getChannelPermissions: async (channelName: string): Promise<ChannelPermissions> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
channel_name: channelName,
|
||||
can_transcribe: true,
|
||||
can_extract: true,
|
||||
};
|
||||
}
|
||||
const { data } = await api.get<ChannelPermissions>(`/stream/permissions/channel/${channelName}`);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,651 @@
|
||||
import api from "./client";
|
||||
import { TaskStatus, Complexity, TaskNature, TaskType } from "@/types";
|
||||
import type {
|
||||
Task,
|
||||
TaskCreate,
|
||||
Team,
|
||||
ProgressRequest,
|
||||
CheckpointRequest,
|
||||
CommitRequest,
|
||||
SoftBlockRequest,
|
||||
EscalateRequest,
|
||||
EscalateResponse,
|
||||
TaskCountResponse,
|
||||
} from "@/types";
|
||||
import { isMockMode, mockTasks } from "@/lib/mock-data";
|
||||
|
||||
export interface TaskFilters {
|
||||
status?: TaskStatus;
|
||||
team?: Team;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export const tasksApi = {
|
||||
// List tasks with optional filters
|
||||
list: async (filters?: TaskFilters): Promise<Task[]> => {
|
||||
if (isMockMode()) {
|
||||
let tasks = [...mockTasks];
|
||||
if (filters?.status) {
|
||||
tasks = tasks.filter((t) => t.status === filters.status);
|
||||
}
|
||||
if (filters?.team) {
|
||||
tasks = tasks.filter((t) => t.team === filters.team);
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.status) params.append("status", filters.status);
|
||||
if (filters?.team) params.append("team", filters.team);
|
||||
if (filters?.limit) params.append("limit", String(filters.limit));
|
||||
if (filters?.offset) params.append("offset", String(filters.offset));
|
||||
|
||||
const url = "/tasks?" + params.toString();
|
||||
const { data } = await api.get<Task[]>(url);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get single task
|
||||
get: async (taskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const task = mockTasks.find((t) => t.id === taskId);
|
||||
if (!task) throw new Error("Task not found");
|
||||
return task;
|
||||
}
|
||||
|
||||
const { data } = await api.get<Task>("/tasks/" + taskId);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Create task
|
||||
create: async (task: TaskCreate): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const now = new Date().toISOString();
|
||||
const newTask: Task = {
|
||||
id: `task-${Date.now()}`,
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
team: task.team,
|
||||
priority: task.priority ?? 2,
|
||||
sequence: mockTasks.length + 1, // Auto-increment sequence
|
||||
estimated_complexity: task.estimated_complexity ?? Complexity.MEDIUM,
|
||||
nature: task.nature ?? TaskNature.TECHNICAL,
|
||||
task_type: task.task_type ?? TaskType.CODE,
|
||||
project_id: task.project_id,
|
||||
docs_complete: false,
|
||||
pr_created: false,
|
||||
pm_approvals: {},
|
||||
acceptance_criteria: task.acceptance_criteria,
|
||||
parent_task_id: task.parent_task_id ?? null,
|
||||
target_date: task.target_date ?? null,
|
||||
status: task.status ?? TaskStatus.PENDING,
|
||||
dependency_ids: [],
|
||||
blocker_ids: [],
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by: "00000000-0000-0000-0000-000000000001", // CEO
|
||||
assigned_to: null,
|
||||
claimed_at: null,
|
||||
started_at: null,
|
||||
completed_at: null,
|
||||
self_verified: false,
|
||||
qa_verified: null,
|
||||
plan: null,
|
||||
progress_updates: [],
|
||||
checkpoints: [],
|
||||
commits: [],
|
||||
dev_notes: null,
|
||||
qa_notes: null,
|
||||
auditor_notes: null,
|
||||
quick_context: null,
|
||||
sessions: [],
|
||||
branch_name: null,
|
||||
pr_number: null,
|
||||
pr_url: null,
|
||||
};
|
||||
mockTasks.push(newTask);
|
||||
return newTask;
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks", task);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Update task
|
||||
update: async (taskId: string, updates: Partial<Task>): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
mockTasks[idx] = { ...mockTasks[idx], ...updates, updated_at: new Date().toISOString() };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.put<Task>("/tasks/" + taskId, updates);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Delete task
|
||||
delete: async (taskId: string): Promise<void> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx !== -1) mockTasks.splice(idx, 1);
|
||||
return;
|
||||
}
|
||||
await api.delete("/tasks/" + taskId);
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// LIFECYCLE ACTIONS
|
||||
// =========================================================================
|
||||
|
||||
claim: async (taskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.CLAIMED, claimed_at: now, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/claim");
|
||||
return data;
|
||||
},
|
||||
|
||||
start: async (taskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.IN_PROGRESS, started_at: now, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/start");
|
||||
return data;
|
||||
},
|
||||
|
||||
block: async (taskId: string, blockerId?: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
const blockerIds = blockerId ? [...mockTasks[idx].blocker_ids, blockerId] : mockTasks[idx].blocker_ids;
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.BLOCKED, blocker_ids: blockerIds, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/block", { blocker_id: blockerId });
|
||||
return data;
|
||||
},
|
||||
|
||||
unblock: async (taskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.IN_PROGRESS, blocker_ids: [], updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/unblock");
|
||||
return data;
|
||||
},
|
||||
|
||||
pause: async (taskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.PAUSED, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/pause");
|
||||
return data;
|
||||
},
|
||||
|
||||
resume: async (taskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.IN_PROGRESS, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/resume");
|
||||
return data;
|
||||
},
|
||||
|
||||
verify: async (taskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.VERIFYING, self_verified: true, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/verify");
|
||||
return data;
|
||||
},
|
||||
|
||||
submitQa: async (taskId: string, devNotes?: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.AWAITING_QA, dev_notes: devNotes ?? null, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/submit-qa", { dev_notes: devNotes });
|
||||
return data;
|
||||
},
|
||||
|
||||
passQa: async (taskId: string, qaNotes?: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.AWAITING_DOCUMENTATION, qa_verified: true, qa_notes: qaNotes ?? null, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/pass-qa", { qa_notes: qaNotes });
|
||||
return data;
|
||||
},
|
||||
|
||||
failQa: async (taskId: string, qaNotes?: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.NEEDS_REVISION, qa_verified: false, qa_notes: qaNotes ?? null, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/fail-qa", { qa_notes: qaNotes });
|
||||
return data;
|
||||
},
|
||||
|
||||
complete: async (taskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.COMPLETED, completed_at: now, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/complete");
|
||||
return data;
|
||||
},
|
||||
|
||||
cancel: async (taskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.CANCELLED, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/cancel");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Note: reopen is not supported by backend - use update with status change instead
|
||||
reopen: async (taskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.PENDING, completed_at: null, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
// Backend doesn't have /reopen endpoint - use update instead
|
||||
const { data } = await api.put<Task>("/tasks/" + taskId, { status: TaskStatus.PENDING });
|
||||
return data;
|
||||
},
|
||||
|
||||
// Activate a task from BACKLOG status (PM only)
|
||||
activate: async (taskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.PENDING, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/activate");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Mark documentation as complete (Documenter only)
|
||||
docsComplete: async (taskId: string, docNotes?: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.AWAITING_PM_REVIEW, updated_at: now };
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/docs-complete", docNotes ?? null);
|
||||
return data;
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// CONVENIENCE METHODS
|
||||
// =========================================================================
|
||||
|
||||
getMyTasks: async (): Promise<Task[]> => {
|
||||
if (isMockMode()) {
|
||||
// Return tasks assigned to "me" (mock: any assigned task)
|
||||
return mockTasks.filter((t) => t.assigned_to !== null);
|
||||
}
|
||||
const { data } = await api.get<Task[]>("/tasks/my");
|
||||
return data;
|
||||
},
|
||||
|
||||
getPending: async (): Promise<Task[]> => {
|
||||
if (isMockMode()) {
|
||||
return mockTasks.filter((t) => t.status === TaskStatus.PENDING);
|
||||
}
|
||||
const { data } = await api.get<Task[]>("/tasks/pending");
|
||||
return data;
|
||||
},
|
||||
|
||||
getBlocked: async (): Promise<Task[]> => {
|
||||
if (isMockMode()) {
|
||||
return mockTasks.filter((t) => t.status === TaskStatus.BLOCKED);
|
||||
}
|
||||
const { data } = await api.get<Task[]>("/tasks/blocked");
|
||||
return data;
|
||||
},
|
||||
|
||||
getAwaitingQa: async (): Promise<Task[]> => {
|
||||
if (isMockMode()) {
|
||||
return mockTasks.filter((t) => t.status === TaskStatus.AWAITING_QA);
|
||||
}
|
||||
const { data } = await api.get<Task[]>("/tasks/awaiting-qa");
|
||||
return data;
|
||||
},
|
||||
|
||||
getTeamTasks: async (team: Team): Promise<Task[]> => {
|
||||
if (isMockMode()) {
|
||||
return mockTasks.filter((t) => t.team === team);
|
||||
}
|
||||
const { data } = await api.get<Task[]>("/tasks/team/" + team);
|
||||
return data;
|
||||
},
|
||||
|
||||
getAwaitingDocs: async (): Promise<Task[]> => {
|
||||
if (isMockMode()) {
|
||||
return mockTasks.filter((t) => t.status === TaskStatus.AWAITING_DOCUMENTATION);
|
||||
}
|
||||
const { data } = await api.get<Task[]>("/tasks/awaiting-docs");
|
||||
return data;
|
||||
},
|
||||
|
||||
getSubtasks: async (taskId: string): Promise<Task[]> => {
|
||||
if (isMockMode()) {
|
||||
return mockTasks.filter((t) => t.parent_task_id === taskId);
|
||||
}
|
||||
const { data } = await api.get<Task[]>("/tasks/" + taskId + "/subtasks");
|
||||
return data;
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// STATS
|
||||
// =========================================================================
|
||||
|
||||
getStats: async (): Promise<TaskCountResponse> => {
|
||||
if (isMockMode()) {
|
||||
const counts: Record<string, number> = {};
|
||||
mockTasks.forEach((t) => {
|
||||
counts[t.status] = (counts[t.status] || 0) + 1;
|
||||
});
|
||||
return { counts };
|
||||
}
|
||||
const { data } = await api.get<TaskCountResponse>("/tasks/stats");
|
||||
return data;
|
||||
},
|
||||
|
||||
getStatsByTeam: async (): Promise<TaskCountResponse> => {
|
||||
if (isMockMode()) {
|
||||
const counts: Record<string, number> = {};
|
||||
mockTasks.forEach((t) => {
|
||||
counts[t.team] = (counts[t.team] || 0) + 1;
|
||||
});
|
||||
return { counts };
|
||||
}
|
||||
const { data } = await api.get<TaskCountResponse>("/tasks/stats/by-team");
|
||||
return data;
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// PROGRESS TRACKING
|
||||
// =========================================================================
|
||||
|
||||
addProgress: async (taskId: string, request: ProgressRequest): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
const update = {
|
||||
timestamp: now,
|
||||
agent_id: "00000000-0000-0000-0000-000000000001",
|
||||
message: request.message,
|
||||
percentage: request.percentage ?? null,
|
||||
};
|
||||
mockTasks[idx] = {
|
||||
...mockTasks[idx],
|
||||
progress_updates: [...mockTasks[idx].progress_updates, update],
|
||||
updated_at: now,
|
||||
};
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/progress", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
addCheckpoint: async (taskId: string, request: CheckpointRequest): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
const checkpoint = {
|
||||
id: `checkpoint-${Date.now()}`,
|
||||
timestamp: now,
|
||||
agent_id: "00000000-0000-0000-0000-000000000001",
|
||||
state_summary: request.state_summary,
|
||||
remaining_work: request.remaining_work,
|
||||
notes: request.notes ?? null,
|
||||
};
|
||||
mockTasks[idx] = {
|
||||
...mockTasks[idx],
|
||||
checkpoints: [...mockTasks[idx].checkpoints, checkpoint],
|
||||
updated_at: now,
|
||||
};
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/checkpoint", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
addCommit: async (taskId: string, request: CommitRequest): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
const commit = {
|
||||
hash: request.hash,
|
||||
message: request.message,
|
||||
timestamp: now,
|
||||
author_agent_id: "00000000-0000-0000-0000-000000000001",
|
||||
};
|
||||
mockTasks[idx] = {
|
||||
...mockTasks[idx],
|
||||
commits: [...mockTasks[idx].commits, commit],
|
||||
updated_at: now,
|
||||
};
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/commit", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// SOFT BLOCK & ESCALATION
|
||||
// =========================================================================
|
||||
|
||||
softBlock: async (taskId: string, request: SoftBlockRequest): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = {
|
||||
...mockTasks[idx],
|
||||
status: TaskStatus.BLOCKED,
|
||||
updated_at: now,
|
||||
};
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/soft-block", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
escalate: async (taskId: string, request: EscalateRequest): Promise<EscalateResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
status: "escalated",
|
||||
task_id: taskId,
|
||||
escalated_to: request.escalate_to || "cell-pm",
|
||||
reason: request.reason,
|
||||
message: "Task escalated successfully (mock)",
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<EscalateResponse>("/tasks/" + taskId + "/escalate", request);
|
||||
return data;
|
||||
},
|
||||
|
||||
submitPmReview: async (taskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = {
|
||||
...mockTasks[idx],
|
||||
status: TaskStatus.AWAITING_PM_REVIEW,
|
||||
updated_at: now,
|
||||
};
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/submit-pm-review");
|
||||
return data;
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// CEO APPROVAL (CEO only - Human-in-the-Loop)
|
||||
// =========================================================================
|
||||
|
||||
// Get tasks awaiting CEO approval
|
||||
getAwaitingCeoApproval: async (): Promise<Task[]> => {
|
||||
if (isMockMode()) {
|
||||
return mockTasks.filter((t) => t.status === TaskStatus.AWAITING_CEO_APPROVAL);
|
||||
}
|
||||
const { data } = await api.get<Task[]>("/tasks/awaiting-ceo-approval");
|
||||
return data;
|
||||
},
|
||||
|
||||
// CEO approves a task (completes it)
|
||||
ceoApprove: async (taskId: string, notes?: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = {
|
||||
...mockTasks[idx],
|
||||
status: TaskStatus.COMPLETED,
|
||||
completed_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/ceo-approve", { notes });
|
||||
return data;
|
||||
},
|
||||
|
||||
// CEO rejects a task (sends back for revision)
|
||||
ceoReject: async (taskId: string, notes: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = {
|
||||
...mockTasks[idx],
|
||||
status: TaskStatus.NEEDS_REVISION,
|
||||
updated_at: now,
|
||||
};
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/ceo-reject", { notes });
|
||||
return data;
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// ADDITIONAL CONVENIENCE METHODS
|
||||
// =========================================================================
|
||||
|
||||
// Get tasks awaiting PM review
|
||||
getAwaitingPmReview: async (): Promise<Task[]> => {
|
||||
if (isMockMode()) {
|
||||
return mockTasks.filter((t) => t.status === TaskStatus.AWAITING_PM_REVIEW);
|
||||
}
|
||||
const { data } = await api.get<Task[]>("/tasks/awaiting-pm-review");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get all descendants of a task (subtasks, sub-subtasks, etc.)
|
||||
getDescendants: async (taskId: string): Promise<Task[]> => {
|
||||
if (isMockMode()) {
|
||||
// Simple mock: just return direct subtasks
|
||||
return mockTasks.filter((t) => t.parent_task_id === taskId);
|
||||
}
|
||||
const { data } = await api.get<Task[]>("/tasks/" + taskId + "/descendants");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get sessions linked to a task
|
||||
getSessions: async (taskId: string): Promise<{ session_id: string; is_primary: boolean; relationship_type: string }[]> => {
|
||||
if (isMockMode()) {
|
||||
return [];
|
||||
}
|
||||
const { data } = await api.get<{ session_id: string; is_primary: boolean; relationship_type: string }[]>(
|
||||
"/tasks/" + taskId + "/sessions"
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Escalate task directly to CEO
|
||||
escalateToCeo: async (taskId: string, reason: string): Promise<EscalateResponse> => {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
status: "escalated",
|
||||
task_id: taskId,
|
||||
escalated_to: "ceo",
|
||||
reason,
|
||||
message: "Task escalated to CEO (mock)",
|
||||
};
|
||||
}
|
||||
const { data } = await api.post<EscalateResponse>("/tasks/" + taskId + "/escalate-to-ceo", { reason });
|
||||
return data;
|
||||
},
|
||||
|
||||
// Substitute a task with another (create replacement)
|
||||
substitute: async (taskId: string, replacementTaskId: string): Promise<Task> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockTasks.findIndex((t) => t.id === taskId);
|
||||
if (idx === -1) throw new Error("Task not found");
|
||||
const now = new Date().toISOString();
|
||||
mockTasks[idx] = {
|
||||
...mockTasks[idx],
|
||||
status: TaskStatus.CANCELLED,
|
||||
updated_at: now,
|
||||
};
|
||||
return mockTasks[idx];
|
||||
}
|
||||
const { data } = await api.post<Task>("/tasks/" + taskId + "/substitute", {
|
||||
replacement_task_id: replacementTaskId,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
import api from "./client";
|
||||
import type {
|
||||
WorkSession,
|
||||
WorkSessionSummary,
|
||||
WorkSessionCreate,
|
||||
WorkSessionStatus,
|
||||
} from "@/types";
|
||||
import { isMockMode } from "@/lib/mock-data";
|
||||
|
||||
// Mock data for offline mode
|
||||
const mockWorkSessions: WorkSession[] = [];
|
||||
|
||||
export interface WorkSessionFilters {
|
||||
project_id?: string;
|
||||
agent_id?: string;
|
||||
status?: WorkSessionStatus;
|
||||
active_only?: boolean;
|
||||
}
|
||||
|
||||
export const workSessionsApi = {
|
||||
// List work sessions with optional filters
|
||||
list: async (filters?: WorkSessionFilters): Promise<WorkSessionSummary[]> => {
|
||||
if (isMockMode()) {
|
||||
let sessions = [...mockWorkSessions];
|
||||
if (filters?.project_id) {
|
||||
sessions = sessions.filter((s) => s.project_id === filters.project_id);
|
||||
}
|
||||
if (filters?.agent_id) {
|
||||
sessions = sessions.filter((s) => s.agent_id === filters.agent_id);
|
||||
}
|
||||
if (filters?.status) {
|
||||
sessions = sessions.filter((s) => s.status === filters.status);
|
||||
}
|
||||
if (filters?.active_only) {
|
||||
sessions = sessions.filter((s) => s.status === "active");
|
||||
}
|
||||
return sessions.map((s) => ({
|
||||
id: s.id,
|
||||
task_id: s.task_id,
|
||||
branch_name: s.branch_name,
|
||||
status: s.status,
|
||||
started_at: s.started_at,
|
||||
has_pr: s.pr_number !== null,
|
||||
}));
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.project_id) params.append("project_id", filters.project_id);
|
||||
if (filters?.agent_id) params.append("agent_id", filters.agent_id);
|
||||
if (filters?.status) params.append("status", filters.status);
|
||||
if (filters?.active_only) params.append("active_only", "true");
|
||||
|
||||
const url = "/work-sessions?" + params.toString();
|
||||
const { data } = await api.get<WorkSessionSummary[]>(url);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get single work session
|
||||
get: async (sessionId: string): Promise<WorkSession> => {
|
||||
if (isMockMode()) {
|
||||
const session = mockWorkSessions.find((s) => s.id === sessionId);
|
||||
if (!session) throw new Error("Work session not found");
|
||||
return session;
|
||||
}
|
||||
|
||||
const { data } = await api.get<WorkSession>("/work-sessions/" + sessionId);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get active work session for a task
|
||||
getForTask: async (taskId: string): Promise<WorkSession | null> => {
|
||||
if (isMockMode()) {
|
||||
const session = mockWorkSessions.find(
|
||||
(s) => s.task_id === taskId && s.status === "active"
|
||||
);
|
||||
return session ?? null;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await api.get<WorkSession | null>("/work-sessions/task/" + taskId);
|
||||
return data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// Create work session (Developer or PM)
|
||||
create: async (session: WorkSessionCreate): Promise<WorkSession> => {
|
||||
if (isMockMode()) {
|
||||
const now = new Date().toISOString();
|
||||
const newSession: WorkSession = {
|
||||
id: `session-${Date.now()}`,
|
||||
project_id: session.project_id,
|
||||
task_id: session.task_id,
|
||||
agent_id: "00000000-0000-0000-0000-000000000001", // CEO in mock
|
||||
branch_name: session.branch_name,
|
||||
base_branch: session.base_branch,
|
||||
target_branch: session.target_branch,
|
||||
started_at: now,
|
||||
ended_at: null,
|
||||
status: "active" as WorkSessionStatus,
|
||||
commits: [],
|
||||
files_modified: [],
|
||||
pr_number: null,
|
||||
pr_url: null,
|
||||
pr_status: null,
|
||||
pr_created_at: null,
|
||||
pr_merged_at: null,
|
||||
merged_by: null,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
mockWorkSessions.push(newSession);
|
||||
return newSession;
|
||||
}
|
||||
const { data } = await api.post<WorkSession>("/work-sessions", session);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Add a commit to the work session
|
||||
addCommit: async (sessionId: string, commitSha: string): Promise<WorkSession> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
|
||||
if (idx === -1) throw new Error("Work session not found");
|
||||
const now = new Date().toISOString();
|
||||
mockWorkSessions[idx] = {
|
||||
...mockWorkSessions[idx],
|
||||
commits: [...mockWorkSessions[idx].commits, commitSha],
|
||||
updated_at: now,
|
||||
};
|
||||
return mockWorkSessions[idx];
|
||||
}
|
||||
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/commits", {
|
||||
commit_sha: commitSha,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// Add modified files to the work session
|
||||
addFiles: async (sessionId: string, filePaths: string[]): Promise<WorkSession> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
|
||||
if (idx === -1) throw new Error("Work session not found");
|
||||
const now = new Date().toISOString();
|
||||
const existingFiles = new Set(mockWorkSessions[idx].files_modified);
|
||||
filePaths.forEach((f) => existingFiles.add(f));
|
||||
mockWorkSessions[idx] = {
|
||||
...mockWorkSessions[idx],
|
||||
files_modified: Array.from(existingFiles),
|
||||
updated_at: now,
|
||||
};
|
||||
return mockWorkSessions[idx];
|
||||
}
|
||||
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/files", {
|
||||
file_paths: filePaths,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// Record PR creation
|
||||
createPR: async (sessionId: string, prNumber: number, prUrl: string): Promise<WorkSession> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
|
||||
if (idx === -1) throw new Error("Work session not found");
|
||||
const now = new Date().toISOString();
|
||||
mockWorkSessions[idx] = {
|
||||
...mockWorkSessions[idx],
|
||||
pr_number: prNumber,
|
||||
pr_url: prUrl,
|
||||
pr_status: "open",
|
||||
pr_created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
return mockWorkSessions[idx];
|
||||
}
|
||||
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/pr", {
|
||||
pr_number: prNumber,
|
||||
pr_url: prUrl,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// Update PR status
|
||||
updatePRStatus: async (sessionId: string, prStatus: string): Promise<WorkSession> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
|
||||
if (idx === -1) throw new Error("Work session not found");
|
||||
const now = new Date().toISOString();
|
||||
mockWorkSessions[idx] = {
|
||||
...mockWorkSessions[idx],
|
||||
pr_status: prStatus,
|
||||
updated_at: now,
|
||||
};
|
||||
return mockWorkSessions[idx];
|
||||
}
|
||||
const { data } = await api.patch<WorkSession>("/work-sessions/" + sessionId + "/pr", {
|
||||
pr_status: prStatus,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// Record PR merge (PM only)
|
||||
mergePR: async (sessionId: string, mergedBy: string): Promise<WorkSession> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
|
||||
if (idx === -1) throw new Error("Work session not found");
|
||||
const now = new Date().toISOString();
|
||||
mockWorkSessions[idx] = {
|
||||
...mockWorkSessions[idx],
|
||||
pr_status: "merged",
|
||||
pr_merged_at: now,
|
||||
merged_by: mergedBy,
|
||||
status: "completed" as WorkSessionStatus,
|
||||
ended_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
return mockWorkSessions[idx];
|
||||
}
|
||||
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/pr/merge", {
|
||||
merged_by: mergedBy,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// Complete the session
|
||||
complete: async (sessionId: string): Promise<WorkSession> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
|
||||
if (idx === -1) throw new Error("Work session not found");
|
||||
const now = new Date().toISOString();
|
||||
mockWorkSessions[idx] = {
|
||||
...mockWorkSessions[idx],
|
||||
status: "completed" as WorkSessionStatus,
|
||||
ended_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
return mockWorkSessions[idx];
|
||||
}
|
||||
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/complete");
|
||||
return data;
|
||||
},
|
||||
|
||||
// Abandon the session
|
||||
abandon: async (sessionId: string, reason?: string): Promise<WorkSession> => {
|
||||
if (isMockMode()) {
|
||||
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
|
||||
if (idx === -1) throw new Error("Work session not found");
|
||||
const now = new Date().toISOString();
|
||||
mockWorkSessions[idx] = {
|
||||
...mockWorkSessions[idx],
|
||||
status: "abandoned" as WorkSessionStatus,
|
||||
ended_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
return mockWorkSessions[idx];
|
||||
}
|
||||
const params = reason ? `?reason=${encodeURIComponent(reason)}` : "";
|
||||
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/abandon" + params);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user