Global search overlay (Cmd+K), copy button on messages, fix Turbopack ESM
This commit is contained in:
@@ -3,6 +3,7 @@ import "./globals.css";
|
|||||||
import { BottomNav } from "@/components/BottomNav";
|
import { BottomNav } from "@/components/BottomNav";
|
||||||
import { Providers } from "@/components/Providers";
|
import { Providers } from "@/components/Providers";
|
||||||
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
||||||
|
import { SearchOverlay } from "@/components/SearchOverlay";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Hermes OS",
|
title: "Hermes OS",
|
||||||
@@ -31,6 +32,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
<ErrorBoundary>{children}</ErrorBoundary>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
</main>
|
</main>
|
||||||
<BottomNav />
|
<BottomNav />
|
||||||
|
<SearchOverlay />
|
||||||
</Providers>
|
</Providers>
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Wrench, ChevronDown, ChevronRight, Lightbulb } from "lucide-react";
|
import { Wrench, ChevronDown, ChevronRight, Lightbulb, Copy, Check } from "lucide-react";
|
||||||
import type { Message, ToolCall } from "@/lib/types";
|
import type { Message, ToolCall } from "@/lib/types";
|
||||||
|
|
||||||
type Verbosity = "compact" | "normal" | "full";
|
type Verbosity = "compact" | "normal" | "full";
|
||||||
@@ -85,10 +85,18 @@ export function MessageBubble({ message, toolResults, verbosity = "normal", show
|
|||||||
|
|
||||||
if (!content && toolCalls.length === 0 && !reasoning) return null;
|
if (!content && toolCalls.length === 0 && !reasoning) return null;
|
||||||
|
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
const showTools = verbosity !== "compact" && toolCalls.length > 0;
|
const showTools = verbosity !== "compact" && toolCalls.length > 0;
|
||||||
const showReasoning = verbosity === "full" && reasoning;
|
const showReasoning = verbosity === "full" && reasoning;
|
||||||
const expandTools = verbosity === "full";
|
const expandTools = verbosity === "full";
|
||||||
|
|
||||||
|
const copyContent = () => {
|
||||||
|
navigator.clipboard.writeText(content).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
|
<div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
|
||||||
<div
|
<div
|
||||||
@@ -134,6 +142,17 @@ export function MessageBubble({ message, toolResults, verbosity = "normal", show
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Copy button - long press or tap area */}
|
||||||
|
{content && !isUser && (
|
||||||
|
<button
|
||||||
|
onClick={copyContent}
|
||||||
|
className="mt-1 p-0.5 rounded active:bg-white/[0.04]"
|
||||||
|
style={{ color: "var(--text-3)" }}
|
||||||
|
>
|
||||||
|
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Timestamp */}
|
{/* Timestamp */}
|
||||||
{showTimestamp && message.timestamp && (
|
{showTimestamp && message.timestamp && (
|
||||||
<div className="text-[10px] mt-1" style={{ color: isUser ? "rgba(255,255,255,0.5)" : "var(--text-3)" }}>
|
<div className="text-[10px] mt-1" style={{ color: isUser ? "rgba(255,255,255,0.5)" : "var(--text-3)" }}>
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { Search, X, Clock, MessageSquare } from "lucide-react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useSessionSearch } from "@/lib/hooks";
|
||||||
|
|
||||||
|
export function SearchOverlay() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
const { data, isFetching } = useSessionSearch(open ? query : "");
|
||||||
|
|
||||||
|
// Cmd+K / Ctrl+K to open
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setOpen(false);
|
||||||
|
setQuery("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handler);
|
||||||
|
return () => window.removeEventListener("keydown", handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) inputRef.current?.focus();
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const go = (sessionId: string) => {
|
||||||
|
setOpen(false);
|
||||||
|
setQuery("");
|
||||||
|
router.push("/history/" + sessionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[200] flex flex-col"
|
||||||
|
style={{ background: "rgba(0,0,0,0.7)", backdropFilter: "blur(4px)" }}
|
||||||
|
onClick={() => { setOpen(false); setQuery(""); }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mx-4 mt-16 max-w-lg w-full self-center"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: "var(--text-3)" }} />
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Search all sessions..."
|
||||||
|
className="w-full pl-9 pr-9 py-3 rounded-t-2xl text-[15px] outline-none"
|
||||||
|
style={{ background: "var(--bg-raised)", border: "1px solid var(--border)", color: "var(--text-1)", borderBottom: "none" }}
|
||||||
|
/>
|
||||||
|
{query && (
|
||||||
|
<button onClick={() => setQuery("")} className="absolute right-3 top-1/2 -translate-y-1/2" style={{ color: "var(--text-3)" }}>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="rounded-b-2xl max-h-[60vh] overflow-y-auto"
|
||||||
|
style={{ background: "var(--bg-raised)", border: "1px solid var(--border)", borderTop: "1px solid var(--border)" }}
|
||||||
|
>
|
||||||
|
{query.length < 2 ? (
|
||||||
|
<div className="p-4 text-center text-[13px]" style={{ color: "var(--text-3)" }}>
|
||||||
|
Type to search across all sessions
|
||||||
|
</div>
|
||||||
|
) : isFetching ? (
|
||||||
|
<div className="p-4 text-center text-[13px]" style={{ color: "var(--text-3)" }}>
|
||||||
|
Searching...
|
||||||
|
</div>
|
||||||
|
) : data?.results?.length ? (
|
||||||
|
data.results.map((r) => (
|
||||||
|
<button
|
||||||
|
key={r.message_id}
|
||||||
|
onClick={() => go(r.session_id)}
|
||||||
|
className="w-full text-left px-4 py-3 border-b border-white/[0.04] active:bg-white/[0.04] transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-0.5">
|
||||||
|
<MessageSquare size={12} style={{ color: "var(--accent)" }} />
|
||||||
|
<span className="text-[12px] font-medium" style={{ color: "var(--text-2)" }}>
|
||||||
|
{r.session_title || r.session_id.slice(0, 12)}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] ml-auto" style={{ color: "var(--text-3)" }}>{r.role}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[13px] line-clamp-2" style={{ color: "var(--text-1)" }}>{r.content}</p>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="p-4 text-center text-[13px]" style={{ color: "var(--text-3)" }}>
|
||||||
|
No results for “{query}”
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import type { NextConfig } from "next";
|
|||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
transpilePackages: ["react-markdown", "remark-gfm"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Reference in New Issue
Block a user