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);
|
||||||
|
@ -27,15 +29,20 @@ export async function generateMetadata({ params }): Promise<Metadata> {
|
||||||
process.env.NEXT_PUBLIC_ENV_MODE === 'LOCAL' ||
|
process.env.NEXT_PUBLIC_ENV_MODE === 'LOCAL' ||
|
||||||
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 =
|
||||||
const ogImage = isDevelopment
|
projectData.description ||
|
||||||
? `${process.env.NEXT_PUBLIC_URL}/share-page/og-fallback.png`
|
'Replay this Agent conversation on Kortix Suna';
|
||||||
: `${process.env.NEXT_PUBLIC_URL}/api/share-page/og-image?title=${projectData.name}`;
|
const ogImage = isDevelopment
|
||||||
|
? `${process.env.NEXT_PUBLIC_URL}/share-page/og-fallback.png`
|
||||||
|
: `${process.env.NEXT_PUBLIC_URL}/api/share-page/og-image?title=${projectData.name}`;
|
||||||
|
|
||||||
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,34 +171,37 @@ export default function ThreadPage({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleStreamStatusChange = useCallback((hookStatus: string) => {
|
const handleStreamStatusChange = useCallback(
|
||||||
console.log(`[PAGE] Hook status changed: ${hookStatus}`);
|
(hookStatus: string) => {
|
||||||
switch (hookStatus) {
|
console.log(`[PAGE] Hook status changed: ${hookStatus}`);
|
||||||
case 'idle':
|
switch (hookStatus) {
|
||||||
case 'completed':
|
case 'idle':
|
||||||
case 'stopped':
|
case 'completed':
|
||||||
case 'agent_not_running':
|
case 'stopped':
|
||||||
setAgentStatus('idle');
|
case 'agent_not_running':
|
||||||
setAgentRunId(null);
|
|
||||||
// Reset auto-opened state when agent completes to trigger tool detection
|
|
||||||
setAutoOpenedPanel(false);
|
|
||||||
break;
|
|
||||||
case 'connecting':
|
|
||||||
setAgentStatus('connecting');
|
|
||||||
break;
|
|
||||||
case 'streaming':
|
|
||||||
setAgentStatus('running');
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
setAgentStatus('error');
|
|
||||||
// Handle errors by going back to idle state after a short delay
|
|
||||||
setTimeout(() => {
|
|
||||||
setAgentStatus('idle');
|
setAgentStatus('idle');
|
||||||
setAgentRunId(null);
|
setAgentRunId(null);
|
||||||
}, 3000);
|
// Reset auto-opened state when agent completes to trigger tool detection
|
||||||
break;
|
setAutoOpenedPanel(false);
|
||||||
}
|
break;
|
||||||
}, [threadId]);
|
case 'connecting':
|
||||||
|
setAgentStatus('connecting');
|
||||||
|
break;
|
||||||
|
case 'streaming':
|
||||||
|
setAgentStatus('running');
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
setAgentStatus('error');
|
||||||
|
// Handle errors by going back to idle state after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
setAgentStatus('idle');
|
||||||
|
setAgentRunId(null);
|
||||||
|
}, 3000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[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,63 +475,84 @@ 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(
|
||||||
if (!toolContent) return null;
|
(toolContent?: string, isSuccess?: boolean) => {
|
||||||
|
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
|
||||||
? 'bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300'
|
</div>
|
||||||
: 'bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-300'
|
<div
|
||||||
}`}>
|
className={`px-2 py-0.5 rounded-full text-xs ${
|
||||||
{isSuccess ? 'Success' : 'Failed'}
|
isSuccess
|
||||||
|
? '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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isSuccess ? 'Success' : 'Failed'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
[],
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle tool clicks
|
// Handle tool clicks
|
||||||
const handleToolClick = useCallback((clickedAssistantMessageId: string | null, clickedToolName: string) => {
|
const handleToolClick = useCallback(
|
||||||
// Explicitly ignore ask tags from opening the side panel
|
(clickedAssistantMessageId: string | null, clickedToolName: string) => {
|
||||||
if (clickedToolName === 'ask') {
|
// Explicitly ignore ask tags from opening the side panel
|
||||||
return;
|
if (clickedToolName === 'ask') {
|
||||||
}
|
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.',
|
||||||
return;
|
);
|
||||||
}
|
toast.warning('Cannot view details: Assistant message ID is missing.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Reset user closed state when explicitly clicking a tool
|
// Reset user closed state when explicitly clicking a tool
|
||||||
userClosedPanelRef.current = false;
|
userClosedPanelRef.current = false;
|
||||||
|
|
||||||
// Direct mapping using the message-to-tool-index map
|
// Direct mapping using the message-to-tool-index map
|
||||||
const toolIndex = messageToToolIndex[clickedAssistantMessageId];
|
const toolIndex = messageToToolIndex[clickedAssistantMessageId];
|
||||||
|
|
||||||
if (toolIndex !== undefined) {
|
if (toolIndex !== undefined) {
|
||||||
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}`,
|
||||||
}
|
);
|
||||||
}, [messageToToolIndex]);
|
toast.info('Could not find details for this tool call.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[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(
|
||||||
if (!userHasScrolled && messagesEndRef.current) {
|
(behavior: ScrollBehavior = 'smooth') => {
|
||||||
messagesEndRef.current.scrollIntoView({ behavior });
|
if (!userHasScrolled && messagesEndRef.current) {
|
||||||
}
|
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(() => {
|
||||||
|
@ -613,7 +673,7 @@ export default function ThreadPage({
|
||||||
// Skip if not playing or no messages
|
// Skip if not playing or no messages
|
||||||
if (!isPlaying || messages.length === 0 || currentMessageIndex <= 0) return;
|
if (!isPlaying || messages.length === 0 || currentMessageIndex <= 0) return;
|
||||||
|
|
||||||
// Get all messages up to the current index
|
// Get all messages up to the current index
|
||||||
const currentMessages = messages.slice(0, currentMessageIndex);
|
const currentMessages = messages.slice(0, currentMessageIndex);
|
||||||
|
|
||||||
// Find the most recent tool message to determine which panel to show
|
// Find the most recent tool message to determine which panel to show
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue