Quick chat FAB, active session indicators, memory tab counts, improved new session

This commit is contained in:
Hermes
2026-04-09 21:36:02 -05:00
parent 2550f4c4a7
commit d86cbea6a5
5 changed files with 51 additions and 25 deletions
+16 -4
View File
@@ -4,9 +4,11 @@ import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { ArrowLeft, Send, Loader2 } from "lucide-react"; import { ArrowLeft, Send, Loader2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useToast } from "@/components/Toast";
export default function NewSessionPage() { export default function NewSessionPage() {
const router = useRouter(); const router = useRouter();
const { toast } = useToast();
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
@@ -14,7 +16,6 @@ export default function NewSessionPage() {
if (!message.trim() || sending) return; if (!message.trim() || sending) return;
setSending(true); setSending(true);
try { try {
// Create session
const res = await fetch("/api/sessions", { const res = await fetch("/api/sessions", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -24,7 +25,6 @@ export default function NewSessionPage() {
const sessionId = data.session?.id; const sessionId = data.session?.id;
if (!sessionId) throw new Error("No session ID"); if (!sessionId) throw new Error("No session ID");
// Send first message
await fetch("/api/sessions/" + sessionId + "/chat", { await fetch("/api/sessions/" + sessionId + "/chat", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -32,7 +32,8 @@ export default function NewSessionPage() {
}); });
router.push("/history/" + sessionId); router.push("/history/" + sessionId);
} catch { } catch (e) {
toast("Failed to create session", "error");
setSending(false); setSending(false);
} }
}; };
@@ -46,11 +47,15 @@ export default function NewSessionPage() {
<h1 className="text-[15px] font-medium" style={{ color: "var(--text-1)" }}>New Session</h1> <h1 className="text-[15px] font-medium" style={{ color: "var(--text-1)" }}>New Session</h1>
</div> </div>
<p className="text-[12px]" style={{ color: "var(--text-3)" }}>
Creates a new web session and sends your first message.
</p>
<textarea <textarea
value={message} value={message}
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
placeholder="What do you need?" placeholder="What do you need?"
className="w-full min-h-[120px] p-4 rounded-2xl text-[14px] resize-none outline-none transition-colors" className="w-full min-h-[140px] p-4 rounded-2xl text-[14px] resize-none outline-none transition-colors leading-relaxed"
style={{ style={{
background: "var(--bg-raised)", background: "var(--bg-raised)",
border: "1px solid var(--border)", border: "1px solid var(--border)",
@@ -58,6 +63,7 @@ export default function NewSessionPage() {
}} }}
autoFocus autoFocus
disabled={sending} disabled={sending}
onKeyDown={(e) => { if (e.key === "Enter" && e.metaKey) create(); }}
/> />
<button <button
@@ -71,6 +77,12 @@ export default function NewSessionPage() {
<><Send size={16} /> Send</> <><Send size={16} /> Send</>
)} )}
</button> </button>
{sending && (
<p className="text-[12px] text-center" style={{ color: "var(--text-3)" }}>
This may take a moment Hermes is processing your message.
</p>
)}
</div> </div>
); );
} }
+21 -17
View File
@@ -6,10 +6,7 @@ import { useMemory, useAddMemory, usePatchMemory, useDeleteMemory } from "@/lib/
import { MemoryEntry } from "@/components/MemoryEntry"; import { MemoryEntry } from "@/components/MemoryEntry";
import { useToast } from "@/components/Toast"; import { useToast } from "@/components/Toast";
const TABS = [ const TAB_IDS = ["memory", "user"] as const;
{ id: "memory", label: "Agent" },
{ id: "user", label: "User" },
] as const;
export default function MemoryPage() { export default function MemoryPage() {
const [tab, setTab] = useState<"memory" | "user">("memory"); const [tab, setTab] = useState<"memory" | "user">("memory");
@@ -63,19 +60,26 @@ export default function MemoryPage() {
</div> </div>
<div className="flex gap-1 p-1 rounded-xl" style={{ background: "var(--bg-raised)" }}> <div className="flex gap-1 p-1 rounded-xl" style={{ background: "var(--bg-raised)" }}>
{TABS.map((t) => ( {TAB_IDS.map((id) => {
<button const t = data?.targets?.find((x) => x.target === id);
key={t.id} const label = id === "memory" ? "Agent" : "User";
onClick={() => setTab(t.id)} return (
className="flex-1 py-2 rounded-lg text-sm font-medium transition-all" <button
style={{ key={id}
background: tab === t.id ? "var(--bg-hover)" : "transparent", onClick={() => setTab(id)}
color: tab === t.id ? "var(--text-1)" : "var(--text-3)", className="flex-1 py-2 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-1.5"
}} style={{
> background: tab === id ? "var(--bg-hover)" : "transparent",
{t.label} color: tab === id ? "var(--text-1)" : "var(--text-3)",
</button> }}
))} >
{label}
{t && (
<span className="text-[10px] opacity-50">{t.entry_count}</span>
)}
</button>
);
})}
</div> </div>
{usage && ( {usage && (
+10 -1
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { Puzzle, Timer, Sparkles, Settings } from "lucide-react"; import { Puzzle, Timer, Sparkles, Settings, MessageCircle } from "lucide-react";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { StatusCard } from "@/components/StatusCard"; import { StatusCard } from "@/components/StatusCard";
import { StatsCard } from "@/components/StatsCard"; import { StatsCard } from "@/components/StatsCard";
@@ -66,6 +66,15 @@ export default function PulsePage() {
</div> </div>
)} )}
</div> </div>
{/* Quick chat FAB */}
<Link
href="/history/new"
className="fixed right-5 z-40 flex items-center justify-center w-14 h-14 rounded-full shadow-lg active:scale-90 transition-transform"
style={{ background: "var(--accent)", bottom: "calc(5.5rem + var(--safe-bottom))" }}
>
<MessageCircle size={22} color="white" />
</Link>
</div> </div>
); );
} }
+2 -2
View File
@@ -37,8 +37,8 @@ export function SessionCard({ session }: { session: Session }) {
${cost.toFixed(2)} ${cost.toFixed(2)}
</span> </span>
)} )}
<span className="text-[11px] ml-auto" style={{ color: "var(--text-3)" }}> <span className="text-[11px] ml-auto" style={{ color: !session.ended_at ? "var(--accent)" : "var(--text-3)" }}>
{timeAgo(session.started_at)} {timeAgo(session.started_at, !session.ended_at)}
</span> </span>
</div> </div>
</div> </div>
+2 -1
View File
@@ -1,4 +1,5 @@
export function timeAgo(ts: number): string { export function timeAgo(ts: number, active?: boolean): string {
if (active) return "now";
const now = Date.now() / 1000; const now = Date.now() / 1000;
const diff = now - ts; const diff = now - ts;
if (diff < 60) return "just now"; if (diff < 60) return "just now";