react structure
This commit is contained in:
@@ -46,6 +46,7 @@ coverage/
|
|||||||
|
|
||||||
# TypeScript
|
# TypeScript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
vite.config.d.ts
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: lint format check test install-hooks help
|
.PHONY: lint format check test install-hooks help frontend-dev frontend-build
|
||||||
|
|
||||||
help: ## Show this help
|
help: ## Show this help
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||||
@@ -26,3 +26,9 @@ test: ## Run all tests
|
|||||||
install-hooks: ## Install pre-commit hooks
|
install-hooks: ## Install pre-commit hooks
|
||||||
uv pip install pre-commit
|
uv pip install pre-commit
|
||||||
pre-commit install
|
pre-commit install
|
||||||
|
|
||||||
|
frontend-dev: ## Start frontend dev server
|
||||||
|
cd core/frontend && npm run dev
|
||||||
|
|
||||||
|
frontend-build: ## Build frontend for production
|
||||||
|
cd core/frontend && npm run build
|
||||||
|
|||||||
@@ -135,10 +135,12 @@ def create_app(model: str | None = None) -> web.Application:
|
|||||||
|
|
||||||
def _setup_static_serving(app: web.Application) -> None:
|
def _setup_static_serving(app: web.Application) -> None:
|
||||||
"""Serve frontend static files if the dist directory exists."""
|
"""Serve frontend static files if the dist directory exists."""
|
||||||
# Try relative to CWD (repo root) and relative to this file
|
# Try: CWD/frontend/dist, core/frontend/dist, repo_root/frontend/dist
|
||||||
|
_here = Path(__file__).resolve().parent # core/framework/server/
|
||||||
candidates = [
|
candidates = [
|
||||||
Path("frontend/dist"),
|
Path("frontend/dist"),
|
||||||
Path(__file__).resolve().parent.parent.parent.parent / "frontend" / "dist",
|
_here.parent.parent / "frontend" / "dist", # core/frontend/dist
|
||||||
|
_here.parent.parent.parent / "frontend" / "dist", # repo_root/frontend/dist
|
||||||
]
|
]
|
||||||
|
|
||||||
dist_dir: Path | None = None
|
dist_dir: Path | None = None
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Hive</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+2550
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "hive-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^7.1.0",
|
||||||
|
"tailwind-merge": "^3.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@types/node": "^25.3.0",
|
||||||
|
"@types/react": "^18.3.18",
|
||||||
|
"@types/react-dom": "^18.3.5",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^6.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
import Index from "./pages/index";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Index />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { api } from "./client";
|
||||||
|
import type {
|
||||||
|
Agent,
|
||||||
|
AgentDetail,
|
||||||
|
DiscoverResult,
|
||||||
|
EntryPoint,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const agentsApi = {
|
||||||
|
discover: () => api.get<DiscoverResult>("/discover"),
|
||||||
|
|
||||||
|
list: () => api.get<{ agents: Agent[] }>("/agents"),
|
||||||
|
|
||||||
|
load: (agentPath: string, agentId?: string, model?: string) =>
|
||||||
|
api.post<Agent>("/agents", {
|
||||||
|
agent_path: agentPath,
|
||||||
|
agent_id: agentId,
|
||||||
|
model,
|
||||||
|
}),
|
||||||
|
|
||||||
|
get: (agentId: string) => api.get<AgentDetail>(`/agents/${agentId}`),
|
||||||
|
|
||||||
|
unload: (agentId: string) =>
|
||||||
|
api.delete<{ unloaded: string }>(`/agents/${agentId}`),
|
||||||
|
|
||||||
|
stats: (agentId: string) =>
|
||||||
|
api.get<Record<string, unknown>>(`/agents/${agentId}/stats`),
|
||||||
|
|
||||||
|
entryPoints: (agentId: string) =>
|
||||||
|
api.get<{ entry_points: EntryPoint[] }>(`/agents/${agentId}/entry-points`),
|
||||||
|
|
||||||
|
graphs: (agentId: string) =>
|
||||||
|
api.get<{ graphs: string[] }>(`/agents/${agentId}/graphs`),
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const API_BASE = "/api";
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
constructor(
|
||||||
|
public status: number,
|
||||||
|
public body: { error: string; type?: string },
|
||||||
|
) {
|
||||||
|
super(body.error);
|
||||||
|
this.name = "ApiError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||||
|
const url = `${API_BASE}${path}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response
|
||||||
|
.json()
|
||||||
|
.catch(() => ({ error: response.statusText }));
|
||||||
|
throw new ApiError(response.status, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
get: <T>(path: string) => request<T>(path),
|
||||||
|
post: <T>(path: string, body?: unknown) =>
|
||||||
|
request<T>(path, {
|
||||||
|
method: "POST",
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
}),
|
||||||
|
delete: <T>(path: string) => request<T>(path, { method: "DELETE" }),
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { api } from "./client";
|
||||||
|
import type {
|
||||||
|
TriggerResult,
|
||||||
|
InjectResult,
|
||||||
|
ChatResult,
|
||||||
|
StopResult,
|
||||||
|
ResumeResult,
|
||||||
|
ReplayResult,
|
||||||
|
GoalProgress,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const executionApi = {
|
||||||
|
trigger: (
|
||||||
|
agentId: string,
|
||||||
|
entryPointId: string,
|
||||||
|
inputData: Record<string, unknown>,
|
||||||
|
sessionState?: Record<string, unknown>,
|
||||||
|
) =>
|
||||||
|
api.post<TriggerResult>(`/agents/${agentId}/trigger`, {
|
||||||
|
entry_point_id: entryPointId,
|
||||||
|
input_data: inputData,
|
||||||
|
session_state: sessionState,
|
||||||
|
}),
|
||||||
|
|
||||||
|
inject: (
|
||||||
|
agentId: string,
|
||||||
|
nodeId: string,
|
||||||
|
content: string,
|
||||||
|
graphId?: string,
|
||||||
|
) =>
|
||||||
|
api.post<InjectResult>(`/agents/${agentId}/inject`, {
|
||||||
|
node_id: nodeId,
|
||||||
|
content,
|
||||||
|
graph_id: graphId,
|
||||||
|
}),
|
||||||
|
|
||||||
|
chat: (agentId: string, message: string) =>
|
||||||
|
api.post<ChatResult>(`/agents/${agentId}/chat`, { message }),
|
||||||
|
|
||||||
|
stop: (agentId: string, executionId: string) =>
|
||||||
|
api.post<StopResult>(`/agents/${agentId}/stop`, {
|
||||||
|
execution_id: executionId,
|
||||||
|
}),
|
||||||
|
|
||||||
|
pause: (agentId: string, executionId: string) =>
|
||||||
|
api.post<StopResult>(`/agents/${agentId}/pause`, {
|
||||||
|
execution_id: executionId,
|
||||||
|
}),
|
||||||
|
|
||||||
|
resume: (agentId: string, sessionId: string, checkpointId?: string) =>
|
||||||
|
api.post<ResumeResult>(`/agents/${agentId}/resume`, {
|
||||||
|
session_id: sessionId,
|
||||||
|
checkpoint_id: checkpointId,
|
||||||
|
}),
|
||||||
|
|
||||||
|
replay: (agentId: string, sessionId: string, checkpointId: string) =>
|
||||||
|
api.post<ReplayResult>(`/agents/${agentId}/replay`, {
|
||||||
|
session_id: sessionId,
|
||||||
|
checkpoint_id: checkpointId,
|
||||||
|
}),
|
||||||
|
|
||||||
|
goalProgress: (agentId: string) =>
|
||||||
|
api.get<GoalProgress>(`/agents/${agentId}/goal-progress`),
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { api } from "./client";
|
||||||
|
import type { NodeSpec, NodeDetail, NodeCriteria } from "./types";
|
||||||
|
|
||||||
|
export const graphsApi = {
|
||||||
|
nodes: (agentId: string, graphId: string, sessionId?: string) =>
|
||||||
|
api.get<{ nodes: NodeSpec[] }>(
|
||||||
|
`/agents/${agentId}/graphs/${graphId}/nodes${sessionId ? `?session_id=${sessionId}` : ""}`,
|
||||||
|
),
|
||||||
|
|
||||||
|
node: (agentId: string, graphId: string, nodeId: string) =>
|
||||||
|
api.get<NodeDetail>(
|
||||||
|
`/agents/${agentId}/graphs/${graphId}/nodes/${nodeId}`,
|
||||||
|
),
|
||||||
|
|
||||||
|
nodeCriteria: (
|
||||||
|
agentId: string,
|
||||||
|
graphId: string,
|
||||||
|
nodeId: string,
|
||||||
|
sessionId?: string,
|
||||||
|
) =>
|
||||||
|
api.get<NodeCriteria>(
|
||||||
|
`/agents/${agentId}/graphs/${graphId}/nodes/${nodeId}/criteria${sessionId ? `?session_id=${sessionId}` : ""}`,
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { api } from "./client";
|
||||||
|
import type { LogEntry, LogNodeDetail, LogToolStep } from "./types";
|
||||||
|
|
||||||
|
export const logsApi = {
|
||||||
|
list: (agentId: string, limit?: number) =>
|
||||||
|
api.get<{ logs: LogEntry[] }>(
|
||||||
|
`/agents/${agentId}/logs${limit ? `?limit=${limit}` : ""}`,
|
||||||
|
),
|
||||||
|
|
||||||
|
summary: (agentId: string, sessionId: string) =>
|
||||||
|
api.get<LogEntry>(
|
||||||
|
`/agents/${agentId}/logs?session_id=${sessionId}&level=summary`,
|
||||||
|
),
|
||||||
|
|
||||||
|
details: (agentId: string, sessionId: string) =>
|
||||||
|
api.get<{ session_id: string; nodes: LogNodeDetail[] }>(
|
||||||
|
`/agents/${agentId}/logs?session_id=${sessionId}&level=details`,
|
||||||
|
),
|
||||||
|
|
||||||
|
tools: (agentId: string, sessionId: string) =>
|
||||||
|
api.get<{ session_id: string; steps: LogToolStep[] }>(
|
||||||
|
`/agents/${agentId}/logs?session_id=${sessionId}&level=tools`,
|
||||||
|
),
|
||||||
|
|
||||||
|
nodeLogs: (
|
||||||
|
agentId: string,
|
||||||
|
graphId: string,
|
||||||
|
nodeId: string,
|
||||||
|
sessionId: string,
|
||||||
|
level?: string,
|
||||||
|
) =>
|
||||||
|
api.get<{
|
||||||
|
session_id: string;
|
||||||
|
node_id: string;
|
||||||
|
details?: LogNodeDetail[];
|
||||||
|
tool_logs?: LogToolStep[];
|
||||||
|
}>(
|
||||||
|
`/agents/${agentId}/graphs/${graphId}/nodes/${nodeId}/logs?session_id=${sessionId}${level ? `&level=${level}` : ""}`,
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { api } from "./client";
|
||||||
|
import type {
|
||||||
|
SessionSummary,
|
||||||
|
SessionDetail,
|
||||||
|
Checkpoint,
|
||||||
|
Message,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const sessionsApi = {
|
||||||
|
list: (agentId: string) =>
|
||||||
|
api.get<{ sessions: SessionSummary[] }>(`/agents/${agentId}/sessions`),
|
||||||
|
|
||||||
|
get: (agentId: string, sessionId: string) =>
|
||||||
|
api.get<SessionDetail>(`/agents/${agentId}/sessions/${sessionId}`),
|
||||||
|
|
||||||
|
delete: (agentId: string, sessionId: string) =>
|
||||||
|
api.delete<{ deleted: string }>(`/agents/${agentId}/sessions/${sessionId}`),
|
||||||
|
|
||||||
|
checkpoints: (agentId: string, sessionId: string) =>
|
||||||
|
api.get<{ checkpoints: Checkpoint[] }>(
|
||||||
|
`/agents/${agentId}/sessions/${sessionId}/checkpoints`,
|
||||||
|
),
|
||||||
|
|
||||||
|
restore: (agentId: string, sessionId: string, checkpointId: string) =>
|
||||||
|
api.post<{ execution_id: string }>(
|
||||||
|
`/agents/${agentId}/sessions/${sessionId}/checkpoints/${checkpointId}/restore`,
|
||||||
|
),
|
||||||
|
|
||||||
|
messages: (agentId: string, sessionId: string, nodeId?: string) =>
|
||||||
|
api.get<{ messages: Message[] }>(
|
||||||
|
`/agents/${agentId}/sessions/${sessionId}/messages${nodeId ? `?node_id=${nodeId}` : ""}`,
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
// --- Agent types ---
|
||||||
|
|
||||||
|
export interface Agent {
|
||||||
|
id: string;
|
||||||
|
agent_path: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
goal: string;
|
||||||
|
node_count: number;
|
||||||
|
loaded_at: number;
|
||||||
|
uptime_seconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntryPoint {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
entry_node: string;
|
||||||
|
trigger_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentDetail extends Agent {
|
||||||
|
entry_points: EntryPoint[];
|
||||||
|
graphs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiscoverEntry {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
session_count: number;
|
||||||
|
node_count: number;
|
||||||
|
tool_count: number;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keyed by category name. */
|
||||||
|
export type DiscoverResult = Record<string, DiscoverEntry[]>;
|
||||||
|
|
||||||
|
// --- Execution types ---
|
||||||
|
|
||||||
|
export interface TriggerResult {
|
||||||
|
execution_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InjectResult {
|
||||||
|
delivered: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatResult {
|
||||||
|
status: "started" | "injected";
|
||||||
|
execution_id?: string;
|
||||||
|
node_id?: string;
|
||||||
|
delivered?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StopResult {
|
||||||
|
stopped: boolean;
|
||||||
|
execution_id?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResumeResult {
|
||||||
|
execution_id: string;
|
||||||
|
resumed_from: string;
|
||||||
|
checkpoint_id: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReplayResult {
|
||||||
|
execution_id: string;
|
||||||
|
replayed_from: string;
|
||||||
|
checkpoint_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoalProgress {
|
||||||
|
progress: number;
|
||||||
|
criteria: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Session types ---
|
||||||
|
|
||||||
|
export interface SessionSummary {
|
||||||
|
session_id: string;
|
||||||
|
status?: string;
|
||||||
|
started_at?: string | null;
|
||||||
|
completed_at?: string | null;
|
||||||
|
steps?: number;
|
||||||
|
paused_at?: string | null;
|
||||||
|
checkpoint_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionDetail {
|
||||||
|
status: string;
|
||||||
|
started_at: string;
|
||||||
|
completed_at: string | null;
|
||||||
|
input_data: Record<string, unknown>;
|
||||||
|
memory: Record<string, unknown>;
|
||||||
|
progress: {
|
||||||
|
current_node: string | null;
|
||||||
|
paused_at: string | null;
|
||||||
|
steps_executed: number;
|
||||||
|
path: string[];
|
||||||
|
node_visit_counts: Record<string, number>;
|
||||||
|
nodes_with_failures: string[];
|
||||||
|
resume_from?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Checkpoint {
|
||||||
|
checkpoint_id: string;
|
||||||
|
current_node: string | null;
|
||||||
|
next_node: string | null;
|
||||||
|
is_clean: boolean;
|
||||||
|
timestamp: string | null;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
seq: number;
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
_node_id: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Graph / Node types ---
|
||||||
|
|
||||||
|
export interface NodeSpec {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
node_type: string;
|
||||||
|
input_keys: string[];
|
||||||
|
output_keys: string[];
|
||||||
|
nullable_output_keys: string[];
|
||||||
|
tools: string[];
|
||||||
|
routes: Record<string, string>;
|
||||||
|
max_retries: number;
|
||||||
|
max_node_visits: number;
|
||||||
|
client_facing: boolean;
|
||||||
|
success_criteria: string | null;
|
||||||
|
// Runtime enrichment (when session_id provided)
|
||||||
|
visit_count?: number;
|
||||||
|
has_failures?: boolean;
|
||||||
|
is_current?: boolean;
|
||||||
|
in_path?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EdgeInfo {
|
||||||
|
target: string;
|
||||||
|
condition: string;
|
||||||
|
priority: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeDetail extends NodeSpec {
|
||||||
|
edges: EdgeInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeCriteria {
|
||||||
|
node_id: string;
|
||||||
|
success_criteria: string | null;
|
||||||
|
output_keys: string[];
|
||||||
|
last_execution?: {
|
||||||
|
success: boolean;
|
||||||
|
error: string | null;
|
||||||
|
retry_count: number;
|
||||||
|
needs_attention: boolean;
|
||||||
|
attention_reasons: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Log types ---
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogNodeDetail {
|
||||||
|
node_id: string;
|
||||||
|
node_name: string;
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
retry_count?: number;
|
||||||
|
needs_attention?: boolean;
|
||||||
|
attention_reasons?: string[];
|
||||||
|
total_steps: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogToolStep {
|
||||||
|
node_id: string;
|
||||||
|
step_index: number;
|
||||||
|
llm_text: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SSE Event types ---
|
||||||
|
|
||||||
|
export type EventTypeName =
|
||||||
|
| "execution_started"
|
||||||
|
| "execution_completed"
|
||||||
|
| "execution_failed"
|
||||||
|
| "execution_paused"
|
||||||
|
| "execution_resumed"
|
||||||
|
| "state_changed"
|
||||||
|
| "state_conflict"
|
||||||
|
| "goal_progress"
|
||||||
|
| "goal_achieved"
|
||||||
|
| "constraint_violation"
|
||||||
|
| "stream_started"
|
||||||
|
| "stream_stopped"
|
||||||
|
| "node_loop_started"
|
||||||
|
| "node_loop_iteration"
|
||||||
|
| "node_loop_completed"
|
||||||
|
| "llm_text_delta"
|
||||||
|
| "llm_reasoning_delta"
|
||||||
|
| "tool_call_started"
|
||||||
|
| "tool_call_completed"
|
||||||
|
| "client_output_delta"
|
||||||
|
| "client_input_requested"
|
||||||
|
| "node_internal_output"
|
||||||
|
| "node_input_blocked"
|
||||||
|
| "node_stalled"
|
||||||
|
| "node_tool_doom_loop"
|
||||||
|
| "judge_verdict"
|
||||||
|
| "output_key_set"
|
||||||
|
| "node_retry"
|
||||||
|
| "edge_traversed"
|
||||||
|
| "context_compacted"
|
||||||
|
| "webhook_received"
|
||||||
|
| "custom"
|
||||||
|
| "escalation_requested";
|
||||||
|
|
||||||
|
export interface AgentEvent {
|
||||||
|
type: EventTypeName;
|
||||||
|
stream_id: string;
|
||||||
|
node_id: string | null;
|
||||||
|
execution_id: string | null;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
timestamp: string;
|
||||||
|
correlation_id: string | null;
|
||||||
|
graph_id: string | null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useEffect, useRef, useCallback, useState } from "react";
|
||||||
|
import type { AgentEvent, EventTypeName } from "@/api/types";
|
||||||
|
|
||||||
|
interface UseSSEOptions {
|
||||||
|
agentId: string;
|
||||||
|
eventTypes?: EventTypeName[];
|
||||||
|
onEvent?: (event: AgentEvent) => void;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSSE({
|
||||||
|
agentId,
|
||||||
|
eventTypes,
|
||||||
|
onEvent,
|
||||||
|
enabled = true,
|
||||||
|
}: UseSSEOptions) {
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
|
const [lastEvent, setLastEvent] = useState<AgentEvent | null>(null);
|
||||||
|
const eventSourceRef = useRef<EventSource | null>(null);
|
||||||
|
const onEventRef = useRef(onEvent);
|
||||||
|
onEventRef.current = onEvent;
|
||||||
|
|
||||||
|
const typesKey = eventTypes?.join(",") ?? "";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !agentId) return;
|
||||||
|
|
||||||
|
let url = `/api/agents/${agentId}/events`;
|
||||||
|
if (eventTypes?.length) {
|
||||||
|
url += `?types=${eventTypes.join(",")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const es = new EventSource(url);
|
||||||
|
eventSourceRef.current = es;
|
||||||
|
|
||||||
|
es.onopen = () => setConnected(true);
|
||||||
|
es.onerror = () => setConnected(false);
|
||||||
|
|
||||||
|
const handler = (e: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const event: AgentEvent = JSON.parse(e.data);
|
||||||
|
setLastEvent(event);
|
||||||
|
onEventRef.current?.(event);
|
||||||
|
} catch {
|
||||||
|
// Ignore parse errors (keepalive comments)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen on generic message for all events
|
||||||
|
es.onmessage = handler;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
es.close();
|
||||||
|
eventSourceRef.current = null;
|
||||||
|
setConnected(false);
|
||||||
|
};
|
||||||
|
}, [agentId, enabled, typesKey]);
|
||||||
|
|
||||||
|
const close = useCallback(() => {
|
||||||
|
eventSourceRef.current?.close();
|
||||||
|
eventSourceRef.current = null;
|
||||||
|
setConnected(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { connected, lastEvent, close };
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import App from "./App";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default function Index() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-neutral-950 text-white">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-5xl font-bold mb-4">Hive</h1>
|
||||||
|
<p className="text-neutral-400 text-lg">Agent Dashboard</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"composite": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"declaration": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://localhost:8787",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
+4
-1
@@ -9,7 +9,10 @@
|
|||||||
},
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test:duplicates": "bun test scripts/auto-close-duplicates"
|
"test:duplicates": "bun test scripts/auto-close-duplicates",
|
||||||
|
"frontend:dev": "cd core/frontend && npm run dev",
|
||||||
|
"frontend:build": "cd core/frontend && npm run build",
|
||||||
|
"frontend:preview": "cd core/frontend && npm run preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Plan: Set Up React Frontend for Hive
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The `feat/open-hive` branch has a complete backend HTTP API (aiohttp on port 8787) with CRUD, execution control, sessions, SSE streaming, and more. The server already has SPA static-file serving built in — it looks for `frontend/dist/index.html` and serves it with a catch-all fallback. **No frontend exists yet.** The user has a Lovable.dev design they'll paste pages from later, so the scaffold must be Lovable-compatible (React 18, Vite, Tailwind, shadcn/ui, React Router).
|
||||||
|
|
||||||
|
The goal: create a deployable frontend shell with a typed API client layer, so the user can immediately start dropping in Lovable pages.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
| Decision | Choice | Why |
|
||||||
|
|----------|--------|-----|
|
||||||
|
| Location | `core/frontend/` | Keeps frontend co-located with the Python framework inside `core/`. Requires a small tweak to app.py to add `core/frontend/dist` as a lookup candidate. |
|
||||||
|
| Build tool | **Vite** | SPA output, Lovable uses Vite, CRA deprecated, Next.js is overkill for SPA |
|
||||||
|
| Package manager | **npm** | Root `package.json` declares `npm@10.2.0` — stay consistent |
|
||||||
|
| Styling | **Tailwind CSS v4 + shadcn/ui** | Lovable generates these; shadcn copies source into project |
|
||||||
|
| Routing | **React Router** | Lovable uses it; SPA client-side routing matches backend catch-all |
|
||||||
|
| Dev proxy | Vite `server.proxy` → `:8787` | Avoids CORS issues, SSE EventSource works through proxy |
|
||||||
|
|
||||||
|
## Files to Create
|
||||||
|
|
||||||
|
```
|
||||||
|
core/frontend/
|
||||||
|
├── package.json
|
||||||
|
├── vite.config.ts
|
||||||
|
├── tsconfig.json
|
||||||
|
├── tsconfig.node.json
|
||||||
|
├── index.html
|
||||||
|
├── components.json # shadcn config (via npx shadcn@latest init)
|
||||||
|
├── src/
|
||||||
|
│ ├── main.tsx # React entry point
|
||||||
|
│ ├── App.tsx # Router shell
|
||||||
|
│ ├── index.css # Tailwind imports
|
||||||
|
│ ├── vite-env.d.ts # Vite type declarations
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ └── utils.ts # shadcn cn() utility
|
||||||
|
│ ├── api/
|
||||||
|
│ │ ├── client.ts # Base fetch wrapper (/api prefix)
|
||||||
|
│ │ ├── types.ts # All TS types matching backend responses
|
||||||
|
│ │ ├── agents.ts # Agent CRUD endpoints
|
||||||
|
│ │ ├── execution.ts # Trigger, chat, inject, stop, resume
|
||||||
|
│ │ ├── sessions.ts # Sessions & checkpoints
|
||||||
|
│ │ ├── graphs.ts # Graph/node inspection
|
||||||
|
│ │ └── logs.ts # Log retrieval
|
||||||
|
│ ├── hooks/
|
||||||
|
│ │ └── use-sse.ts # SSE EventSource hook
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.tsx # Placeholder landing page
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
|
||||||
|
- `core/framework/server/app.py` — add `core/frontend/dist` as a static-file lookup candidate
|
||||||
|
- `package.json` — add `frontend:dev` and `frontend:build` convenience scripts
|
||||||
|
- `Makefile` — add `frontend-dev` and `frontend-build` targets
|
||||||
|
|
||||||
|
## Lovable Compatibility
|
||||||
|
|
||||||
|
When pasting Lovable pages later:
|
||||||
|
1. **Imports like `@/components/ui/button`** work via the `@` alias
|
||||||
|
2. **Run `npx shadcn@latest add <component>`** for each UI component a page needs
|
||||||
|
3. **Add routes** to `App.tsx` — Lovable pages export default React components
|
||||||
|
4. **If pages use `@tanstack/react-query`**, install it: `npm install @tanstack/react-query`
|
||||||
|
5. **Tailwind classes** work out of the box
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. `cd core/frontend && npm run build` succeeds and produces `core/frontend/dist/index.html`
|
||||||
|
2. Start backend: `cd core && uv run python -m framework.runner.cli serve` — logs "Serving frontend from ..."
|
||||||
|
3. Open `http://localhost:8787` — placeholder page renders
|
||||||
|
4. Dev mode: `cd core/frontend && npm run dev` on `:5173`, API calls proxy to `:8787`
|
||||||
Reference in New Issue
Block a user