fix: imports

This commit is contained in:
Austin Pickett 2026-04-19 18:52:04 -04:00
parent 60fd4b7d16
commit 823b6d08ed
11 changed files with 2127 additions and 146 deletions

View file

@ -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">&#183;</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">&#183;</span>
<span>{session.tool_call_count} {t.common.tools}</span>
<span>
{session.tool_call_count} {t.common.tools}
</span>
</>
)}
<span className="text-border">&#183;</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"