feat: add Chat button and redesign chat page with sidebar layout
- Add Chat button to public agent detail page - Redesign chat page with collapsible sidebar: - Left sidebar shows agent config, tools, and collections - Right side shows chat interface - Toggle button to show/hide sidebar - Sidebar displays provider, model, temperature, system prompt - Links to individual tools and collections
This commit is contained in:
parent
3bad010833
commit
f40fedae8d
2 changed files with 375 additions and 188 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { AIProvider } from '@tpmjs/types/agent';
|
import type { AIProvider } from '@tpmjs/types/agent';
|
||||||
|
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||||
import { Button } from '@tpmjs/ui/Button/Button';
|
import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
@ -8,6 +9,36 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
|
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
|
||||||
import { AppHeader } from '~/components/AppHeader';
|
import { AppHeader } from '~/components/AppHeader';
|
||||||
|
import { LikeButton } from '~/components/LikeButton';
|
||||||
|
|
||||||
|
interface AgentTool {
|
||||||
|
id: string;
|
||||||
|
toolId: string;
|
||||||
|
position: number;
|
||||||
|
tool: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
likeCount: number;
|
||||||
|
package: {
|
||||||
|
id: string;
|
||||||
|
npmPackageName: string;
|
||||||
|
category: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentCollection {
|
||||||
|
id: string;
|
||||||
|
collectionId: string;
|
||||||
|
position: number;
|
||||||
|
collection: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
toolCount: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface Agent {
|
interface Agent {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -16,11 +47,19 @@ interface Agent {
|
||||||
description: string | null;
|
description: string | null;
|
||||||
provider: AIProvider;
|
provider: AIProvider;
|
||||||
modelId: string;
|
modelId: string;
|
||||||
|
systemPrompt: string | null;
|
||||||
|
temperature: number;
|
||||||
|
maxToolCallsPerTurn: number;
|
||||||
|
likeCount: number;
|
||||||
|
toolCount: number;
|
||||||
|
collectionCount: number;
|
||||||
createdBy: {
|
createdBy: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
image: string | null;
|
image: string | null;
|
||||||
};
|
};
|
||||||
|
tools: AgentTool[];
|
||||||
|
collections: AgentCollection[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
|
|
@ -188,6 +227,7 @@ export default function PublicAgentChatPage(): React.ReactElement {
|
||||||
const [expandedToolCalls, setExpandedToolCalls] = useState<Set<string>>(new Set());
|
const [expandedToolCalls, setExpandedToolCalls] = useState<Set<string>>(new Set());
|
||||||
const [hasMoreMessages, setHasMoreMessages] = useState(false);
|
const [hasMoreMessages, setHasMoreMessages] = useState(false);
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
|
||||||
// Track first item index for prepending (Virtuoso pattern)
|
// Track first item index for prepending (Virtuoso pattern)
|
||||||
const [firstItemIndex, setFirstItemIndex] = useState(10000);
|
const [firstItemIndex, setFirstItemIndex] = useState(10000);
|
||||||
|
|
@ -471,197 +511,336 @@ export default function PublicAgentChatPage(): React.ReactElement {
|
||||||
<div className="min-h-screen bg-background flex flex-col">
|
<div className="min-h-screen bg-background flex flex-col">
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
|
|
||||||
{/* Chat Header */}
|
<div className="flex-1 flex overflow-hidden">
|
||||||
<div className="border-b border-border bg-surface/50">
|
{/* Sidebar */}
|
||||||
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
|
<div
|
||||||
<div className="flex items-center gap-4">
|
className={`${sidebarOpen ? 'w-80' : 'w-0'} transition-all duration-300 border-r border-border bg-surface/30 overflow-hidden flex-shrink-0`}
|
||||||
<Link
|
>
|
||||||
href={`/agents/${agentId}`}
|
<div className="w-80 h-full overflow-y-auto p-4">
|
||||||
className="text-foreground-secondary hover:text-foreground transition-colors"
|
{/* Agent Header */}
|
||||||
>
|
<div className="mb-6">
|
||||||
<Icon icon="arrowLeft" size="sm" />
|
<Link
|
||||||
</Link>
|
href={`/agents/${agentId}`}
|
||||||
<div>
|
className="inline-flex items-center gap-1 text-sm text-foreground-secondary hover:text-foreground mb-3"
|
||||||
<h1 className="text-lg font-semibold text-foreground">{agent.name}</h1>
|
>
|
||||||
<p className="text-sm text-foreground-tertiary">
|
<Icon icon="arrowLeft" size="xs" />
|
||||||
{PROVIDER_DISPLAY_NAMES[agent.provider]} • Created by {agent.createdBy.name}
|
Back to Agent
|
||||||
</p>
|
</Link>
|
||||||
</div>
|
<div className="flex items-start justify-between">
|
||||||
</div>
|
<div>
|
||||||
<Button variant="outline" size="sm" onClick={startNewConversation}>
|
<h2 className="text-lg font-semibold text-foreground">{agent.name}</h2>
|
||||||
<Icon icon="plus" size="xs" className="mr-2" />
|
{agent.description && (
|
||||||
New Chat
|
<p className="text-sm text-foreground-secondary mt-1 line-clamp-2">
|
||||||
</Button>
|
{agent.description}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
{/* Chat Area */}
|
</div>
|
||||||
<div className="flex-1 flex flex-col max-w-4xl mx-auto w-full overflow-hidden">
|
<div className="flex items-center gap-2 mt-3">
|
||||||
{/* Messages */}
|
<LikeButton
|
||||||
{messages.length === 0 && !streamingContent ? (
|
entityType="agent"
|
||||||
<div className="flex-1 flex items-center justify-center p-4">
|
entityId={agent.id}
|
||||||
<div className="text-center">
|
initialCount={agent.likeCount}
|
||||||
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-4">
|
size="sm"
|
||||||
<Icon icon="message" size="lg" className="text-primary" />
|
showCount={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-medium text-foreground mb-2">Start a conversation</h3>
|
|
||||||
<p className="text-foreground-secondary max-w-sm">
|
|
||||||
Send a message to start chatting with {agent.name}.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Virtuoso
|
|
||||||
ref={virtuosoRef}
|
|
||||||
className="flex-1"
|
|
||||||
data={messages}
|
|
||||||
firstItemIndex={firstItemIndex}
|
|
||||||
initialTopMostItemIndex={messages.length - 1}
|
|
||||||
startReached={loadMoreMessages}
|
|
||||||
followOutput="smooth"
|
|
||||||
components={{
|
|
||||||
Header: () =>
|
|
||||||
hasMoreMessages ? (
|
|
||||||
<div className="flex justify-center py-4">
|
|
||||||
{isLoadingMore ? (
|
|
||||||
<div className="flex items-center gap-2 text-foreground-secondary">
|
|
||||||
<Icon icon="loader" size="sm" className="animate-spin" />
|
|
||||||
<span className="text-sm">Loading older messages...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={loadMoreMessages}
|
|
||||||
className="text-sm text-primary hover:text-primary/80 transition-colors"
|
|
||||||
>
|
|
||||||
Load older messages
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null,
|
|
||||||
Footer: () => (
|
|
||||||
<div className="px-4 pb-4 space-y-4">
|
|
||||||
{/* Live tool calls during streaming */}
|
|
||||||
{toolCalls.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{toolCalls.map((tc) => (
|
|
||||||
<div key={tc.toolCallId} className="flex justify-start">
|
|
||||||
<div className="max-w-[80%]">
|
|
||||||
<ToolCallCard
|
|
||||||
toolCall={tc}
|
|
||||||
isExpanded={expandedToolCalls.has(tc.toolCallId)}
|
|
||||||
onToggle={() => toggleToolCall(tc.toolCallId)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{streamingContent && (
|
{/* Configuration */}
|
||||||
<div className="flex justify-start">
|
<div className="mb-6">
|
||||||
<div className="max-w-[80%] rounded-lg p-4 bg-surface-secondary">
|
<h3 className="text-sm font-medium text-foreground mb-3 flex items-center gap-2">
|
||||||
<p className="whitespace-pre-wrap text-sm">{streamingContent}</p>
|
<Icon icon="box" size="xs" className="text-foreground-tertiary" />
|
||||||
<span className="inline-block w-2 h-4 bg-primary animate-pulse ml-1" />
|
Configuration
|
||||||
</div>
|
</h3>
|
||||||
</div>
|
<div className="space-y-2 text-sm">
|
||||||
)}
|
<div className="flex justify-between">
|
||||||
|
<span className="text-foreground-tertiary">Provider</span>
|
||||||
{isSending && !streamingContent && toolCalls.length === 0 && (
|
<Badge variant="secondary" size="sm">
|
||||||
<div className="flex justify-start">
|
{PROVIDER_DISPLAY_NAMES[agent.provider]}
|
||||||
<div className="rounded-lg p-4 bg-surface-secondary">
|
</Badge>
|
||||||
<div className="flex items-center gap-2 text-foreground-secondary">
|
|
||||||
<Icon icon="loader" size="sm" className="animate-spin" />
|
|
||||||
<span className="text-sm">Thinking...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
),
|
<div className="flex justify-between">
|
||||||
}}
|
<span className="text-foreground-tertiary">Model</span>
|
||||||
itemContent={(_index, message) => {
|
<span className="text-foreground font-mono text-xs">{agent.modelId}</span>
|
||||||
// Parse tool output safely
|
|
||||||
const getToolOutput = () => {
|
|
||||||
if (message.toolResult) return message.toolResult;
|
|
||||||
try {
|
|
||||||
return JSON.parse(message.content || '{}');
|
|
||||||
} catch {
|
|
||||||
return { result: message.content };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="px-4 py-2">
|
|
||||||
<div
|
|
||||||
className={`flex ${message.role === 'USER' ? 'justify-end' : 'justify-start'}`}
|
|
||||||
>
|
|
||||||
{message.role === 'TOOL' ? (
|
|
||||||
<div className="max-w-[80%]">
|
|
||||||
<ToolCallCard
|
|
||||||
toolCall={{
|
|
||||||
toolCallId: message.toolCallId || message.id,
|
|
||||||
toolName: message.toolName || 'Unknown Tool',
|
|
||||||
output: getToolOutput(),
|
|
||||||
status: 'success',
|
|
||||||
}}
|
|
||||||
isExpanded={expandedToolCalls.has(message.toolCallId || message.id)}
|
|
||||||
onToggle={() => toggleToolCall(message.toolCallId || message.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`max-w-[80%] rounded-lg p-4 ${
|
|
||||||
message.role === 'USER'
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: 'bg-surface-secondary'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="whitespace-pre-wrap text-sm">{message.content}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="flex justify-between">
|
||||||
}}
|
<span className="text-foreground-tertiary">Temperature</span>
|
||||||
/>
|
<span className="text-foreground">{agent.temperature}</span>
|
||||||
)}
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-foreground-tertiary">Max Tool Calls</span>
|
||||||
|
<span className="text-foreground">{agent.maxToolCallsPerTurn}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Error Message */}
|
{agent.systemPrompt && (
|
||||||
{error && (
|
<div className="mt-3">
|
||||||
<div className="px-4 py-2 bg-red-50 dark:bg-red-900/20 border-t border-red-200 dark:border-red-800">
|
<span className="text-xs text-foreground-tertiary block mb-1">System Prompt</span>
|
||||||
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
<pre className="p-2 bg-background border border-border rounded text-xs text-foreground-secondary whitespace-pre-wrap font-mono max-h-32 overflow-y-auto">
|
||||||
|
{agent.systemPrompt}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tools */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-sm font-medium text-foreground mb-3 flex items-center gap-2">
|
||||||
|
<Icon icon="puzzle" size="xs" className="text-foreground-tertiary" />
|
||||||
|
Tools ({agent.tools.length})
|
||||||
|
</h3>
|
||||||
|
{agent.tools.length === 0 ? (
|
||||||
|
<p className="text-sm text-foreground-tertiary">No tools configured</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{agent.tools.map((at) => (
|
||||||
|
<Link
|
||||||
|
key={at.id}
|
||||||
|
href={`/tool/${at.tool.package.npmPackageName}/${at.tool.name}`}
|
||||||
|
className="block p-2 bg-background border border-border rounded hover:border-foreground/20 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-medium text-foreground truncate">
|
||||||
|
{at.tool.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-foreground-tertiary truncate">
|
||||||
|
{at.tool.package.npmPackageName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" size="sm" className="ml-2 flex-shrink-0">
|
||||||
|
{at.tool.package.category}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Collections */}
|
||||||
|
{agent.collections.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-foreground mb-3 flex items-center gap-2">
|
||||||
|
<Icon icon="folder" size="xs" className="text-foreground-tertiary" />
|
||||||
|
Collections ({agent.collections.length})
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{agent.collections.map((ac) => (
|
||||||
|
<Link
|
||||||
|
key={ac.id}
|
||||||
|
href={`/collections/${ac.collection.id}`}
|
||||||
|
className="block p-2 bg-background border border-border rounded hover:border-foreground/20 transition-colors"
|
||||||
|
>
|
||||||
|
<p className="text-sm font-medium text-foreground truncate">
|
||||||
|
{ac.collection.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-foreground-tertiary">
|
||||||
|
{ac.collection.toolCount} tools
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Input Area */}
|
{/* Main Chat Area */}
|
||||||
<div className="border-t border-border p-4">
|
<div className="flex-1 flex flex-col min-w-0">
|
||||||
<div className="flex items-end gap-2">
|
{/* Chat Header */}
|
||||||
<textarea
|
<div className="border-b border-border bg-surface/50 px-4 py-3 flex items-center justify-between">
|
||||||
ref={inputRef}
|
<div className="flex items-center gap-3">
|
||||||
value={input}
|
<button
|
||||||
onChange={(e) => setInput(e.target.value)}
|
type="button"
|
||||||
onKeyDown={handleKeyDown}
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
placeholder="Type a message..."
|
className="p-2 hover:bg-surface-secondary rounded-lg transition-colors"
|
||||||
rows={1}
|
title={sidebarOpen ? 'Hide sidebar' : 'Show sidebar'}
|
||||||
className="flex-1 px-4 py-3 bg-background border border-border rounded-lg text-foreground placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary resize-none min-h-[48px] max-h-[200px]"
|
>
|
||||||
style={{
|
<Icon icon="menu" size="sm" />
|
||||||
height: 'auto',
|
</button>
|
||||||
minHeight: '48px',
|
<div>
|
||||||
}}
|
<h1 className="text-lg font-semibold text-foreground">{agent.name}</h1>
|
||||||
onInput={(e) => {
|
<p className="text-sm text-foreground-tertiary">
|
||||||
const target = e.target as HTMLTextAreaElement;
|
{PROVIDER_DISPLAY_NAMES[agent.provider]} • {agent.modelId}
|
||||||
target.style.height = 'auto';
|
</p>
|
||||||
target.style.height = `${Math.min(target.scrollHeight, 200)}px`;
|
</div>
|
||||||
}}
|
</div>
|
||||||
/>
|
<Button variant="outline" size="sm" onClick={startNewConversation}>
|
||||||
<Button onClick={handleSend} disabled={isSending || !input.trim()}>
|
<Icon icon="plus" size="xs" className="mr-2" />
|
||||||
<Icon icon="send" size="sm" />
|
New Chat
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-foreground-tertiary mt-2">
|
|
||||||
Press Enter to send, Shift+Enter for new line
|
{/* Messages */}
|
||||||
</p>
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{messages.length === 0 && !streamingContent ? (
|
||||||
|
<div className="h-full flex items-center justify-center p-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Icon icon="message" size="lg" className="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-foreground mb-2">Start a conversation</h3>
|
||||||
|
<p className="text-foreground-secondary max-w-sm">
|
||||||
|
Send a message to start chatting with {agent.name}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Virtuoso
|
||||||
|
ref={virtuosoRef}
|
||||||
|
className="h-full"
|
||||||
|
data={messages}
|
||||||
|
firstItemIndex={firstItemIndex}
|
||||||
|
initialTopMostItemIndex={messages.length - 1}
|
||||||
|
startReached={loadMoreMessages}
|
||||||
|
followOutput="smooth"
|
||||||
|
components={{
|
||||||
|
Header: () =>
|
||||||
|
hasMoreMessages ? (
|
||||||
|
<div className="flex justify-center py-4">
|
||||||
|
{isLoadingMore ? (
|
||||||
|
<div className="flex items-center gap-2 text-foreground-secondary">
|
||||||
|
<Icon icon="loader" size="sm" className="animate-spin" />
|
||||||
|
<span className="text-sm">Loading older messages...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={loadMoreMessages}
|
||||||
|
className="text-sm text-primary hover:text-primary/80 transition-colors"
|
||||||
|
>
|
||||||
|
Load older messages
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
Footer: () => (
|
||||||
|
<div className="px-4 pb-4 space-y-4">
|
||||||
|
{/* Live tool calls during streaming */}
|
||||||
|
{toolCalls.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{toolCalls.map((tc) => (
|
||||||
|
<div key={tc.toolCallId} className="flex justify-start">
|
||||||
|
<div className="max-w-[80%]">
|
||||||
|
<ToolCallCard
|
||||||
|
toolCall={tc}
|
||||||
|
isExpanded={expandedToolCalls.has(tc.toolCallId)}
|
||||||
|
onToggle={() => toggleToolCall(tc.toolCallId)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{streamingContent && (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="max-w-[80%] rounded-lg p-4 bg-surface-secondary">
|
||||||
|
<p className="whitespace-pre-wrap text-sm">{streamingContent}</p>
|
||||||
|
<span className="inline-block w-2 h-4 bg-primary animate-pulse ml-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSending && !streamingContent && toolCalls.length === 0 && (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="rounded-lg p-4 bg-surface-secondary">
|
||||||
|
<div className="flex items-center gap-2 text-foreground-secondary">
|
||||||
|
<Icon icon="loader" size="sm" className="animate-spin" />
|
||||||
|
<span className="text-sm">Thinking...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
itemContent={(_index, message) => {
|
||||||
|
// Parse tool output safely
|
||||||
|
const getToolOutput = () => {
|
||||||
|
if (message.toolResult) return message.toolResult;
|
||||||
|
try {
|
||||||
|
return JSON.parse(message.content || '{}');
|
||||||
|
} catch {
|
||||||
|
return { result: message.content };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-4 py-2">
|
||||||
|
<div
|
||||||
|
className={`flex ${message.role === 'USER' ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
{message.role === 'TOOL' ? (
|
||||||
|
<div className="max-w-[80%]">
|
||||||
|
<ToolCallCard
|
||||||
|
toolCall={{
|
||||||
|
toolCallId: message.toolCallId || message.id,
|
||||||
|
toolName: message.toolName || 'Unknown Tool',
|
||||||
|
output: getToolOutput(),
|
||||||
|
status: 'success',
|
||||||
|
}}
|
||||||
|
isExpanded={expandedToolCalls.has(message.toolCallId || message.id)}
|
||||||
|
onToggle={() => toggleToolCall(message.toolCallId || message.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={`max-w-[80%] rounded-lg p-4 ${
|
||||||
|
message.role === 'USER'
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'bg-surface-secondary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="whitespace-pre-wrap text-sm">{message.content}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<div className="px-4 py-2 bg-red-50 dark:bg-red-900/20 border-t border-red-200 dark:border-red-800">
|
||||||
|
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Input Area */}
|
||||||
|
<div className="border-t border-border p-4">
|
||||||
|
<div className="flex items-end gap-2">
|
||||||
|
<textarea
|
||||||
|
ref={inputRef}
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Type a message..."
|
||||||
|
rows={1}
|
||||||
|
className="flex-1 px-4 py-3 bg-background border border-border rounded-lg text-foreground placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary resize-none min-h-[48px] max-h-[200px]"
|
||||||
|
style={{
|
||||||
|
height: 'auto',
|
||||||
|
minHeight: '48px',
|
||||||
|
}}
|
||||||
|
onInput={(e) => {
|
||||||
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
target.style.height = 'auto';
|
||||||
|
target.style.height = `${Math.min(target.scrollHeight, 200)}px`;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button onClick={handleSend} disabled={isSending || !input.trim()}>
|
||||||
|
<Icon icon="send" size="sm" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-foreground-tertiary mt-2">
|
||||||
|
Press Enter to send, Shift+Enter for new line
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -160,13 +160,21 @@ export default function PublicAgentDetailPage(): React.ReactElement {
|
||||||
<h1 className="text-3xl font-bold text-foreground mb-2">{agent.name}</h1>
|
<h1 className="text-3xl font-bold text-foreground mb-2">{agent.name}</h1>
|
||||||
{agent.description && <p className="text-foreground-secondary">{agent.description}</p>}
|
{agent.description && <p className="text-foreground-secondary">{agent.description}</p>}
|
||||||
</div>
|
</div>
|
||||||
<LikeButton
|
<div className="flex items-center gap-3">
|
||||||
entityType="agent"
|
<LikeButton
|
||||||
entityId={agent.id}
|
entityType="agent"
|
||||||
initialCount={agent.likeCount}
|
entityId={agent.id}
|
||||||
showCount={true}
|
initialCount={agent.likeCount}
|
||||||
variant="outline"
|
showCount={true}
|
||||||
/>
|
variant="outline"
|
||||||
|
/>
|
||||||
|
<Link href={`/agents/${agentId}/chat`}>
|
||||||
|
<Button>
|
||||||
|
<Icon icon="message" size="sm" className="mr-2" />
|
||||||
|
Chat
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Meta info */}
|
{/* Meta info */}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue