mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-14 04:02:26 +00:00
fix: imports
This commit is contained in:
parent
60fd4b7d16
commit
823b6d08ed
11 changed files with 2127 additions and 146 deletions
|
|
@ -13,9 +13,13 @@ import {
|
|||
Hash,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { H2 } from "@nous-research/ui/ui/components/typography/h2";
|
||||
import { H2 } from "@nous-research/ui";
|
||||
import { api } from "@/lib/api";
|
||||
import type { SessionInfo, SessionMessage, SessionSearchResult } from "@/lib/api";
|
||||
import type {
|
||||
SessionInfo,
|
||||
SessionMessage,
|
||||
SessionSearchResult,
|
||||
} from "@/lib/api";
|
||||
import { timeAgo } from "@/lib/utils";
|
||||
import { Markdown } from "@/components/Markdown";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
|
@ -23,14 +27,15 @@ import { Button } from "@/components/ui/button";
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { useI18n } from "@/i18n";
|
||||
|
||||
const SOURCE_CONFIG: Record<string, { icon: typeof Terminal; color: string }> = {
|
||||
cli: { icon: Terminal, color: "text-primary" },
|
||||
telegram: { icon: MessageCircle, color: "text-[oklch(0.65_0.15_250)]" },
|
||||
discord: { icon: Hash, color: "text-[oklch(0.65_0.15_280)]" },
|
||||
slack: { icon: MessageSquare, color: "text-[oklch(0.7_0.15_155)]" },
|
||||
whatsapp: { icon: Globe, color: "text-success" },
|
||||
cron: { icon: Clock, color: "text-warning" },
|
||||
};
|
||||
const SOURCE_CONFIG: Record<string, { icon: typeof Terminal; color: string }> =
|
||||
{
|
||||
cli: { icon: Terminal, color: "text-primary" },
|
||||
telegram: { icon: MessageCircle, color: "text-[oklch(0.65_0.15_250)]" },
|
||||
discord: { icon: Hash, color: "text-[oklch(0.65_0.15_280)]" },
|
||||
slack: { icon: MessageSquare, color: "text-[oklch(0.7_0.15_155)]" },
|
||||
whatsapp: { icon: Globe, color: "text-success" },
|
||||
cron: { icon: Clock, color: "text-warning" },
|
||||
};
|
||||
|
||||
/** Render an FTS5 snippet with highlighted matches.
|
||||
* The backend wraps matches in >>> and <<< delimiters. */
|
||||
|
|
@ -47,7 +52,7 @@ function SnippetHighlight({ snippet }: { snippet: string }) {
|
|||
parts.push(
|
||||
<mark key={i++} className="bg-warning/30 text-warning px-0.5">
|
||||
{match[1]}
|
||||
</mark>
|
||||
</mark>,
|
||||
);
|
||||
last = regex.lastIndex;
|
||||
}
|
||||
|
|
@ -61,7 +66,11 @@ function SnippetHighlight({ snippet }: { snippet: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ToolCallBlock({ toolCall }: { toolCall: { id: string; function: { name: string; arguments: string } } }) {
|
||||
function ToolCallBlock({
|
||||
toolCall,
|
||||
}: {
|
||||
toolCall: { id: string; function: { name: string; arguments: string } };
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { t } = useI18n();
|
||||
|
||||
|
|
@ -80,8 +89,14 @@ function ToolCallBlock({ toolCall }: { toolCall: { id: string; function: { name:
|
|||
onClick={() => setOpen(!open)}
|
||||
aria-label={`${open ? t.common.collapse : t.common.expand} tool call ${toolCall.function.name}`}
|
||||
>
|
||||
{open ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
||||
<span className="font-mono-ui font-medium">{toolCall.function.name}</span>
|
||||
{open ? (
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
)}
|
||||
<span className="font-mono-ui font-medium">
|
||||
{toolCall.function.name}
|
||||
</span>
|
||||
<span className="text-warning/50 ml-auto">{toolCall.id}</span>
|
||||
</button>
|
||||
{open && (
|
||||
|
|
@ -93,18 +108,45 @@ function ToolCallBlock({ toolCall }: { toolCall: { id: string; function: { name:
|
|||
);
|
||||
}
|
||||
|
||||
function MessageBubble({ msg, highlight }: { msg: SessionMessage; highlight?: string }) {
|
||||
function MessageBubble({
|
||||
msg,
|
||||
highlight,
|
||||
}: {
|
||||
msg: SessionMessage;
|
||||
highlight?: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const ROLE_STYLES: Record<string, { bg: string; text: string; label: string }> = {
|
||||
user: { bg: "bg-primary/10", text: "text-primary", label: t.sessions.roles.user },
|
||||
assistant: { bg: "bg-success/10", text: "text-success", label: t.sessions.roles.assistant },
|
||||
system: { bg: "bg-muted", text: "text-muted-foreground", label: t.sessions.roles.system },
|
||||
tool: { bg: "bg-warning/10", text: "text-warning", label: t.sessions.roles.tool },
|
||||
const ROLE_STYLES: Record<
|
||||
string,
|
||||
{ bg: string; text: string; label: string }
|
||||
> = {
|
||||
user: {
|
||||
bg: "bg-primary/10",
|
||||
text: "text-primary",
|
||||
label: t.sessions.roles.user,
|
||||
},
|
||||
assistant: {
|
||||
bg: "bg-success/10",
|
||||
text: "text-success",
|
||||
label: t.sessions.roles.assistant,
|
||||
},
|
||||
system: {
|
||||
bg: "bg-muted",
|
||||
text: "text-muted-foreground",
|
||||
label: t.sessions.roles.system,
|
||||
},
|
||||
tool: {
|
||||
bg: "bg-warning/10",
|
||||
text: "text-warning",
|
||||
label: t.sessions.roles.tool,
|
||||
},
|
||||
};
|
||||
|
||||
const style = ROLE_STYLES[msg.role] ?? ROLE_STYLES.system;
|
||||
const label = msg.tool_name ? `${t.sessions.roles.tool}: ${msg.tool_name}` : style.label;
|
||||
const label = msg.tool_name
|
||||
? `${t.sessions.roles.tool}: ${msg.tool_name}`
|
||||
: style.label;
|
||||
|
||||
// Check if any search term appears as a prefix of any word in content
|
||||
const isHit = (() => {
|
||||
|
|
@ -115,26 +157,35 @@ function MessageBubble({ msg, highlight }: { msg: SessionMessage; highlight?: st
|
|||
})();
|
||||
|
||||
// Split search query into terms for inline highlighting
|
||||
const highlightTerms = isHit && highlight
|
||||
? highlight.split(/\s+/).filter(Boolean)
|
||||
: undefined;
|
||||
const highlightTerms =
|
||||
isHit && highlight ? highlight.split(/\s+/).filter(Boolean) : undefined;
|
||||
|
||||
return (
|
||||
<div className={`${style.bg} p-3 ${isHit ? "ring-1 ring-warning/40" : ""}`} data-search-hit={isHit || undefined}>
|
||||
<div
|
||||
className={`${style.bg} p-3 ${isHit ? "ring-1 ring-warning/40" : ""}`}
|
||||
data-search-hit={isHit || undefined}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`text-xs font-semibold ${style.text}`}>{label}</span>
|
||||
{isHit && (
|
||||
<Badge variant="warning" className="text-[9px] py-0 px-1.5">{t.common.match}</Badge>
|
||||
<Badge variant="warning" className="text-[9px] py-0 px-1.5">
|
||||
{t.common.match}
|
||||
</Badge>
|
||||
)}
|
||||
{msg.timestamp && (
|
||||
<span className="text-[10px] text-muted-foreground">{timeAgo(msg.timestamp)}</span>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{timeAgo(msg.timestamp)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{msg.content && (
|
||||
msg.role === "system"
|
||||
? <div className="text-sm text-foreground whitespace-pre-wrap leading-relaxed">{msg.content}</div>
|
||||
: <Markdown content={msg.content} highlightTerms={highlightTerms} />
|
||||
)}
|
||||
{msg.content &&
|
||||
(msg.role === "system" ? (
|
||||
<div className="text-sm text-foreground whitespace-pre-wrap leading-relaxed">
|
||||
{msg.content}
|
||||
</div>
|
||||
) : (
|
||||
<Markdown content={msg.content} highlightTerms={highlightTerms} />
|
||||
))}
|
||||
{msg.tool_calls && msg.tool_calls.length > 0 && (
|
||||
<div className="mt-1">
|
||||
{msg.tool_calls.map((tc) => (
|
||||
|
|
@ -147,7 +198,13 @@ function MessageBubble({ msg, highlight }: { msg: SessionMessage; highlight?: st
|
|||
}
|
||||
|
||||
/** Message list with auto-scroll to first search hit. */
|
||||
function MessageList({ messages, highlight }: { messages: SessionMessage[]; highlight?: string }) {
|
||||
function MessageList({
|
||||
messages,
|
||||
highlight,
|
||||
}: {
|
||||
messages: SessionMessage[];
|
||||
highlight?: string;
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -163,7 +220,10 @@ function MessageList({ messages, highlight }: { messages: SessionMessage[]; high
|
|||
}, [messages, highlight]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="flex flex-col gap-3 max-h-[600px] overflow-y-auto pr-2">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex flex-col gap-3 max-h-[600px] overflow-y-auto pr-2"
|
||||
>
|
||||
{messages.map((msg, i) => (
|
||||
<MessageBubble key={i} msg={msg} highlight={highlight} />
|
||||
))}
|
||||
|
|
@ -202,16 +262,20 @@ function SessionRow({
|
|||
}
|
||||
}, [isExpanded, session.id, messages, loading]);
|
||||
|
||||
const sourceInfo = (session.source ? SOURCE_CONFIG[session.source] : null) ?? { icon: Globe, color: "text-muted-foreground" };
|
||||
const sourceInfo = (session.source
|
||||
? SOURCE_CONFIG[session.source]
|
||||
: null) ?? { icon: Globe, color: "text-muted-foreground" };
|
||||
const SourceIcon = sourceInfo.icon;
|
||||
const hasTitle = session.title && session.title !== "Untitled";
|
||||
|
||||
return (
|
||||
<div className={`border overflow-hidden transition-colors ${
|
||||
session.is_active
|
||||
? "border-success/30 bg-success/[0.03]"
|
||||
: "border-border"
|
||||
}`}>
|
||||
<div
|
||||
className={`border overflow-hidden transition-colors ${
|
||||
session.is_active
|
||||
? "border-success/30 bg-success/[0.03]"
|
||||
: "border-border"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-between p-3 cursor-pointer hover:bg-secondary/30 transition-colors"
|
||||
onClick={onToggle}
|
||||
|
|
@ -222,8 +286,14 @@ function SessionRow({
|
|||
</div>
|
||||
<div className="flex flex-col gap-0.5 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm truncate pr-2 ${hasTitle ? "font-medium" : "text-muted-foreground italic"}`}>
|
||||
{hasTitle ? session.title : (session.preview ? session.preview.slice(0, 60) : t.sessions.untitledSession)}
|
||||
<span
|
||||
className={`text-sm truncate pr-2 ${hasTitle ? "font-medium" : "text-muted-foreground italic"}`}
|
||||
>
|
||||
{hasTitle
|
||||
? session.title
|
||||
: session.preview
|
||||
? session.preview.slice(0, 60)
|
||||
: t.sessions.untitledSession}
|
||||
</span>
|
||||
{session.is_active && (
|
||||
<Badge variant="success" className="text-[10px] shrink-0">
|
||||
|
|
@ -233,21 +303,25 @@ function SessionRow({
|
|||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<span className="truncate max-w-[120px] sm:max-w-[180px]">{(session.model ?? t.common.unknown).split("/").pop()}</span>
|
||||
<span className="truncate max-w-[120px] sm:max-w-[180px]">
|
||||
{(session.model ?? t.common.unknown).split("/").pop()}
|
||||
</span>
|
||||
<span className="text-border">·</span>
|
||||
<span>{session.message_count} {t.common.msgs}</span>
|
||||
<span>
|
||||
{session.message_count} {t.common.msgs}
|
||||
</span>
|
||||
{session.tool_call_count > 0 && (
|
||||
<>
|
||||
<span className="text-border">·</span>
|
||||
<span>{session.tool_call_count} {t.common.tools}</span>
|
||||
<span>
|
||||
{session.tool_call_count} {t.common.tools}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="text-border">·</span>
|
||||
<span>{timeAgo(session.last_active)}</span>
|
||||
</div>
|
||||
{snippet && (
|
||||
<SnippetHighlight snippet={snippet} />
|
||||
)}
|
||||
{snippet && <SnippetHighlight snippet={snippet} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -281,7 +355,9 @@ function SessionRow({
|
|||
<p className="text-sm text-destructive py-4 text-center">{error}</p>
|
||||
)}
|
||||
{messages && messages.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground py-4 text-center">{t.sessions.noMessages}</p>
|
||||
<p className="text-sm text-muted-foreground py-4 text-center">
|
||||
{t.sessions.noMessages}
|
||||
</p>
|
||||
)}
|
||||
{messages && messages.length > 0 && (
|
||||
<MessageList messages={messages} highlight={searchQuery} />
|
||||
|
|
@ -300,7 +376,9 @@ export default function SessionsPage() {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState("");
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [searchResults, setSearchResults] = useState<SessionSearchResult[] | null>(null);
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
SessionSearchResult[] | null
|
||||
>(null);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
const { t } = useI18n();
|
||||
|
|
@ -420,7 +498,9 @@ export default function SessionsPage() {
|
|||
{search ? t.sessions.noMatch : t.sessions.noSessions}
|
||||
</p>
|
||||
{!search && (
|
||||
<p className="text-xs mt-1 text-muted-foreground/60">{t.sessions.startConversation}</p>
|
||||
<p className="text-xs mt-1 text-muted-foreground/60">
|
||||
{t.sessions.startConversation}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -445,7 +525,8 @@ export default function SessionsPage() {
|
|||
{!searchResults && total > PAGE_SIZE && (
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} {t.common.of} {total}
|
||||
{page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)}{" "}
|
||||
{t.common.of} {total}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
|
|
@ -459,7 +540,8 @@ export default function SessionsPage() {
|
|||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground px-2">
|
||||
{t.common.page} {page + 1} {t.common.of} {Math.ceil(total / PAGE_SIZE)}
|
||||
{t.common.page} {page + 1} {t.common.of}{" "}
|
||||
{Math.ceil(total / PAGE_SIZE)}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue