MVP: Pulse, Memory, History modules
- Mobile-first PWA with dark glassmorphic theme - Pulse: status card, recent sessions, memory snapshot - Memory: view/edit agent + user memory with usage bars - History: session list, platform filter, search, session replay - TanStack Query hooks for all Hermes WebAPI endpoints - Bottom tab navigation, safe-area support
This commit is contained in:
+88
@@ -0,0 +1,88 @@
|
||||
const BASE = process.env.NEXT_PUBLIC_HERMES_API || "http://localhost:8787";
|
||||
|
||||
async function get<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`);
|
||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function post<T>(path: string, body?: unknown): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function patch<T>(path: string, body: unknown): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function del<T>(path: string, body?: unknown): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
import type {
|
||||
Session, Message, MemoryResponse, SkillSummary, SkillDetail,
|
||||
CronJob, ConfigResponse, SearchResult,
|
||||
} from "./types";
|
||||
|
||||
// Sessions
|
||||
export const fetchSessions = (limit = 50, offset = 0, source?: string) =>
|
||||
get<{ items: Session[]; total: number }>(
|
||||
`/api/sessions?limit=${limit}&offset=${offset}${source ? `&source=${source}` : ""}`
|
||||
);
|
||||
|
||||
export const fetchSession = (id: string) =>
|
||||
get<{ session: Session }>(`/api/sessions/${id}`);
|
||||
|
||||
export const fetchMessages = (sessionId: string, limit = 200, offset = 0) =>
|
||||
get<{ items: Message[]; total: number }>(
|
||||
`/api/sessions/${sessionId}/messages?limit=${limit}&offset=${offset}`
|
||||
);
|
||||
|
||||
export const searchSessions = (q: string, limit = 20) =>
|
||||
get<{ query: string; count: number; results: SearchResult[] }>(
|
||||
`/api/sessions/search?q=${encodeURIComponent(q)}&limit=${limit}`
|
||||
);
|
||||
|
||||
// Memory
|
||||
export const fetchMemory = () => get<MemoryResponse>("/api/memory");
|
||||
|
||||
export const addMemory = (target: string, content: string) =>
|
||||
post<{ success: boolean }>("/api/memory", { target, content });
|
||||
|
||||
export const patchMemory = (target: string, old_text: string, content: string) =>
|
||||
patch<{ success: boolean }>("/api/memory", { target, old_text, content });
|
||||
|
||||
export const deleteMemory = (target: string, old_text: string) =>
|
||||
del<{ success: boolean }>("/api/memory", { target, old_text });
|
||||
|
||||
// Skills
|
||||
export const fetchSkills = (category?: string) =>
|
||||
get<{ success: boolean; skills: SkillSummary[] }>(
|
||||
`/api/skills${category ? `?category=${category}` : ""}`
|
||||
);
|
||||
|
||||
export const fetchSkill = (name: string) =>
|
||||
get<SkillDetail>(`/api/skills/${name}`);
|
||||
|
||||
// Config
|
||||
export const fetchConfig = () => get<ConfigResponse>("/api/config");
|
||||
|
||||
// Health
|
||||
export const fetchHealth = () => get<{ status: string }>("/api/health");
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import * as api from "./api";
|
||||
|
||||
// Sessions
|
||||
export function useSessions(limit = 20, source?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["sessions", limit, source],
|
||||
queryFn: () => api.fetchSessions(limit, 0, source),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSession(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ["session", id],
|
||||
queryFn: () => api.fetchSession(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
export function useMessages(sessionId: string) {
|
||||
return useQuery({
|
||||
queryKey: ["messages", sessionId],
|
||||
queryFn: () => api.fetchMessages(sessionId),
|
||||
enabled: !!sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSessionSearch(query: string) {
|
||||
return useQuery({
|
||||
queryKey: ["search", query],
|
||||
queryFn: () => api.searchSessions(query),
|
||||
enabled: query.length >= 2,
|
||||
});
|
||||
}
|
||||
|
||||
// Memory
|
||||
export function useMemory() {
|
||||
return useQuery({
|
||||
queryKey: ["memory"],
|
||||
queryFn: api.fetchMemory,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAddMemory() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ target, content }: { target: string; content: string }) =>
|
||||
api.addMemory(target, content),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["memory"] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function usePatchMemory() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ target, old_text, content }: { target: string; old_text: string; content: string }) =>
|
||||
api.patchMemory(target, old_text, content),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["memory"] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteMemory() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ target, old_text }: { target: string; old_text: string }) =>
|
||||
api.deleteMemory(target, old_text),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["memory"] }),
|
||||
});
|
||||
}
|
||||
|
||||
// Skills
|
||||
export function useSkills(category?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["skills", category],
|
||||
queryFn: () => api.fetchSkills(category),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSkill(name: string) {
|
||||
return useQuery({
|
||||
queryKey: ["skill", name],
|
||||
queryFn: () => api.fetchSkill(name),
|
||||
enabled: !!name,
|
||||
});
|
||||
}
|
||||
|
||||
// Config
|
||||
export function useConfig() {
|
||||
return useQuery({
|
||||
queryKey: ["config"],
|
||||
queryFn: api.fetchConfig,
|
||||
});
|
||||
}
|
||||
|
||||
// Health
|
||||
export function useHealth() {
|
||||
return useQuery({
|
||||
queryKey: ["health"],
|
||||
queryFn: api.fetchHealth,
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// Hermes WebAPI response types
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
source: string;
|
||||
user_id: string | null;
|
||||
model: string | null;
|
||||
started_at: number;
|
||||
ended_at: number | null;
|
||||
end_reason: string | null;
|
||||
message_count: number;
|
||||
tool_call_count: number;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
cache_read_tokens: number;
|
||||
cache_write_tokens: number;
|
||||
reasoning_tokens: number;
|
||||
estimated_cost_usd: number | null;
|
||||
title: string | null;
|
||||
parent_session_id: string | null;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: number;
|
||||
session_id: string;
|
||||
role: "user" | "assistant" | "tool" | "system";
|
||||
content: string | null;
|
||||
tool_call_id: string | null;
|
||||
tool_calls: string | null;
|
||||
tool_name: string | null;
|
||||
timestamp: number;
|
||||
token_count: number | null;
|
||||
finish_reason: string | null;
|
||||
reasoning: string | null;
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
id: string;
|
||||
function: { name: string; arguments: string };
|
||||
}
|
||||
|
||||
export interface MemoryTarget {
|
||||
target: "memory" | "user";
|
||||
entries: string[];
|
||||
usage: string;
|
||||
entry_count: number;
|
||||
}
|
||||
|
||||
export interface MemoryResponse {
|
||||
targets: MemoryTarget[];
|
||||
}
|
||||
|
||||
export interface SkillSummary {
|
||||
name: string;
|
||||
description: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export interface SkillDetail {
|
||||
name: string;
|
||||
content: string;
|
||||
linked_files?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export interface CronJob {
|
||||
id: string;
|
||||
name?: string;
|
||||
prompt: string;
|
||||
schedule: string;
|
||||
status: string;
|
||||
deliver: string;
|
||||
skill?: string;
|
||||
skills?: string[];
|
||||
model?: string;
|
||||
provider?: string;
|
||||
repeat?: number;
|
||||
run_count: number;
|
||||
last_run?: string;
|
||||
next_run?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface GatewayState {
|
||||
pid: number;
|
||||
kind: string;
|
||||
gateway_state: string;
|
||||
platforms: Record<string, { state: string; updated_at: string }>;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ConfigResponse {
|
||||
model: string;
|
||||
provider: string | null;
|
||||
api_mode: string | null;
|
||||
base_url: string | null;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
session_id: string;
|
||||
message_id: number;
|
||||
role: string;
|
||||
content: string;
|
||||
timestamp: number;
|
||||
session_title: string | null;
|
||||
session_source: string;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
export function timeAgo(ts: number): string {
|
||||
const now = Date.now() / 1000;
|
||||
const diff = now - ts;
|
||||
if (diff < 60) return "just now";
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
||||
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
|
||||
return new Date(ts * 1000).toLocaleDateString();
|
||||
}
|
||||
|
||||
export function formatTokens(n: number): string {
|
||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
||||
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
||||
return String(n);
|
||||
}
|
||||
|
||||
export function platformColor(source: string): string {
|
||||
const map: Record<string, string> = {
|
||||
cli: "#f59e0b",
|
||||
telegram: "#38bdf8",
|
||||
discord: "#8b5cf6",
|
||||
whatsapp: "#22c55e",
|
||||
web: "#a78bfa",
|
||||
webapi: "#a78bfa",
|
||||
};
|
||||
return map[source] || "#6b7280";
|
||||
}
|
||||
|
||||
export function platformBadgeClass(source: string): string {
|
||||
const map: Record<string, string> = {
|
||||
cli: "badge-cli",
|
||||
telegram: "badge-telegram",
|
||||
discord: "badge-discord",
|
||||
whatsapp: "badge-whatsapp",
|
||||
web: "badge-web",
|
||||
webapi: "badge-web",
|
||||
};
|
||||
return map[source] || "badge-cli";
|
||||
}
|
||||
Reference in New Issue
Block a user