diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0a43c68 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# Hermes WebAPI base URL (gateway) +NEXT_PUBLIC_HERMES_API=http://localhost:8787 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38138c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.next/ +.env.local +*.tsbuildinfo diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8bd0e39 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..13ce3d9 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,50 @@ +@import "tailwindcss"; + +:root { + --safe-bottom: env(safe-area-inset-bottom, 0px); +} + +body { + background: #0a0a0f; + color: #f0f0f0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + -webkit-font-smoothing: antialiased; + overscroll-behavior: none; +} + +/* Glass card */ +.glass { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 16px; +} + +.glass-hover:active { + background: rgba(255, 255, 255, 0.07); +} + +/* Scrollbar */ +::-webkit-scrollbar { width: 4px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; } + +/* Safe area padding for bottom nav */ +.pb-safe { + padding-bottom: calc(4.5rem + var(--safe-bottom)); +} + +/* Platform badges */ +.badge-cli { background: rgba(245,158,11,0.15); color: #f59e0b; } +.badge-telegram { background: rgba(56,189,248,0.15); color: #38bdf8; } +.badge-discord { background: rgba(139,92,246,0.15); color: #8b5cf6; } +.badge-whatsapp { background: rgba(34,197,94,0.15); color: #22c55e; } +.badge-web { background: rgba(168,85,247,0.15); color: #a78bfa; } + +/* Pulse dot animation */ +@keyframes pulse-dot { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} +.animate-pulse-dot { + animation: pulse-dot 2s ease-in-out infinite; +} diff --git a/app/history/[id]/page.tsx b/app/history/[id]/page.tsx new file mode 100644 index 0000000..267bfe7 --- /dev/null +++ b/app/history/[id]/page.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { use } from "react"; +import { ArrowLeft, MessageSquare, Wrench } from "lucide-react"; +import Link from "next/link"; +import { useSession, useMessages } from "@/lib/hooks"; +import { timeAgo, formatTokens, platformBadgeClass } from "@/lib/utils"; +import { MessageBubble } from "@/components/MessageBubble"; + +export default function SessionReplayPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params); + const { data: sessionData } = useSession(id); + const { data: messagesData, isLoading } = useMessages(id); + + const session = sessionData?.session; + const messages = messagesData?.items || []; + + // Group tool results with their preceding assistant tool_calls + const visibleMessages = messages.filter( + (m) => m.role === "user" || m.role === "assistant" + ); + + // Collect tool results keyed by tool_call_id + const toolResults = new Map(); + for (const m of messages) { + if (m.role === "tool" && m.tool_call_id) { + toolResults.set(m.tool_call_id, { + name: m.tool_name || "tool", + content: m.content || "", + }); + } + } + + return ( +
+ {/* Header */} +
+ + + +
+

+ {session?.title || `Session ${id.slice(0, 8)}`} +

+ {session && ( +
+ + {session.source} + + {session.message_count} msgs + {formatTokens(session.input_tokens + session.output_tokens)} tok + {timeAgo(session.started_at)} +
+ )} +
+
+ + {/* Messages */} + {isLoading ? ( +
+ {[...Array(4)].map((_, i) => ( +
+
+
+
+ ))} +
+ ) : ( +
+ {visibleMessages.map((m) => ( + + ))} + {visibleMessages.length === 0 && ( +
+ Empty session +
+ )} +
+ )} +
+ ); +} diff --git a/app/history/page.tsx b/app/history/page.tsx new file mode 100644 index 0000000..1781046 --- /dev/null +++ b/app/history/page.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useState } from "react"; +import { Clock, Search, X } from "lucide-react"; +import { useSessions, useSessionSearch } from "@/lib/hooks"; +import { SessionCard } from "@/components/SessionCard"; + +const PLATFORMS = [ + { id: null, label: "All" }, + { id: "cli", label: "CLI" }, + { id: "telegram", label: "Telegram" }, + { id: "discord", label: "Discord" }, + { id: "web", label: "Web" }, +] as const; + +export default function HistoryPage() { + const [filter, setFilter] = useState(null); + const [query, setQuery] = useState(""); + const [searching, setSearching] = useState(false); + + const { data: sessionsData, isLoading } = useSessions(30, filter || undefined); + const { data: searchData, isFetching: searchLoading } = useSessionSearch( + searching ? query : "" + ); + + const showSearch = searching && query.length >= 2; + + return ( +
+
+
+ +

History

+
+
+ + {/* Search bar */} +
+ + { setQuery(e.target.value); setSearching(true); }} + onFocus={() => setSearching(true)} + placeholder="Search sessions..." + className="w-full pl-9 pr-9 py-2.5 rounded-xl text-sm bg-white/[0.04] border border-white/[0.08] text-zinc-200 placeholder-zinc-600 outline-none focus:border-white/[0.15] transition-colors" + /> + {query && ( + + )} +
+ + {/* Search results */} + {showSearch ? ( +
+
+ {searchLoading ? "Searching..." : `${searchData?.count || 0} results`} +
+ +
+ ) : ( + <> + {/* Platform filter */} +
+ {PLATFORMS.map((p) => ( + + ))} +
+ + {/* Session list */} + {isLoading ? ( +
+ {[...Array(5)].map((_, i) => ( +
+
+
+
+ ))} +
+ ) : ( +
+ {sessionsData?.items?.map((s) => ( + + ))} + {sessionsData?.items?.length === 0 && ( +
+ No sessions found +
+ )} +
+ )} + + )} +
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..c2f9b07 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,42 @@ +import type { Metadata, Viewport } from "next"; +import "./globals.css"; +import { BottomNav } from "@/components/BottomNav"; +import { Providers } from "@/components/Providers"; + +export const metadata: Metadata = { + title: "Hermes OS", + description: "Mobile dashboard for Hermes Agent", + icons: { icon: "/icon-192.png", apple: "/icon-192.png" }, + appleWebApp: { capable: true, statusBarStyle: "black-translucent", title: "Hermes" }, +}; + +export const viewport: Viewport = { + themeColor: "#0a0a0f", + width: "device-width", + initialScale: 1, + maximumScale: 1, + viewportFit: "cover", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + + +
+ {children} +
+ +
+