react structure
This commit is contained in:
@@ -46,6 +46,7 @@ coverage/
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
vite.config.d.ts
|
||||
|
||||
# Python
|
||||
__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
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||
@@ -26,3 +26,9 @@ test: ## Run all tests
|
||||
install-hooks: ## Install pre-commit hooks
|
||||
uv pip install pre-commit
|
||||
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:
|
||||
"""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 = [
|
||||
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
|
||||
|
||||
@@ -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",
|
||||
"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": {
|
||||
"@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