fix: overflow issue and file refetch

This commit is contained in:
Vukasin 2025-05-29 23:06:07 +02:00
parent c14a2e4cb4
commit 65fa837ae1
5 changed files with 160 additions and 86 deletions

View File

@ -417,7 +417,8 @@
margin-top: 0.5em; margin-top: 0.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
padding: 0.75em 1em; padding: 0.75em 1em;
background-color: theme('colors.slate.100'); /* background 95 */
background-color: theme('colors.background/95');
border-radius: 0.375rem; border-radius: 0.375rem;
overflow-x: auto; overflow-x: auto;
font-family: var(--font-mono); font-family: var(--font-mono);
@ -439,7 +440,7 @@
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
font-size: 0.85em; font-size: 0.85em;
font-family: var(--font-mono); font-family: var(--font-mono);
background-color: theme('colors.slate.100'); background-color: theme('colors.background/95');
border-radius: 3px; border-radius: 3px;
word-break: break-word; word-break: break-word;
} }
@ -478,14 +479,14 @@
.dark & { .dark & {
/* Code blocks in dark mode */ /* Code blocks in dark mode */
& pre { & pre {
background-color: theme('colors.zinc.800'); background-color: theme('colors.background/95');
border: 1px solid theme('colors.zinc.700'); /* border: 1px solid theme('colors.zinc.700'); */
} }
& code:not([class*='language-']) { & code:not([class*='language-']) {
background-color: theme('colors.zinc.800'); background-color: theme('colors.background/95');
color: theme('colors.zinc.200'); color: theme('colors.zinc.200');
border: 1px solid theme('colors.zinc.700'); /* border: 1px solid theme('colors.zinc.700'); */
} }
/* Tables in dark mode */ /* Tables in dark mode */

View File

@ -262,7 +262,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
className="flex-1 overflow-y-auto scrollbar-thin scrollbar-track-secondary/0 scrollbar-thumb-primary/10 scrollbar-thumb-rounded-full hover:scrollbar-thumb-primary/10 px-6 py-4 pb-72 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60" className="flex-1 overflow-y-auto scrollbar-thin scrollbar-track-secondary/0 scrollbar-thumb-primary/10 scrollbar-thumb-rounded-full hover:scrollbar-thumb-primary/10 px-6 py-4 pb-72 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
onScroll={handleScroll} onScroll={handleScroll}
> >
<div className="mx-auto max-w-3xl"> <div className="mx-auto max-w-3xl min-w-0">
{displayMessages.length === 0 && !streamingTextContent && !streamingToolCall && {displayMessages.length === 0 && !streamingTextContent && !streamingToolCall &&
!streamingText && !currentToolCall && agentStatus === 'idle' ? ( !streamingText && !currentToolCall && agentStatus === 'idle' ? (
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
@ -271,7 +271,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
</div> </div>
</div> </div>
) : ( ) : (
<div className="space-y-8"> <div className="space-y-8 min-w-0">
{(() => { {(() => {
type MessageGroup = { type MessageGroup = {
@ -379,8 +379,8 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
if (debugMode) { if (debugMode) {
return ( return (
<div key={group.key} className="flex justify-end"> <div key={group.key} className="flex justify-end">
<div className="inline-flex max-w-[85%] rounded-xl bg-primary/10 px-4 py-3"> <div className="flex max-w-[85%] rounded-xl bg-primary/10 px-4 py-3 break-words overflow-hidden">
<pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto"> <pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto min-w-0 flex-1">
{message.content} {message.content}
</pre> </pre>
</div> </div>
@ -402,10 +402,10 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
return ( return (
<div key={group.key} className="flex justify-end"> <div key={group.key} className="flex justify-end">
<div className="inline-flex max-w-[85%] rounded-xl bg-primary/10 px-4 py-3"> <div className="flex max-w-[85%] rounded-xl bg-primary/10 px-4 py-3 break-words overflow-hidden">
<div className="space-y-3"> <div className="space-y-3 min-w-0 flex-1">
{cleanContent && ( {cleanContent && (
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3">{cleanContent}</Markdown> <Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere">{cleanContent}</Markdown>
)} )}
{/* Use the helper function to render user attachments */} {/* Use the helper function to render user attachments */}
@ -422,8 +422,8 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
<KortixLogo /> <KortixLogo />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="inline-flex max-w-[90%] rounded-lg px-4 text-sm"> <div className="flex max-w-[90%] rounded-lg px-4 text-sm break-words overflow-hidden">
<div className="space-y-2"> <div className="space-y-2 min-w-0 flex-1">
{(() => { {(() => {
// In debug mode, just show raw messages content // In debug mode, just show raw messages content
if (debugMode) { if (debugMode) {
@ -487,7 +487,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
elements.push( elements.push(
<div key={msgKey} className={assistantMessageCount > 0 ? "mt-2" : ""}> <div key={msgKey} className={assistantMessageCount > 0 ? "mt-2" : ""}>
<div className="prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3"> <div className="prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-hidden">
{renderedContent} {renderedContent}
</div> </div>
</div> </div>
@ -534,7 +534,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
return ( return (
<> <>
{textBeforeTag && ( {textBeforeTag && (
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3">{textBeforeTag}</Markdown> <Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere">{textBeforeTag}</Markdown>
)} )}
{showCursor && ( {showCursor && (
<span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse" /> <span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse" />
@ -611,7 +611,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
) : ( ) : (
<> <>
{textBeforeTag && ( {textBeforeTag && (
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3">{textBeforeTag}</Markdown> <Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere">{textBeforeTag}</Markdown>
)} )}
{showCursor && ( {showCursor && (
<span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse" /> <span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse" />

View File

@ -12,7 +12,7 @@ export type CodeBlockProps = {
function CodeBlock({ children, className, ...props }: CodeBlockProps) { function CodeBlock({ children, className, ...props }: CodeBlockProps) {
return ( return (
<div className={cn(className)} {...props}> <div className={cn('w-px flex-grow min-w-0 overflow-hidden flex', className)} {...props}>
{children} {children}
</div> </div>
); );
@ -41,6 +41,10 @@ function CodeBlockCode({
useEffect(() => { useEffect(() => {
async function highlight() { async function highlight() {
if (!code || typeof code !== 'string') {
setHighlightedHtml(null);
return;
}
const html = await codeToHtml(code, { const html = await codeToHtml(code, {
lang: language, lang: language,
theme, theme,
@ -60,7 +64,7 @@ function CodeBlockCode({
highlight(); highlight();
}, [code, language, theme]); }, [code, language, theme]);
const classNames = cn('[&_pre]:!bg-background/95 [&_pre]:rounded-lg [&_pre]:p-4', className); const classNames = cn('[&_pre]:!bg-background/95 [&_pre]:rounded-lg [&_pre]:p-4 [&_pre]:!overflow-x-auto [&_pre]:!w-px [&_pre]:!flex-grow [&_pre]:!min-w-0 [&_pre]:!box-border [&_.shiki]:!overflow-x-auto [&_.shiki]:!w-px [&_.shiki]:!flex-grow [&_.shiki]:!min-w-0 [&_code]:!min-w-0 [&_code]:!whitespace-pre', 'w-px flex-grow min-w-0 overflow-hidden flex w-full', className);
// SSR fallback: render plain code if not hydrated yet // SSR fallback: render plain code if not hydrated yet
return highlightedHtml ? ( return highlightedHtml ? (
@ -71,7 +75,7 @@ function CodeBlockCode({
/> />
) : ( ) : (
<div className={classNames} {...props}> <div className={classNames} {...props}>
<pre> <pre className="!overflow-x-auto !w-px !flex-grow !min-w-0 !box-border">
<code>{code}</code> <code>{code}</code>
</pre> </pre>
</div> </div>

View File

@ -47,11 +47,11 @@ const INITIAL_COMPONENTS: Partial<Components> = {
const language = extractLanguage(className); const language = extractLanguage(className);
return ( return (
<CodeBlock className="rounded-md overflow-hidden my-4 border border-zinc-200 dark:border-zinc-800"> <CodeBlock className="rounded-md overflow-hidden my-4 border border-zinc-200 dark:border-zinc-800 max-w-full min-w-0 w-full">
<CodeBlockCode <CodeBlockCode
code={children as string} code={children as string}
language={language} language={language}
className="text-sm overflow-x-auto" className="text-sm"
/> />
</CodeBlock> </CodeBlock>
); );

View File

@ -128,15 +128,33 @@ export function useCachedFile<T = string>(
url.searchParams.append('path', normalizedPath); url.searchParams.append('path', normalizedPath);
// Fetch with authentication // Fetch with authentication
const response = await fetch(url.toString(), { const attemptFetch = async (isRetry: boolean = false): Promise<Response> => {
headers: { const response = await fetch(url.toString(), {
'Authorization': `Bearer ${session?.access_token}`, headers: {
}, 'Authorization': `Bearer ${session?.access_token}`
}); }
});
if (!response.ok) {
const responseText = await response.text();
const errorMessage = `Failed to load file: ${response.status} ${response.statusText}`;
// Check if this is a workspace initialization error and we haven't retried yet
const isWorkspaceNotRunning = responseText.includes('Workspace is not running');
if (isWorkspaceNotRunning && !isRetry) {
console.log(`[FILE CACHE] Workspace not ready, retrying in 2s for ${normalizedPath}`);
await new Promise(resolve => setTimeout(resolve, 2000));
return attemptFetch(true);
}
console.error(`[FILE CACHE] Failed response for ${normalizedPath}: Status ${response.status}`);
throw new Error(errorMessage);
}
return response;
};
if (!response.ok) { const response = await attemptFetch();
throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
}
// Process content based on contentType // Process content based on contentType
let content; let content;
@ -502,13 +520,32 @@ export const FileCache = {
// Properly encode the path parameter for UTF-8 support // Properly encode the path parameter for UTF-8 support
url.searchParams.append('path', normalizedPath); url.searchParams.append('path', normalizedPath);
const response = await fetch(url.toString(), { const attemptFetch = async (isRetry: boolean = false): Promise<Response> => {
headers: { const response = await fetch(url.toString(), {
'Authorization': `Bearer ${token}` headers: {
}, 'Authorization': `Bearer ${token}`
}); },
});
if (!response.ok) {
const responseText = await response.text();
const errorMessage = `Failed to preload file: ${response.status}`;
// Check if this is a workspace initialization error and we haven't retried yet
const isWorkspaceNotRunning = responseText.includes('Workspace is not running');
if (isWorkspaceNotRunning && !isRetry) {
console.log(`[FILE CACHE] Workspace not ready during preload, retrying in 2s for ${normalizedPath}`);
await new Promise(resolve => setTimeout(resolve, 2000));
return attemptFetch(true);
}
throw new Error(errorMessage);
}
return response;
};
if (!response.ok) throw new Error(`Failed to preload file: ${response.status}`); const response = await attemptFetch();
// Determine how to process the content based on file type // Determine how to process the content based on file type
const extension = path.split('.').pop()?.toLowerCase(); const extension = path.split('.').pop()?.toLowerCase();
@ -623,16 +660,33 @@ export async function getCachedFile(
console.log(`[FILE CACHE] Fetching file: ${url.toString()}`); console.log(`[FILE CACHE] Fetching file: ${url.toString()}`);
const response = await fetch(url.toString(), { const attemptFetch = async (isRetry: boolean = false): Promise<Response> => {
headers: { const response = await fetch(url.toString(), {
'Authorization': `Bearer ${options.token}` headers: {
'Authorization': `Bearer ${options.token}`
}
});
if (!response.ok) {
const responseText = await response.text();
const errorMessage = `Failed to load file: ${response.status} ${response.statusText}`;
// Check if this is a workspace initialization error and we haven't retried yet
const isWorkspaceNotRunning = responseText.includes('Workspace is not running');
if (isWorkspaceNotRunning && !isRetry) {
console.log(`[FILE CACHE] Workspace not ready, retrying in 2s for ${normalizedPath}`);
await new Promise(resolve => setTimeout(resolve, 2000));
return attemptFetch(true);
}
console.error(`[FILE CACHE] Failed response for ${normalizedPath}: Status ${response.status}`);
throw new Error(errorMessage);
} }
});
return response;
};
if (!response.ok) { const response = await attemptFetch();
console.error(`[FILE CACHE] Failed response for ${normalizedPath}: Status ${response.status}`);
throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
}
// Process content based on type // Process content based on type
let content; let content;
@ -688,51 +742,66 @@ export async function fetchFileContent(
const requestId = Math.random().toString(36).substring(2, 9); const requestId = Math.random().toString(36).substring(2, 9);
console.log(`[FILE CACHE] Fetching fresh content for ${sandboxId}:${filePath}`); console.log(`[FILE CACHE] Fetching fresh content for ${sandboxId}:${filePath}`);
try { const attemptFetch = async (isRetry: boolean = false): Promise<string | Blob | any> => {
// Prepare the API URL try {
const apiUrl = `${process.env.NEXT_PUBLIC_BACKEND_URL}/sandboxes/${sandboxId}/files/content`; // Prepare the API URL
const url = new URL(apiUrl); const apiUrl = `${process.env.NEXT_PUBLIC_BACKEND_URL}/sandboxes/${sandboxId}/files/content`;
url.searchParams.append('path', filePath); const url = new URL(apiUrl);
url.searchParams.append('path', filePath);
// Set up fetch options
const fetchOptions: RequestInit = {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
};
// Execute fetch
const response = await fetch(url.toString(), fetchOptions);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch file content: ${response.status} ${errorText}`);
}
// CRITICAL: Detect correct response handling based on file type
// Excel files, PDFs and other binary documents should be handled as blobs
const extension = filePath.split('.').pop()?.toLowerCase();
const isBinaryFile = ['xlsx', 'xls', 'docx', 'doc', 'pptx', 'ppt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'zip'].includes(extension || '');
// Handle response based on content type
if (contentType === 'blob' || isBinaryFile) {
const blob = await response.blob();
// Set correct MIME type for known file types // Set up fetch options
if (extension) { const fetchOptions: RequestInit = {
const mimeType = FileCache.getMimeType(filePath); method: 'GET',
if (mimeType && mimeType !== blob.type) { headers: {
// Create a new blob with correct type Authorization: `Bearer ${token}`,
return new Blob([blob], { type: mimeType }); },
} };
// Execute fetch
const response = await fetch(url.toString(), fetchOptions);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch file content: ${response.status} ${errorText}`);
} }
return blob; // CRITICAL: Detect correct response handling based on file type
} else if (contentType === 'json') { // Excel files, PDFs and other binary documents should be handled as blobs
return await response.json(); const extension = filePath.split('.').pop()?.toLowerCase();
} else { const isBinaryFile = ['xlsx', 'xls', 'docx', 'doc', 'pptx', 'ppt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'zip'].includes(extension || '');
return await response.text();
// Handle response based on content type
if (contentType === 'blob' || isBinaryFile) {
const blob = await response.blob();
// Set correct MIME type for known file types
if (extension) {
const mimeType = FileCache.getMimeType(filePath);
if (mimeType && mimeType !== blob.type) {
// Create a new blob with correct type
return new Blob([blob], { type: mimeType });
}
}
return blob;
} else if (contentType === 'json') {
return await response.json();
} else {
return await response.text();
}
} catch (error: any) {
// Check if this is a workspace initialization error and we haven't retried yet
const isWorkspaceNotRunning = error.message?.includes('Workspace is not running');
if (isWorkspaceNotRunning && !isRetry) {
console.log(`[FILE CACHE] Workspace not ready, retrying in 2s for ${filePath}`);
await new Promise(resolve => setTimeout(resolve, 2000));
return attemptFetch(true);
}
throw error;
} }
};
try {
return await attemptFetch();
} catch (error) { } catch (error) {
console.error(`[FILE CACHE] Error fetching file content:`, error); console.error(`[FILE CACHE] Error fetching file content:`, error);
throw error; throw error;