mirror of https://github.com/kortix-ai/suna.git
Merge pull request #346 from rishimohan/fix-share-page-ui
fix: z-index issue on share pages preventing clicks on header, also add canonical url
This commit is contained in:
commit
269da4f041
|
@ -6,6 +6,9 @@ export async function generateMetadata({ params }): Promise<Metadata> {
|
||||||
const fallbackMetaData = {
|
const fallbackMetaData = {
|
||||||
title: 'Shared Conversation | Kortix Suna',
|
title: 'Shared Conversation | Kortix Suna',
|
||||||
description: 'Replay this Agent conversation on Kortix Suna',
|
description: 'Replay this Agent conversation on Kortix Suna',
|
||||||
|
alternates: {
|
||||||
|
canonical: `${process.env.NEXT_PUBLIC_URL}/share/${threadId}`,
|
||||||
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'Shared Conversation | Kortix Suna',
|
title: 'Shared Conversation | Kortix Suna',
|
||||||
description: 'Replay this Agent conversation on Kortix Suna',
|
description: 'Replay this Agent conversation on Kortix Suna',
|
||||||
|
@ -13,7 +16,6 @@ export async function generateMetadata({ params }): Promise<Metadata> {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const threadData = await getThread(threadId);
|
const threadData = await getThread(threadId);
|
||||||
const projectData = await getProject(threadData.project_id);
|
const projectData = await getProject(threadData.project_id);
|
||||||
|
@ -28,7 +30,9 @@ export async function generateMetadata({ params }): Promise<Metadata> {
|
||||||
process.env.NEXT_PUBLIC_ENV_MODE === 'local';
|
process.env.NEXT_PUBLIC_ENV_MODE === 'local';
|
||||||
|
|
||||||
const title = projectData.name || 'Shared Conversation | Kortix Suna';
|
const title = projectData.name || 'Shared Conversation | Kortix Suna';
|
||||||
const description = projectData.description || 'Replay this Agent conversation on Kortix Suna';
|
const description =
|
||||||
|
projectData.description ||
|
||||||
|
'Replay this Agent conversation on Kortix Suna';
|
||||||
const ogImage = isDevelopment
|
const ogImage = isDevelopment
|
||||||
? `${process.env.NEXT_PUBLIC_URL}/share-page/og-fallback.png`
|
? `${process.env.NEXT_PUBLIC_URL}/share-page/og-fallback.png`
|
||||||
: `${process.env.NEXT_PUBLIC_URL}/api/share-page/og-image?title=${projectData.name}`;
|
: `${process.env.NEXT_PUBLIC_URL}/api/share-page/og-image?title=${projectData.name}`;
|
||||||
|
@ -36,6 +40,9 @@ export async function generateMetadata({ params }): Promise<Metadata> {
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
alternates: {
|
||||||
|
canonical: `${process.env.NEXT_PUBLIC_URL}/share/${threadId}`,
|
||||||
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
@ -45,7 +52,6 @@ export async function generateMetadata({ params }): Promise<Metadata> {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images: ogImage,
|
images: ogImage,
|
||||||
creator: '@kortixai',
|
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,16 +12,25 @@ import {
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { FileViewerModal } from '@/components/thread/file-viewer-modal';
|
import { FileViewerModal } from '@/components/thread/file-viewer-modal';
|
||||||
import { ToolCallSidePanel, ToolCallInput } from "@/components/thread/tool-call-side-panel";
|
import {
|
||||||
|
ToolCallSidePanel,
|
||||||
|
ToolCallInput,
|
||||||
|
} from '@/components/thread/tool-call-side-panel';
|
||||||
import { ThreadContent } from '@/components/thread/content/ThreadContent';
|
import { ThreadContent } from '@/components/thread/content/ThreadContent';
|
||||||
import { PlaybackControls, PlaybackController } from '@/components/thread/content/PlaybackControls';
|
import {
|
||||||
import { UnifiedMessage, ParsedMetadata, ThreadParams } from '@/components/thread/types';
|
PlaybackControls,
|
||||||
|
PlaybackController,
|
||||||
|
} from '@/components/thread/content/PlaybackControls';
|
||||||
|
import {
|
||||||
|
UnifiedMessage,
|
||||||
|
ParsedMetadata,
|
||||||
|
ThreadParams,
|
||||||
|
} from '@/components/thread/types';
|
||||||
import { safeJsonParse } from '@/components/thread/utils';
|
import { safeJsonParse } from '@/components/thread/utils';
|
||||||
import { useAgentStream } from '@/hooks/useAgentStream';
|
import { useAgentStream } from '@/hooks/useAgentStream';
|
||||||
import { threadErrorCodeMessages } from '@/lib/constants/errorCodeMessages';
|
import { threadErrorCodeMessages } from '@/lib/constants/errorCodeMessages';
|
||||||
import { ThreadSkeleton } from '@/components/thread/content/ThreadSkeleton';
|
import { ThreadSkeleton } from '@/components/thread/content/ThreadSkeleton';
|
||||||
|
|
||||||
|
|
||||||
// Extend the base Message type with the expected database fields
|
// Extend the base Message type with the expected database fields
|
||||||
interface ApiMessageType extends BaseApiMessageType {
|
interface ApiMessageType extends BaseApiMessageType {
|
||||||
message_id?: string;
|
message_id?: string;
|
||||||
|
@ -43,7 +52,8 @@ interface StreamingToolCall {
|
||||||
|
|
||||||
// Add a helper function to extract tool calls from message content
|
// Add a helper function to extract tool calls from message content
|
||||||
const extractToolCallsFromMessage = (content: string) => {
|
const extractToolCallsFromMessage = (content: string) => {
|
||||||
const toolCallRegex = /<([a-zA-Z\-_]+)(?:\s+[^>]*)?>(?:[\s\S]*?)<\/\1>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/g;
|
const toolCallRegex =
|
||||||
|
/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>(?:[\s\S]*?)<\/\1>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/g;
|
||||||
const results = [];
|
const results = [];
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
|
@ -51,7 +61,7 @@ const extractToolCallsFromMessage = (content: string) => {
|
||||||
const toolName = match[1] || match[2];
|
const toolName = match[1] || match[2];
|
||||||
results.push({
|
results.push({
|
||||||
name: toolName,
|
name: toolName,
|
||||||
fullMatch: match[0]
|
fullMatch: match[0],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +81,9 @@ export default function ThreadPage({
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [agentRunId, setAgentRunId] = useState<string | null>(null);
|
const [agentRunId, setAgentRunId] = useState<string | null>(null);
|
||||||
const [agentStatus, setAgentStatus] = useState<'idle' | 'running' | 'connecting' | 'error'>('idle');
|
const [agentStatus, setAgentStatus] = useState<
|
||||||
|
'idle' | 'running' | 'connecting' | 'error'
|
||||||
|
>('idle');
|
||||||
const [isSidePanelOpen, setIsSidePanelOpen] = useState(false);
|
const [isSidePanelOpen, setIsSidePanelOpen] = useState(false);
|
||||||
const [toolCalls, setToolCalls] = useState<ToolCallInput[]>([]);
|
const [toolCalls, setToolCalls] = useState<ToolCallInput[]>([]);
|
||||||
const [currentToolIndex, setCurrentToolIndex] = useState<number>(0);
|
const [currentToolIndex, setCurrentToolIndex] = useState<number>(0);
|
||||||
|
@ -85,7 +97,9 @@ export default function ThreadPage({
|
||||||
useState<StreamingToolCall | null>(null);
|
useState<StreamingToolCall | null>(null);
|
||||||
|
|
||||||
// Create a message-to-tool-index map for faster lookups
|
// Create a message-to-tool-index map for faster lookups
|
||||||
const [messageToToolIndex, setMessageToToolIndex] = useState<Record<string, number>>({});
|
const [messageToToolIndex, setMessageToToolIndex] = useState<
|
||||||
|
Record<string, number>
|
||||||
|
>({});
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -130,10 +144,12 @@ export default function ThreadPage({
|
||||||
`[STREAM HANDLER] Received message: ID=${message.message_id}, Type=${message.type}`,
|
`[STREAM HANDLER] Received message: ID=${message.message_id}, Type=${message.type}`,
|
||||||
);
|
);
|
||||||
if (!message.message_id) {
|
if (!message.message_id) {
|
||||||
console.warn(`[STREAM HANDLER] Received message is missing ID: Type=${message.type}, Content=${message.content?.substring(0, 50)}...`);
|
console.warn(
|
||||||
|
`[STREAM HANDLER] Received message is missing ID: Type=${message.type}, Content=${message.content?.substring(0, 50)}...`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessages(prev => {
|
setMessages((prev) => {
|
||||||
// First check if the message already exists
|
// First check if the message already exists
|
||||||
const messageExists = prev.some(
|
const messageExists = prev.some(
|
||||||
(m) => m.message_id === message.message_id,
|
(m) => m.message_id === message.message_id,
|
||||||
|
@ -155,7 +171,8 @@ export default function ThreadPage({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleStreamStatusChange = useCallback((hookStatus: string) => {
|
const handleStreamStatusChange = useCallback(
|
||||||
|
(hookStatus: string) => {
|
||||||
console.log(`[PAGE] Hook status changed: ${hookStatus}`);
|
console.log(`[PAGE] Hook status changed: ${hookStatus}`);
|
||||||
switch (hookStatus) {
|
switch (hookStatus) {
|
||||||
case 'idle':
|
case 'idle':
|
||||||
|
@ -182,7 +199,9 @@ export default function ThreadPage({
|
||||||
}, 3000);
|
}, 3000);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [threadId]);
|
},
|
||||||
|
[threadId],
|
||||||
|
);
|
||||||
|
|
||||||
const handleStreamError = useCallback((errorMessage: string) => {
|
const handleStreamError = useCallback((errorMessage: string) => {
|
||||||
console.error(`[PAGE] Stream hook error: ${errorMessage}`);
|
console.error(`[PAGE] Stream hook error: ${errorMessage}`);
|
||||||
|
@ -207,7 +226,11 @@ export default function ThreadPage({
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentMessage = messages[currentMessageIndex];
|
const currentMessage = messages[currentMessageIndex];
|
||||||
console.log(`Playing message ${currentMessageIndex}:`, currentMessage.type, currentMessage.message_id);
|
console.log(
|
||||||
|
`Playing message ${currentMessageIndex}:`,
|
||||||
|
currentMessage.type,
|
||||||
|
currentMessage.message_id,
|
||||||
|
);
|
||||||
|
|
||||||
// Move to the next message
|
// Move to the next message
|
||||||
setCurrentMessageIndex((prevIndex) => prevIndex + 1);
|
setCurrentMessageIndex((prevIndex) => prevIndex + 1);
|
||||||
|
@ -260,7 +283,7 @@ export default function ThreadPage({
|
||||||
|
|
||||||
// Start loading all data in parallel
|
// Start loading all data in parallel
|
||||||
const [threadData, messagesData] = await Promise.all([
|
const [threadData, messagesData] = await Promise.all([
|
||||||
getThread(threadId).catch(err => {
|
getThread(threadId).catch((err) => {
|
||||||
if (threadErrorCodeMessages[err.code]) {
|
if (threadErrorCodeMessages[err.code]) {
|
||||||
setError(threadErrorCodeMessages[err.code]);
|
setError(threadErrorCodeMessages[err.code]);
|
||||||
} else {
|
} else {
|
||||||
|
@ -268,7 +291,7 @@ export default function ThreadPage({
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
getMessages(threadId).catch(err => {
|
getMessages(threadId).catch((err) => {
|
||||||
console.warn('Failed to load messages:', err);
|
console.warn('Failed to load messages:', err);
|
||||||
return [];
|
return [];
|
||||||
}),
|
}),
|
||||||
|
@ -277,11 +300,12 @@ export default function ThreadPage({
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
|
||||||
// Load project data if we have a project ID
|
// Load project data if we have a project ID
|
||||||
const projectData = threadData?.project_id ?
|
const projectData = threadData?.project_id
|
||||||
await getProject(threadData.project_id).catch(err => {
|
? await getProject(threadData.project_id).catch((err) => {
|
||||||
console.warn('[SHARE] Could not load project data:', err);
|
console.warn('[SHARE] Could not load project data:', err);
|
||||||
return null;
|
return null;
|
||||||
}) : null;
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
if (projectData) {
|
if (projectData) {
|
||||||
|
@ -319,7 +343,9 @@ export default function ThreadPage({
|
||||||
|
|
||||||
// Calculate historical tool pairs
|
// Calculate historical tool pairs
|
||||||
const historicalToolPairs: ToolCallInput[] = [];
|
const historicalToolPairs: ToolCallInput[] = [];
|
||||||
const assistantMessages = unifiedMessages.filter(m => m.type === 'assistant' && m.message_id);
|
const assistantMessages = unifiedMessages.filter(
|
||||||
|
(m) => m.type === 'assistant' && m.message_id,
|
||||||
|
);
|
||||||
|
|
||||||
// Map to track which assistant messages have tool results
|
// Map to track which assistant messages have tool results
|
||||||
const assistantToolMap = new Map<string, UnifiedMessage>();
|
const assistantToolMap = new Map<string, UnifiedMessage>();
|
||||||
|
@ -355,7 +381,8 @@ export default function ThreadPage({
|
||||||
assistantContent = { content: assistantMsg.content };
|
assistantContent = { content: assistantMsg.content };
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistantMessageText = assistantContent.content || assistantMsg.content;
|
const assistantMessageText =
|
||||||
|
assistantContent.content || assistantMsg.content;
|
||||||
|
|
||||||
// Use a regex to find tool calls in the message content
|
// Use a regex to find tool calls in the message content
|
||||||
const toolCalls = extractToolCallsFromMessage(assistantMessageText);
|
const toolCalls = extractToolCallsFromMessage(assistantMessageText);
|
||||||
|
@ -448,46 +475,63 @@ export default function ThreadPage({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="text-xs font-medium text-muted-foreground">Assistant Message</div>
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
|
Assistant Message
|
||||||
|
</div>
|
||||||
<div className="rounded-md border bg-muted/50 p-3">
|
<div className="rounded-md border bg-muted/50 p-3">
|
||||||
<div className="text-xs prose prose-xs dark:prose-invert chat-markdown max-w-none">{assistantContent}</div>
|
<div className="text-xs prose prose-xs dark:prose-invert chat-markdown max-w-none">
|
||||||
|
{assistantContent}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Process the tool result data
|
// Process the tool result data
|
||||||
const toolViewResult = useCallback((toolContent?: string, isSuccess?: boolean) => {
|
const toolViewResult = useCallback(
|
||||||
|
(toolContent?: string, isSuccess?: boolean) => {
|
||||||
if (!toolContent) return null;
|
if (!toolContent) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="text-xs font-medium text-muted-foreground">Tool Result</div>
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
<div className={`px-2 py-0.5 rounded-full text-xs ${isSuccess
|
Tool Result
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${
|
||||||
|
isSuccess
|
||||||
? 'bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300'
|
? 'bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300'
|
||||||
: 'bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-300'
|
: 'bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-300'
|
||||||
}`}>
|
}`}
|
||||||
|
>
|
||||||
{isSuccess ? 'Success' : 'Failed'}
|
{isSuccess ? 'Success' : 'Failed'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border bg-muted/50 p-3">
|
<div className="rounded-md border bg-muted/50 p-3">
|
||||||
<div className="text-xs prose prose-xs dark:prose-invert chat-markdown max-w-none">{toolContent}</div>
|
<div className="text-xs prose prose-xs dark:prose-invert chat-markdown max-w-none">
|
||||||
|
{toolContent}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, []);
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// Handle tool clicks
|
// Handle tool clicks
|
||||||
const handleToolClick = useCallback((clickedAssistantMessageId: string | null, clickedToolName: string) => {
|
const handleToolClick = useCallback(
|
||||||
|
(clickedAssistantMessageId: string | null, clickedToolName: string) => {
|
||||||
// Explicitly ignore ask tags from opening the side panel
|
// Explicitly ignore ask tags from opening the side panel
|
||||||
if (clickedToolName === 'ask') {
|
if (clickedToolName === 'ask') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clickedAssistantMessageId) {
|
if (!clickedAssistantMessageId) {
|
||||||
console.warn("Clicked assistant message ID is null. Cannot open side panel.");
|
console.warn(
|
||||||
toast.warning("Cannot view details: Assistant message ID is missing.");
|
'Clicked assistant message ID is null. Cannot open side panel.',
|
||||||
|
);
|
||||||
|
toast.warning('Cannot view details: Assistant message ID is missing.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -501,10 +545,14 @@ export default function ThreadPage({
|
||||||
setCurrentToolIndex(toolIndex);
|
setCurrentToolIndex(toolIndex);
|
||||||
setIsSidePanelOpen(true);
|
setIsSidePanelOpen(true);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Could not find matching tool call for message ID: ${clickedAssistantMessageId}`);
|
console.warn(
|
||||||
toast.info("Could not find details for this tool call.");
|
`Could not find matching tool call for message ID: ${clickedAssistantMessageId}`,
|
||||||
|
);
|
||||||
|
toast.info('Could not find details for this tool call.');
|
||||||
}
|
}
|
||||||
}, [messageToToolIndex]);
|
},
|
||||||
|
[messageToToolIndex],
|
||||||
|
);
|
||||||
|
|
||||||
const handleOpenFileViewer = useCallback((filePath?: string) => {
|
const handleOpenFileViewer = useCallback((filePath?: string) => {
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
|
@ -523,7 +571,7 @@ export default function ThreadPage({
|
||||||
toolCalls,
|
toolCalls,
|
||||||
setCurrentToolIndex,
|
setCurrentToolIndex,
|
||||||
onFileViewerOpen: handleOpenFileViewer,
|
onFileViewerOpen: handleOpenFileViewer,
|
||||||
projectName: projectName || 'Shared Conversation'
|
projectName: projectName || 'Shared Conversation',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract the playback state and functions
|
// Extract the playback state and functions
|
||||||
|
@ -534,7 +582,7 @@ export default function ThreadPage({
|
||||||
renderWelcomeOverlay,
|
renderWelcomeOverlay,
|
||||||
togglePlayback,
|
togglePlayback,
|
||||||
resetPlayback,
|
resetPlayback,
|
||||||
skipToEnd
|
skipToEnd,
|
||||||
} = playbackController;
|
} = playbackController;
|
||||||
|
|
||||||
// Connect playbackState to component state
|
// Connect playbackState to component state
|
||||||
|
@ -553,7 +601,8 @@ export default function ThreadPage({
|
||||||
|
|
||||||
// Scroll button visibility
|
// Scroll button visibility
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!latestMessageRef.current || playbackState.visibleMessages.length === 0) return;
|
if (!latestMessageRef.current || playbackState.visibleMessages.length === 0)
|
||||||
|
return;
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
([entry]) => setShowScrollButton(!entry?.isIntersecting),
|
([entry]) => setShowScrollButton(!entry?.isIntersecting),
|
||||||
{ root: messagesContainerRef.current, threshold: 0.1 },
|
{ root: messagesContainerRef.current, threshold: 0.1 },
|
||||||
|
@ -563,13 +612,21 @@ export default function ThreadPage({
|
||||||
}, [playbackState.visibleMessages, streamingText, currentToolCall]);
|
}, [playbackState.visibleMessages, streamingText, currentToolCall]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(`[PAGE] 🔄 Page AgentStatus: ${agentStatus}, Hook Status: ${streamHookStatus}, Target RunID: ${agentRunId || 'none'}, Hook RunID: ${currentHookRunId || 'none'}`);
|
console.log(
|
||||||
|
`[PAGE] 🔄 Page AgentStatus: ${agentStatus}, Hook Status: ${streamHookStatus}, Target RunID: ${agentRunId || 'none'}, Hook RunID: ${currentHookRunId || 'none'}`,
|
||||||
|
);
|
||||||
|
|
||||||
// If the stream hook reports completion/stopping but our UI hasn't updated
|
// If the stream hook reports completion/stopping but our UI hasn't updated
|
||||||
if ((streamHookStatus === 'completed' || streamHookStatus === 'stopped' ||
|
if (
|
||||||
streamHookStatus === 'agent_not_running' || streamHookStatus === 'error') &&
|
(streamHookStatus === 'completed' ||
|
||||||
(agentStatus === 'running' || agentStatus === 'connecting')) {
|
streamHookStatus === 'stopped' ||
|
||||||
console.log('[PAGE] Detected hook completed but UI still shows running, updating status');
|
streamHookStatus === 'agent_not_running' ||
|
||||||
|
streamHookStatus === 'error') &&
|
||||||
|
(agentStatus === 'running' || agentStatus === 'connecting')
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
'[PAGE] Detected hook completed but UI still shows running, updating status',
|
||||||
|
);
|
||||||
setAgentStatus('idle');
|
setAgentStatus('idle');
|
||||||
setAgentRunId(null);
|
setAgentRunId(null);
|
||||||
setAutoOpenedPanel(false);
|
setAutoOpenedPanel(false);
|
||||||
|
@ -577,11 +634,14 @@ export default function ThreadPage({
|
||||||
}, [agentStatus, streamHookStatus, agentRunId, currentHookRunId]);
|
}, [agentStatus, streamHookStatus, agentRunId, currentHookRunId]);
|
||||||
|
|
||||||
// Auto-scroll function for use throughout the component
|
// Auto-scroll function for use throughout the component
|
||||||
const autoScrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
|
const autoScrollToBottom = useCallback(
|
||||||
|
(behavior: ScrollBehavior = 'smooth') => {
|
||||||
if (!userHasScrolled && messagesEndRef.current) {
|
if (!userHasScrolled && messagesEndRef.current) {
|
||||||
messagesEndRef.current.scrollIntoView({ behavior });
|
messagesEndRef.current.scrollIntoView({ behavior });
|
||||||
}
|
}
|
||||||
}, [userHasScrolled]);
|
},
|
||||||
|
[userHasScrolled],
|
||||||
|
);
|
||||||
|
|
||||||
// Very direct approach to update the tool index during message playback
|
// Very direct approach to update the tool index during message playback
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -625,7 +685,9 @@ export default function ThreadPage({
|
||||||
const assistantId = metadata.assistant_message_id;
|
const assistantId = metadata.assistant_message_id;
|
||||||
|
|
||||||
if (assistantId) {
|
if (assistantId) {
|
||||||
console.log(`Looking for tool panel for assistant message ${assistantId}`);
|
console.log(
|
||||||
|
`Looking for tool panel for assistant message ${assistantId}`,
|
||||||
|
);
|
||||||
|
|
||||||
// Scan for matching tool call
|
// Scan for matching tool call
|
||||||
for (let j = 0; j < toolCalls.length; j++) {
|
for (let j = 0; j < toolCalls.length; j++) {
|
||||||
|
@ -648,7 +710,9 @@ export default function ThreadPage({
|
||||||
|
|
||||||
// Loading skeleton UI
|
// Loading skeleton UI
|
||||||
if (isLoading && !initialLoadCompleted.current) {
|
if (isLoading && !initialLoadCompleted.current) {
|
||||||
return <ThreadSkeleton isSidePanelOpen={isSidePanelOpen} showHeader={true} />;
|
return (
|
||||||
|
<ThreadSkeleton isSidePanelOpen={isSidePanelOpen} showHeader={true} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error state UI
|
// Error state UI
|
||||||
|
@ -658,7 +722,7 @@ export default function ThreadPage({
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[650px]' : ''}`}
|
className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[650px]' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 relative z-[100]">
|
||||||
<div className="flex h-14 items-center gap-4 px-4">
|
<div className="flex h-14 items-center gap-4 px-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<span className="text-foreground font-medium">
|
<span className="text-foreground font-medium">
|
||||||
|
@ -671,7 +735,10 @@ export default function ThreadPage({
|
||||||
<div className="flex w-full max-w-md flex-col items-center gap-4 rounded-lg border bg-card p-6 text-center">
|
<div className="flex w-full max-w-md flex-col items-center gap-4 rounded-lg border bg-card p-6 text-center">
|
||||||
<h2 className="text-lg font-semibold text-destructive">Error</h2>
|
<h2 className="text-lg font-semibold text-destructive">Error</h2>
|
||||||
<p className="text-sm text-muted-foreground">{error}</p>
|
<p className="text-sm text-muted-foreground">{error}</p>
|
||||||
<button className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground" onClick={() => router.push('/')}>
|
<button
|
||||||
|
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground"
|
||||||
|
onClick={() => router.push('/')}
|
||||||
|
>
|
||||||
Back to Home
|
Back to Home
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -701,7 +768,7 @@ export default function ThreadPage({
|
||||||
streamingText={playbackState.streamingText}
|
streamingText={playbackState.streamingText}
|
||||||
isStreamingText={playbackState.isStreamingText}
|
isStreamingText={playbackState.isStreamingText}
|
||||||
currentToolCall={playbackState.currentToolCall}
|
currentToolCall={playbackState.currentToolCall}
|
||||||
sandboxId={sandboxId || ""}
|
sandboxId={sandboxId || ''}
|
||||||
project={project}
|
project={project}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button';
|
||||||
import { Play, Pause, ArrowDown, FileText, Info } from 'lucide-react';
|
import { Play, Pause, ArrowDown, FileText, Info } from 'lucide-react';
|
||||||
import { UnifiedMessage } from '@/components/thread/types';
|
import { UnifiedMessage } from '@/components/thread/types';
|
||||||
import { safeJsonParse } from '@/components/thread/utils';
|
import { safeJsonParse } from '@/components/thread/utils';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
// Define the set of tags whose raw XML should be hidden during streaming
|
// Define the set of tags whose raw XML should be hidden during streaming
|
||||||
const HIDE_STREAMING_XML_TAGS = new Set([
|
const HIDE_STREAMING_XML_TAGS = new Set([
|
||||||
|
@ -29,7 +30,7 @@ const HIDE_STREAMING_XML_TAGS = new Set([
|
||||||
'ask',
|
'ask',
|
||||||
'complete',
|
'complete',
|
||||||
'crawl-webpage',
|
'crawl-webpage',
|
||||||
'web-search'
|
'web-search',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export interface PlaybackControlsProps {
|
export interface PlaybackControlsProps {
|
||||||
|
@ -70,16 +71,16 @@ export const PlaybackControls = ({
|
||||||
toolCalls,
|
toolCalls,
|
||||||
setCurrentToolIndex,
|
setCurrentToolIndex,
|
||||||
onFileViewerOpen,
|
onFileViewerOpen,
|
||||||
projectName = 'Shared Conversation'
|
projectName = 'Shared Conversation',
|
||||||
}: PlaybackControlsProps): PlaybackController => {
|
}: PlaybackControlsProps): PlaybackController => {
|
||||||
const [playbackState, setPlaybackState] = useState<PlaybackState>({
|
const [playbackState, setPlaybackState] = useState<PlaybackState>({
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
currentMessageIndex: 0,
|
currentMessageIndex: 0,
|
||||||
visibleMessages: [],
|
visibleMessages: [],
|
||||||
streamingText: "",
|
streamingText: '',
|
||||||
isStreamingText: false,
|
isStreamingText: false,
|
||||||
currentToolCall: null,
|
currentToolCall: null,
|
||||||
toolPlaybackIndex: -1
|
toolPlaybackIndex: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract state variables for easier access
|
// Extract state variables for easier access
|
||||||
|
@ -90,18 +91,18 @@ export const PlaybackControls = ({
|
||||||
streamingText,
|
streamingText,
|
||||||
isStreamingText,
|
isStreamingText,
|
||||||
currentToolCall,
|
currentToolCall,
|
||||||
toolPlaybackIndex
|
toolPlaybackIndex,
|
||||||
} = playbackState;
|
} = playbackState;
|
||||||
|
|
||||||
// Helper function to update playback state
|
// Helper function to update playback state
|
||||||
const updatePlaybackState = useCallback((updates: Partial<PlaybackState>) => {
|
const updatePlaybackState = useCallback((updates: Partial<PlaybackState>) => {
|
||||||
setPlaybackState(prev => ({ ...prev, ...updates }));
|
setPlaybackState((prev) => ({ ...prev, ...updates }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Define togglePlayback and resetPlayback functions
|
// Define togglePlayback and resetPlayback functions
|
||||||
const togglePlayback = useCallback(() => {
|
const togglePlayback = useCallback(() => {
|
||||||
updatePlaybackState({
|
updatePlaybackState({
|
||||||
isPlaying: !isPlaying
|
isPlaying: !isPlaying,
|
||||||
});
|
});
|
||||||
|
|
||||||
// When starting playback, show the side panel
|
// When starting playback, show the side panel
|
||||||
|
@ -115,10 +116,10 @@ export const PlaybackControls = ({
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
currentMessageIndex: 0,
|
currentMessageIndex: 0,
|
||||||
visibleMessages: [],
|
visibleMessages: [],
|
||||||
streamingText: "",
|
streamingText: '',
|
||||||
isStreamingText: false,
|
isStreamingText: false,
|
||||||
currentToolCall: null,
|
currentToolCall: null,
|
||||||
toolPlaybackIndex: -1
|
toolPlaybackIndex: -1,
|
||||||
});
|
});
|
||||||
}, [updatePlaybackState]);
|
}, [updatePlaybackState]);
|
||||||
|
|
||||||
|
@ -127,10 +128,10 @@ export const PlaybackControls = ({
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
currentMessageIndex: messages.length,
|
currentMessageIndex: messages.length,
|
||||||
visibleMessages: messages,
|
visibleMessages: messages,
|
||||||
streamingText: "",
|
streamingText: '',
|
||||||
isStreamingText: false,
|
isStreamingText: false,
|
||||||
currentToolCall: null,
|
currentToolCall: null,
|
||||||
toolPlaybackIndex: toolCalls.length - 1
|
toolPlaybackIndex: toolCalls.length - 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (toolCalls.length > 0) {
|
if (toolCalls.length > 0) {
|
||||||
|
@ -139,22 +140,31 @@ export const PlaybackControls = ({
|
||||||
onToggleSidePanel();
|
onToggleSidePanel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [messages, toolCalls, isSidePanelOpen, onToggleSidePanel, setCurrentToolIndex, updatePlaybackState]);
|
}, [
|
||||||
|
messages,
|
||||||
|
toolCalls,
|
||||||
|
isSidePanelOpen,
|
||||||
|
onToggleSidePanel,
|
||||||
|
setCurrentToolIndex,
|
||||||
|
updatePlaybackState,
|
||||||
|
]);
|
||||||
|
|
||||||
// Streaming text function
|
// Streaming text function
|
||||||
const streamText = useCallback((text: string, onComplete: () => void) => {
|
const streamText = useCallback(
|
||||||
|
(text: string, onComplete: () => void) => {
|
||||||
if (!text || !isPlaying) {
|
if (!text || !isPlaying) {
|
||||||
onComplete();
|
onComplete();
|
||||||
return () => { };
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePlaybackState({
|
updatePlaybackState({
|
||||||
isStreamingText: true,
|
isStreamingText: true,
|
||||||
streamingText: ""
|
streamingText: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define regex to find tool calls in text
|
// Define regex to find tool calls in text
|
||||||
const toolCallRegex = /<([a-zA-Z\-_]+)(?:\s+[^>]*)?>(?:[\s\S]*?)<\/\1>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/g;
|
const toolCallRegex =
|
||||||
|
/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>(?:[\s\S]*?)<\/\1>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/g;
|
||||||
|
|
||||||
// Split text into chunks (handling tool calls as special chunks)
|
// Split text into chunks (handling tool calls as special chunks)
|
||||||
const chunks: { text: string; isTool: boolean; toolName?: string }[] = [];
|
const chunks: { text: string; isTool: boolean; toolName?: string }[] = [];
|
||||||
|
@ -166,7 +176,7 @@ export const PlaybackControls = ({
|
||||||
if (match.index > lastIndex) {
|
if (match.index > lastIndex) {
|
||||||
chunks.push({
|
chunks.push({
|
||||||
text: text.substring(lastIndex, match.index),
|
text: text.substring(lastIndex, match.index),
|
||||||
isTool: false
|
isTool: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +185,7 @@ export const PlaybackControls = ({
|
||||||
chunks.push({
|
chunks.push({
|
||||||
text: match[0],
|
text: match[0],
|
||||||
isTool: true,
|
isTool: true,
|
||||||
toolName
|
toolName,
|
||||||
});
|
});
|
||||||
|
|
||||||
lastIndex = toolCallRegex.lastIndex;
|
lastIndex = toolCallRegex.lastIndex;
|
||||||
|
@ -185,7 +195,7 @@ export const PlaybackControls = ({
|
||||||
if (lastIndex < text.length) {
|
if (lastIndex < text.length) {
|
||||||
chunks.push({
|
chunks.push({
|
||||||
text: text.substring(lastIndex),
|
text: text.substring(lastIndex),
|
||||||
isTool: false
|
isTool: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,7 +214,7 @@ export const PlaybackControls = ({
|
||||||
if (chunkIndex >= chunks.length) {
|
if (chunkIndex >= chunks.length) {
|
||||||
// All chunks processed, we're done
|
// All chunks processed, we're done
|
||||||
updatePlaybackState({
|
updatePlaybackState({
|
||||||
isStreamingText: false
|
isStreamingText: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update visible messages with the complete message
|
// Update visible messages with the complete message
|
||||||
|
@ -214,12 +224,15 @@ export const PlaybackControls = ({
|
||||||
if (lastMessage?.message_id === currentMessage.message_id) {
|
if (lastMessage?.message_id === currentMessage.message_id) {
|
||||||
// Replace the streaming message with the complete one
|
// Replace the streaming message with the complete one
|
||||||
updatePlaybackState({
|
updatePlaybackState({
|
||||||
visibleMessages: [...visibleMessages.slice(0, -1), currentMessage]
|
visibleMessages: [
|
||||||
|
...visibleMessages.slice(0, -1),
|
||||||
|
currentMessage,
|
||||||
|
],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Add the complete message
|
// Add the complete message
|
||||||
updatePlaybackState({
|
updatePlaybackState({
|
||||||
visibleMessages: [...visibleMessages, currentMessage]
|
visibleMessages: [...visibleMessages, currentMessage],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,17 +245,20 @@ export const PlaybackControls = ({
|
||||||
// If this is a tool call chunk and we're at the start of it
|
// If this is a tool call chunk and we're at the start of it
|
||||||
if (currentChunk.isTool && currentIndex === 0) {
|
if (currentChunk.isTool && currentIndex === 0) {
|
||||||
// For tool calls, check if they should be hidden during streaming
|
// For tool calls, check if they should be hidden during streaming
|
||||||
if (currentChunk.toolName && HIDE_STREAMING_XML_TAGS.has(currentChunk.toolName)) {
|
if (
|
||||||
|
currentChunk.toolName &&
|
||||||
|
HIDE_STREAMING_XML_TAGS.has(currentChunk.toolName)
|
||||||
|
) {
|
||||||
// Instead of showing the XML, create a tool call object
|
// Instead of showing the XML, create a tool call object
|
||||||
const toolCall = {
|
const toolCall = {
|
||||||
name: currentChunk.toolName,
|
name: currentChunk.toolName,
|
||||||
arguments: currentChunk.text,
|
arguments: currentChunk.text,
|
||||||
xml_tag_name: currentChunk.toolName
|
xml_tag_name: currentChunk.toolName,
|
||||||
};
|
};
|
||||||
|
|
||||||
updatePlaybackState({
|
updatePlaybackState({
|
||||||
currentToolCall: toolCall,
|
currentToolCall: toolCall,
|
||||||
toolPlaybackIndex: toolPlaybackIndex + 1
|
toolPlaybackIndex: toolPlaybackIndex + 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isSidePanelOpen) {
|
if (!isSidePanelOpen) {
|
||||||
|
@ -273,7 +289,7 @@ export const PlaybackControls = ({
|
||||||
|
|
||||||
// Add more delay for punctuation to make it feel more natural
|
// Add more delay for punctuation to make it feel more natural
|
||||||
const char = currentChunk.text[currentIndex];
|
const char = currentChunk.text[currentIndex];
|
||||||
if (".!?,;:".includes(char)) {
|
if ('.!?,;:'.includes(char)) {
|
||||||
typingDelay = baseDelay + Math.random() * 100 + 50; // Reduced from 300+100 to 100+50ms pause after punctuation
|
typingDelay = baseDelay + Math.random() * 100 + 50; // Reduced from 300+100 to 100+50ms pause after punctuation
|
||||||
} else {
|
} else {
|
||||||
const variableDelay = Math.random() * 5; // Reduced from 15 to 5ms
|
const variableDelay = Math.random() * 5; // Reduced from 15 to 5ms
|
||||||
|
@ -301,11 +317,23 @@ export const PlaybackControls = ({
|
||||||
return () => {
|
return () => {
|
||||||
updatePlaybackState({
|
updatePlaybackState({
|
||||||
isStreamingText: false,
|
isStreamingText: false,
|
||||||
streamingText: ""
|
streamingText: '',
|
||||||
});
|
});
|
||||||
isPaused = true; // Stop processing
|
isPaused = true; // Stop processing
|
||||||
};
|
};
|
||||||
}, [isPlaying, messages, currentMessageIndex, toolPlaybackIndex, setCurrentToolIndex, isSidePanelOpen, onToggleSidePanel, updatePlaybackState, visibleMessages]);
|
},
|
||||||
|
[
|
||||||
|
isPlaying,
|
||||||
|
messages,
|
||||||
|
currentMessageIndex,
|
||||||
|
toolPlaybackIndex,
|
||||||
|
setCurrentToolIndex,
|
||||||
|
isSidePanelOpen,
|
||||||
|
onToggleSidePanel,
|
||||||
|
updatePlaybackState,
|
||||||
|
visibleMessages,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Main playback function
|
// Main playback function
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -322,7 +350,11 @@ export const PlaybackControls = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentMessage = messages[currentMessageIndex];
|
const currentMessage = messages[currentMessageIndex];
|
||||||
console.log(`Playing message ${currentMessageIndex}:`, currentMessage.type, currentMessage.message_id);
|
console.log(
|
||||||
|
`Playing message ${currentMessageIndex}:`,
|
||||||
|
currentMessage.type,
|
||||||
|
currentMessage.message_id,
|
||||||
|
);
|
||||||
|
|
||||||
// If it's an assistant message, stream it
|
// If it's an assistant message, stream it
|
||||||
if (currentMessage.type === 'assistant') {
|
if (currentMessage.type === 'assistant') {
|
||||||
|
@ -348,16 +380,16 @@ export const PlaybackControls = ({
|
||||||
} else {
|
} else {
|
||||||
// For non-assistant messages, just add them to visible messages
|
// For non-assistant messages, just add them to visible messages
|
||||||
updatePlaybackState({
|
updatePlaybackState({
|
||||||
visibleMessages: [...visibleMessages, currentMessage]
|
visibleMessages: [...visibleMessages, currentMessage],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait a moment before showing the next message
|
// Wait a moment before showing the next message
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to the next message
|
// Move to the next message
|
||||||
updatePlaybackState({
|
updatePlaybackState({
|
||||||
currentMessageIndex: currentMessageIndex + 1
|
currentMessageIndex: currentMessageIndex + 1,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -368,7 +400,14 @@ export const PlaybackControls = ({
|
||||||
clearTimeout(playbackTimeout);
|
clearTimeout(playbackTimeout);
|
||||||
if (cleanupStreaming) cleanupStreaming();
|
if (cleanupStreaming) cleanupStreaming();
|
||||||
};
|
};
|
||||||
}, [isPlaying, currentMessageIndex, messages, streamText, updatePlaybackState, visibleMessages]);
|
}, [
|
||||||
|
isPlaying,
|
||||||
|
currentMessageIndex,
|
||||||
|
messages,
|
||||||
|
streamText,
|
||||||
|
updatePlaybackState,
|
||||||
|
visibleMessages,
|
||||||
|
]);
|
||||||
|
|
||||||
// Floating playback controls position based on side panel state
|
// Floating playback controls position based on side panel state
|
||||||
const controlsPositionClass = isSidePanelOpen
|
const controlsPositionClass = isSidePanelOpen
|
||||||
|
@ -376,15 +415,28 @@ export const PlaybackControls = ({
|
||||||
: 'left-1/2 -translate-x-1/2';
|
: 'left-1/2 -translate-x-1/2';
|
||||||
|
|
||||||
// Header with playback controls
|
// Header with playback controls
|
||||||
const renderHeader = useCallback(() => (
|
const renderHeader = useCallback(
|
||||||
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
() => (
|
||||||
|
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 relative z-[50]">
|
||||||
<div className="flex h-14 items-center gap-4 px-4">
|
<div className="flex h-14 items-center gap-4 px-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center justify-center w-6 h-6 rounded-md overflow-hidden bg-primary/10">
|
<div className="flex items-center justify-center w-6 h-6 rounded-md overflow-hidden bg-primary/10">
|
||||||
<img src="/kortix-symbol.svg" alt="Kortix" width={16} height={16} className="object-contain" />
|
<Link href="/">
|
||||||
|
<img
|
||||||
|
src="/kortix-symbol.svg"
|
||||||
|
alt="Kortix"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-foreground">{projectName}</span>
|
<h1>
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{projectName}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
@ -402,9 +454,13 @@ export const PlaybackControls = ({
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={togglePlayback}
|
onClick={togglePlayback}
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
aria-label={isPlaying ? "Pause Replay" : "Play Replay"}
|
aria-label={isPlaying ? 'Pause Replay' : 'Play Replay'}
|
||||||
>
|
>
|
||||||
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
{isPlaying ? (
|
||||||
|
<Pause className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
@ -419,7 +475,7 @@ export const PlaybackControls = ({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={onToggleSidePanel}
|
onClick={onToggleSidePanel}
|
||||||
className={`h-8 w-8 ${isSidePanelOpen ? "text-primary" : ""}`}
|
className={`h-8 w-8 ${isSidePanelOpen ? 'text-primary' : ''}`}
|
||||||
aria-label="Toggle Tool Panel"
|
aria-label="Toggle Tool Panel"
|
||||||
>
|
>
|
||||||
<Info className="h-4 w-4" />
|
<Info className="h-4 w-4" />
|
||||||
|
@ -427,9 +483,20 @@ export const PlaybackControls = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
), [isPlaying, isSidePanelOpen, onFileViewerOpen, onToggleSidePanel, projectName, resetPlayback, togglePlayback]);
|
),
|
||||||
|
[
|
||||||
|
isPlaying,
|
||||||
|
isSidePanelOpen,
|
||||||
|
onFileViewerOpen,
|
||||||
|
onToggleSidePanel,
|
||||||
|
projectName,
|
||||||
|
resetPlayback,
|
||||||
|
togglePlayback,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const renderFloatingControls = useCallback(() => (
|
const renderFloatingControls = useCallback(
|
||||||
|
() => (
|
||||||
<>
|
<>
|
||||||
{messages.length > 0 && (
|
{messages.length > 0 && (
|
||||||
<div
|
<div
|
||||||
|
@ -442,11 +509,21 @@ export const PlaybackControls = ({
|
||||||
onClick={togglePlayback}
|
onClick={togglePlayback}
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
>
|
>
|
||||||
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
{isPlaying ? (
|
||||||
|
<Pause className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex items-center text-xs text-muted-foreground">
|
<div className="flex items-center text-xs text-muted-foreground">
|
||||||
<span>{Math.min(currentMessageIndex + (isStreamingText ? 0 : 1), messages.length)}/{messages.length}</span>
|
<span>
|
||||||
|
{Math.min(
|
||||||
|
currentMessageIndex + (isStreamingText ? 0 : 1),
|
||||||
|
messages.length,
|
||||||
|
)}
|
||||||
|
/{messages.length}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -470,10 +547,22 @@ export const PlaybackControls = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
), [controlsPositionClass, currentMessageIndex, isPlaying, isStreamingText, messages.length, resetPlayback, skipToEnd, togglePlayback]);
|
),
|
||||||
|
[
|
||||||
|
controlsPositionClass,
|
||||||
|
currentMessageIndex,
|
||||||
|
isPlaying,
|
||||||
|
isStreamingText,
|
||||||
|
messages.length,
|
||||||
|
resetPlayback,
|
||||||
|
skipToEnd,
|
||||||
|
togglePlayback,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// When no messages are displayed yet, show the welcome overlay
|
// When s are displayed yet, show the welcome overlay
|
||||||
const renderWelcomeOverlay = useCallback(() => (
|
const renderWelcomeOverlay = useCallback(
|
||||||
|
() => (
|
||||||
<>
|
<>
|
||||||
{visibleMessages.length === 0 && !streamingText && !currentToolCall && (
|
{visibleMessages.length === 0 && !streamingText && !currentToolCall && (
|
||||||
<div className="fixed inset-0 flex flex-col items-center justify-center">
|
<div className="fixed inset-0 flex flex-col items-center justify-center">
|
||||||
|
@ -484,9 +573,12 @@ export const PlaybackControls = ({
|
||||||
<div className="rounded-full bg-primary/10 backdrop-blur-sm w-12 h-12 mx-auto flex items-center justify-center mb-4">
|
<div className="rounded-full bg-primary/10 backdrop-blur-sm w-12 h-12 mx-auto flex items-center justify-center mb-4">
|
||||||
<Play className="h-5 w-5 text-primary" />
|
<Play className="h-5 w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-medium mb-2 text-white">Watch this agent in action</h3>
|
<h3 className="text-lg font-medium mb-2 text-white">
|
||||||
|
Watch this agent in action
|
||||||
|
</h3>
|
||||||
<p className="text-sm text-white/80 mb-4">
|
<p className="text-sm text-white/80 mb-4">
|
||||||
This is a shared view-only agent run. Click play to replay the entire conversation with realistic timing.
|
This is a shared view-only agent run. Click play to replay the
|
||||||
|
entire conversation with realistic timing.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={togglePlayback}
|
onClick={togglePlayback}
|
||||||
|
@ -501,7 +593,9 @@ export const PlaybackControls = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
), [currentToolCall, streamingText, togglePlayback, visibleMessages.length]);
|
),
|
||||||
|
[currentToolCall, streamingText, togglePlayback, visibleMessages.length],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
playbackState,
|
playbackState,
|
||||||
|
@ -511,7 +605,7 @@ export const PlaybackControls = ({
|
||||||
renderWelcomeOverlay,
|
renderWelcomeOverlay,
|
||||||
togglePlayback,
|
togglePlayback,
|
||||||
resetPlayback,
|
resetPlayback,
|
||||||
skipToEnd
|
skipToEnd,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue