Revert "Merge pull request #1335 from Toddyclipsgg/diff-view-v2"
Some checks failed
Docker Publish / docker-build-publish (push) Has been cancelled
Update Stable Branch / prepare-release (push) Has been cancelled

This reverts commit 871aefbe83, reversing
changes made to 8c72ed76b3.
This commit is contained in:
KevIsDev
2025-02-21 15:01:09 +00:00
parent 871aefbe83
commit bffb8a2a90
16 changed files with 384 additions and 1357 deletions

View File

@@ -34,7 +34,6 @@ import ChatAlert from './ChatAlert';
import type { ModelInfo } from '~/lib/modules/llm/types';
import ProgressCompilation from './ProgressCompilation';
import type { ProgressAnnotation } from '~/types/context';
import type { ActionRunner } from '~/lib/runtime/action-runner';
import { LOCAL_PROVIDERS } from '~/lib/stores/settings';
const TEXTAREA_MIN_HEIGHT = 76;
@@ -69,7 +68,6 @@ interface BaseChatProps {
actionAlert?: ActionAlert;
clearAlert?: () => void;
data?: JSONValue[] | undefined;
actionRunner?: ActionRunner;
}
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
@@ -104,7 +102,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
actionAlert,
clearAlert,
data,
actionRunner,
},
ref,
) => {
@@ -307,7 +304,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
data-chat-visible={showChat}
>
<ClientOnly>{() => <Menu />}</ClientOnly>
<div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
<div className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
<div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
{!chatStarted && (
<div id="intro" className="mt-[16vh] max-w-chat mx-auto text-center px-4 lg:px-0">
@@ -321,39 +318,40 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
)}
<div
className={classNames('pt-6 px-2 sm:px-6', {
'h-full flex flex-col': chatStarted,
'h-full flex flex-col pb-4 overflow-y-auto': chatStarted,
})}
ref={scrollRef}
>
<ClientOnly>
{() => {
return chatStarted ? (
<Messages
ref={messageRef}
className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
messages={messages}
isStreaming={isStreaming}
/>
<div className="flex-1 w-full max-w-chat pb-6 mx-auto z-1">
<Messages
ref={messageRef}
className="flex flex-col "
messages={messages}
isStreaming={isStreaming}
/>
</div>
) : null;
}}
</ClientOnly>
<div
className={classNames('flex flex-col gap-4 w-full max-w-chat mx-auto z-prompt mb-6', {
className={classNames('flex flex-col gap-4 w-full max-w-chat mx-auto z-prompt', {
'sticky bottom-2': chatStarted,
'position-absolute': chatStarted,
})}
>
<div className="bg-bolt-elements-background-depth-2">
{actionAlert && (
<ChatAlert
alert={actionAlert}
clearAlert={() => clearAlert?.()}
postMessage={(message) => {
sendMessage?.({} as any, message);
clearAlert?.();
}}
/>
)}
</div>
{actionAlert && (
<ChatAlert
alert={actionAlert}
clearAlert={() => clearAlert?.()}
postMessage={(message) => {
sendMessage?.({} as any, message);
clearAlert?.();
}}
/>
)}
{progressAnnotations && <ProgressCompilation data={progressAnnotations} />}
<div
className={classNames(
@@ -587,15 +585,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
</div>
</div>
</div>
<div className="flex flex-col justify-center gap-5">
{!chatStarted && (
{!chatStarted && (
<div className="flex flex-col justify-center mt-6 gap-5">
<div className="flex justify-center gap-2">
{ImportButtons(importChat)}
<GitCloneButton importChat={importChat} />
<div className="flex items-center gap-2">
{ImportButtons(importChat)}
<GitCloneButton importChat={importChat} className="min-w-[120px]" />
</div>
</div>
)}
{!chatStarted &&
ExamplePrompts((event, messageInput) => {
{ExamplePrompts((event, messageInput) => {
if (isStreaming) {
handleStop?.();
return;
@@ -603,18 +602,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
handleSendMessage?.(event, messageInput);
})}
{!chatStarted && <StarterTemplates />}
</div>
</div>
<ClientOnly>
{() => (
<Workbench
actionRunner={actionRunner ?? ({} as ActionRunner)}
chatStarted={chatStarted}
isStreaming={isStreaming}
/>
<StarterTemplates />
</div>
)}
</ClientOnly>
</div>
<ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
</div>
</div>
);

View File

@@ -9,11 +9,10 @@ interface SliderOption<T> {
text: string;
}
export type SliderOptions<T> = {
left: { value: T; text: string };
middle?: { value: T; text: string };
right: { value: T; text: string };
};
export interface SliderOptions<T> {
left: SliderOption<T>;
right: SliderOption<T>;
}
interface SliderProps<T> {
selected: T;
@@ -22,23 +21,14 @@ interface SliderProps<T> {
}
export const Slider = genericMemo(<T,>({ selected, options, setSelected }: SliderProps<T>) => {
const hasMiddle = !!options.middle;
const isLeftSelected = hasMiddle ? selected === options.left.value : selected === options.left.value;
const isMiddleSelected = hasMiddle && options.middle ? selected === options.middle.value : false;
const isLeftSelected = selected === options.left.value;
return (
<div className="flex items-center flex-wrap shrink-0 gap-1 bg-bolt-elements-background-depth-1 overflow-hidden rounded-full p-1">
<SliderButton selected={isLeftSelected} setSelected={() => setSelected?.(options.left.value)}>
{options.left.text}
</SliderButton>
{options.middle && (
<SliderButton selected={isMiddleSelected} setSelected={() => setSelected?.(options.middle!.value)}>
{options.middle.text}
</SliderButton>
)}
<SliderButton selected={!isLeftSelected && !isMiddleSelected} setSelected={() => setSelected?.(options.right.value)}>
<SliderButton selected={!isLeftSelected} setSelected={() => setSelected?.(options.right.value)}>
{options.right.text}
</SliderButton>
</div>

View File

@@ -1,510 +0,0 @@
import { memo, useMemo, useState, useEffect, useCallback } from 'react';
import { useStore } from '@nanostores/react';
import { workbenchStore } from '~/lib/stores/workbench';
import type { FileMap } from '~/lib/stores/files';
import type { EditorDocument } from '~/components/editor/codemirror/CodeMirrorEditor';
import { diffLines, type Change } from 'diff';
import { getHighlighter } from 'shiki';
import '~/styles/diff-view.css';
import { diffFiles, extractRelativePath } from '~/utils/diff';
import { ActionRunner } from '~/lib/runtime/action-runner';
import type { FileHistory } from '~/types/actions';
import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension';
interface CodeComparisonProps {
beforeCode: string;
afterCode: string;
language: string;
filename: string;
lightTheme: string;
darkTheme: string;
}
interface DiffBlock {
lineNumber: number;
content: string;
type: 'added' | 'removed' | 'unchanged';
correspondingLine?: number;
}
interface FullscreenButtonProps {
onClick: () => void;
isFullscreen: boolean;
}
const FullscreenButton = memo(({ onClick, isFullscreen }: FullscreenButtonProps) => (
<button
onClick={onClick}
className="ml-4 p-1 rounded hover:bg-bolt-elements-background-depth-3 text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary transition-colors"
title={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
>
<div className={isFullscreen ? "i-ph:corners-in" : "i-ph:corners-out"} />
</button>
));
const FullscreenOverlay = memo(({ isFullscreen, children }: { isFullscreen: boolean; children: React.ReactNode }) => {
if (!isFullscreen) return <>{children}</>;
return (
<div className="fixed inset-0 z-[9999] bg-black/50 flex items-center justify-center p-6">
<div className="w-full h-full max-w-[90vw] max-h-[90vh] bg-bolt-elements-background-depth-2 rounded-lg border border-bolt-elements-borderColor shadow-xl overflow-hidden">
{children}
</div>
</div>
);
});
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
const BINARY_REGEX = /[\x00-\x08\x0E-\x1F]/;
const isBinaryFile = (content: string) => {
return content.length > MAX_FILE_SIZE || BINARY_REGEX.test(content);
};
const processChanges = (beforeCode: string, afterCode: string) => {
try {
if (isBinaryFile(beforeCode) || isBinaryFile(afterCode)) {
return {
beforeLines: [],
afterLines: [],
hasChanges: false,
lineChanges: { before: new Set(), after: new Set() },
unifiedBlocks: [],
isBinary: true
};
}
// Normalizar quebras de linha para evitar falsos positivos
const normalizedBefore = beforeCode.replace(/\r\n/g, '\n').trim();
const normalizedAfter = afterCode.replace(/\r\n/g, '\n').trim();
// Se os conteúdos são idênticos após normalização, não há mudanças
if (normalizedBefore === normalizedAfter) {
return {
beforeLines: normalizedBefore.split('\n'),
afterLines: normalizedAfter.split('\n'),
hasChanges: false,
lineChanges: { before: new Set(), after: new Set() },
unifiedBlocks: []
};
}
// Processar as diferenças com configurações mais precisas
const changes = diffLines(normalizedBefore, normalizedAfter, {
newlineIsToken: true,
ignoreWhitespace: false,
ignoreCase: false
});
// Mapear as mudanças com mais precisão
const beforeLines = normalizedBefore.split('\n');
const afterLines = normalizedAfter.split('\n');
const lineChanges = {
before: new Set<number>(),
after: new Set<number>()
};
let beforeLineNumber = 0;
let afterLineNumber = 0;
const unifiedBlocks = changes.map(change => {
const lines = change.value.split('\n').filter(line => line.length > 0);
if (change.added) {
lines.forEach((_, i) => lineChanges.after.add(afterLineNumber + i));
const block = lines.map((line, i) => ({
lineNumber: afterLineNumber + i,
content: line,
type: 'added' as const
}));
afterLineNumber += lines.length;
return block;
}
if (change.removed) {
lines.forEach((_, i) => lineChanges.before.add(beforeLineNumber + i));
const block = lines.map((line, i) => ({
lineNumber: beforeLineNumber + i,
content: line,
type: 'removed' as const
}));
beforeLineNumber += lines.length;
return block;
}
const block = lines.map((line, i) => ({
lineNumber: afterLineNumber + i,
content: line,
type: 'unchanged' as const,
correspondingLine: beforeLineNumber + i
}));
beforeLineNumber += lines.length;
afterLineNumber += lines.length;
return block;
}).flat();
return {
beforeLines,
afterLines,
hasChanges: lineChanges.before.size > 0 || lineChanges.after.size > 0,
lineChanges,
unifiedBlocks,
isBinary: false
};
} catch (error) {
console.error('Error processing changes:', error);
return {
beforeLines: [],
afterLines: [],
hasChanges: false,
lineChanges: { before: new Set(), after: new Set() },
unifiedBlocks: [],
error: true,
isBinary: false
};
}
};
const lineNumberStyles = "w-12 shrink-0 pl-2 py-0.5 text-left font-mono text-bolt-elements-textTertiary border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1";
const lineContentStyles = "px-4 py-0.5 font-mono whitespace-pre flex-1 group-hover:bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary";
const renderContentWarning = (type: 'binary' | 'error') => (
<div className="h-full flex items-center justify-center p-4">
<div className="text-center text-bolt-elements-textTertiary">
<div className={`i-ph:${type === 'binary' ? 'file-x' : 'warning-circle'} text-4xl text-red-400 mb-2 mx-auto`} />
<p className="font-medium text-bolt-elements-textPrimary">
{type === 'binary' ? 'Binary file detected' : 'Error processing file'}
</p>
<p className="text-sm mt-1">
{type === 'binary'
? 'Diff view is not available for binary files'
: 'Could not generate diff preview'}
</p>
</div>
</div>
);
const NoChangesView = memo(({ beforeCode, language, highlighter }: {
beforeCode: string;
language: string;
highlighter: any;
}) => (
<div className="h-full flex flex-col items-center justify-center p-4">
<div className="text-center text-bolt-elements-textTertiary">
<div className="i-ph:files text-4xl text-green-400 mb-2 mx-auto" />
<p className="font-medium text-bolt-elements-textPrimary">Files are identical</p>
<p className="text-sm mt-1">Both versions match exactly</p>
</div>
<div className="mt-4 w-full max-w-2xl bg-bolt-elements-background-depth-1 rounded-lg border border-bolt-elements-borderColor overflow-hidden">
<div className="p-2 text-xs font-bold text-bolt-elements-textTertiary border-b border-bolt-elements-borderColor">
Current Content
</div>
<div className="overflow-auto max-h-96">
{beforeCode.split('\n').map((line, index) => (
<div key={index} className="flex group min-w-fit">
<div className={lineNumberStyles}>{index + 1}</div>
<div className={lineContentStyles}>
<span className="mr-2"> </span>
<span dangerouslySetInnerHTML={{
__html: highlighter ?
highlighter.codeToHtml(line, { lang: language, theme: 'github-dark' })
.replace(/<\/?pre[^>]*>/g, '')
.replace(/<\/?code[^>]*>/g, '')
: line
}} />
</div>
</div>
))}
</div>
</div>
</div>
));
const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language, lightTheme, darkTheme }: CodeComparisonProps) => {
const [isFullscreen, setIsFullscreen] = useState(false);
const [highlighter, setHighlighter] = useState<any>(null);
const toggleFullscreen = useCallback(() => {
setIsFullscreen(prev => !prev);
}, []);
const { unifiedBlocks, hasChanges, isBinary, error } = useMemo(() => processChanges(beforeCode, afterCode), [beforeCode, afterCode]);
useEffect(() => {
getHighlighter({
themes: ['github-dark'],
langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx']
}).then(setHighlighter);
}, []);
if (isBinary || error) return renderContentWarning(isBinary ? 'binary' : 'error');
const renderDiffBlock = (block: DiffBlock, index?: number) => {
const key = index !== undefined ? `${block.lineNumber}-${index}` : block.lineNumber;
const bgColor = {
added: 'bg-green-500/20 border-l-4 border-green-500',
removed: 'bg-red-500/20 border-l-4 border-red-500',
unchanged: ''
}[block.type];
const highlightedCode = highlighter ?
highlighter.codeToHtml(block.content, { lang: language, theme: 'github-dark' }) :
block.content;
return (
<div key={key} className="flex group min-w-fit">
<div className={lineNumberStyles}>
{block.lineNumber + 1}
</div>
<div className={`${lineContentStyles} ${bgColor}`}>
<span className="mr-2 text-bolt-elements-textTertiary">
{block.type === 'added' && '+'}
{block.type === 'removed' && '-'}
{block.type === 'unchanged' && ' '}
</span>
<span
dangerouslySetInnerHTML={{
__html: highlightedCode.replace(/<\/?pre[^>]*>/g, '').replace(/<\/?code[^>]*>/g, '')
}}
/>
</div>
</div>
);
};
return (
<FullscreenOverlay isFullscreen={isFullscreen}>
<div className="w-full h-full flex flex-col">
<div className="flex items-center bg-bolt-elements-background-depth-1 p-2 text-sm text-bolt-elements-textPrimary shrink-0">
<div className="i-ph:file mr-2 h-4 w-4 shrink-0" />
<span className="truncate">{filename}</span>
<span className="ml-auto shrink-0 flex items-center">
{hasChanges ? (
<span className="text-yellow-400">Modified</span>
) : (
<span className="text-green-400">No Changes</span>
)}
<FullscreenButton onClick={toggleFullscreen} isFullscreen={isFullscreen} />
</span>
</div>
<div className="flex-1 overflow-auto diff-panel-content">
{hasChanges ? (
<div className="overflow-x-auto">
{unifiedBlocks.map((block, index) => renderDiffBlock(block, index))}
</div>
) : (
<NoChangesView
beforeCode={beforeCode}
language={language}
highlighter={highlighter}
/>
)}
</div>
</div>
</FullscreenOverlay>
);
});
const SideBySideComparison = memo(({
beforeCode,
afterCode,
language,
filename,
lightTheme,
darkTheme,
}: CodeComparisonProps) => {
const [isFullscreen, setIsFullscreen] = useState(false);
const [highlighter, setHighlighter] = useState<any>(null);
const toggleFullscreen = useCallback(() => {
setIsFullscreen(prev => !prev);
}, []);
const { beforeLines, afterLines, hasChanges, lineChanges, isBinary, error } = useMemo(() => processChanges(beforeCode, afterCode), [beforeCode, afterCode]);
useEffect(() => {
getHighlighter({
themes: ['github-dark'],
langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx']
}).then(setHighlighter);
}, []);
if (isBinary || error) return renderContentWarning(isBinary ? 'binary' : 'error');
const renderCode = (code: string) => {
if (!highlighter) return code;
const highlightedCode = highlighter.codeToHtml(code, {
lang: language,
theme: 'github-dark'
});
return highlightedCode.replace(/<\/?pre[^>]*>/g, '').replace(/<\/?code[^>]*>/g, '');
};
return (
<FullscreenOverlay isFullscreen={isFullscreen}>
<div className="w-full h-full flex flex-col">
<div className="flex items-center bg-bolt-elements-background-depth-1 p-2 text-sm text-bolt-elements-textPrimary shrink-0">
<div className="i-ph:file mr-2 h-4 w-4 shrink-0" />
<span className="truncate">{filename}</span>
<span className="ml-auto shrink-0 flex items-center">
{hasChanges ? (
<span className="text-yellow-400">Modified</span>
) : (
<span className="text-green-400">No Changes</span>
)}
<FullscreenButton onClick={toggleFullscreen} isFullscreen={isFullscreen} />
</span>
</div>
<div className="flex-1 overflow-auto diff-panel-content">
{hasChanges ? (
<div className="grid md:grid-cols-2 divide-x divide-bolt-elements-borderColor relative h-full">
<div className="overflow-auto">
<div className="overflow-auto">
{beforeLines.map((line, index) => (
<div key={`before-${index}`} className="flex group min-w-fit">
<div className={lineNumberStyles}>{index + 1}</div>
<div className={`${lineContentStyles} ${lineChanges.before.has(index) ? 'bg-red-500/20 border-l-4 border-red-500' : ''}`}>
<span className="mr-2 text-bolt-elements-textTertiary">
{lineChanges.before.has(index) ? '-' : ' '}
</span>
<span dangerouslySetInnerHTML={{ __html: renderCode(line) }} />
</div>
</div>
))}
</div>
</div>
<div className="overflow-auto">
{afterLines.map((line, index) => (
<div key={`after-${index}`} className="flex group min-w-fit">
<div className={lineNumberStyles}>{index + 1}</div>
<div className={`${lineContentStyles} ${lineChanges.after.has(index) ? 'bg-green-500/20 border-l-4 border-green-500' : ''}`}>
<span className="mr-2 text-bolt-elements-textTertiary">
{lineChanges.after.has(index) ? '+' : ' '}
</span>
<span dangerouslySetInnerHTML={{ __html: renderCode(line) }} />
</div>
</div>
))}
</div>
</div>
) : (
<NoChangesView
beforeCode={beforeCode}
language={language}
highlighter={highlighter}
/>
)}
</div>
</div>
</FullscreenOverlay>
);
});
interface DiffViewProps {
fileHistory: Record<string, FileHistory>;
setFileHistory: React.Dispatch<React.SetStateAction<Record<string, FileHistory>>>;
diffViewMode: 'inline' | 'side';
actionRunner: ActionRunner;
}
export const DiffView = memo(({ fileHistory, setFileHistory, diffViewMode, actionRunner }: DiffViewProps) => {
const files = useStore(workbenchStore.files) as FileMap;
const selectedFile = useStore(workbenchStore.selectedFile);
const currentDocument = useStore(workbenchStore.currentDocument) as EditorDocument;
const unsavedFiles = useStore(workbenchStore.unsavedFiles);
useEffect(() => {
if (selectedFile && currentDocument) {
const file = files[selectedFile];
if (!file || !('content' in file)) return;
const existingHistory = fileHistory[selectedFile];
const currentContent = currentDocument.value;
const relativePath = extractRelativePath(selectedFile);
const unifiedDiff = diffFiles(
relativePath,
existingHistory?.originalContent || file.content,
currentContent
);
if (unifiedDiff) {
const newChanges = diffLines(
existingHistory?.originalContent || file.content,
currentContent
);
const newHistory: FileHistory = {
originalContent: existingHistory?.originalContent || file.content,
lastModified: Date.now(),
changes: [
...(existingHistory?.changes || []),
...newChanges
].slice(-100), // Limitar histórico de mudanças
versions: [
...(existingHistory?.versions || []),
{
timestamp: Date.now(),
content: currentContent
}
].slice(-10), // Manter apenas as 10 últimas versões
changeSource: 'auto-save'
};
setFileHistory(prev => ({ ...prev, [selectedFile]: newHistory }));
}
}
}, [selectedFile, currentDocument?.value, files, setFileHistory, unsavedFiles]);
if (!selectedFile || !currentDocument) {
return (
<div className="flex w-full h-full justify-center items-center bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary">
Select a file to view differences
</div>
);
}
const file = files[selectedFile];
const originalContent = file && 'content' in file ? file.content : '';
const currentContent = currentDocument.value;
const history = fileHistory[selectedFile];
const effectiveOriginalContent = history?.originalContent || originalContent;
const language = getLanguageFromExtension(selectedFile.split('.').pop() || '');
try {
return (
<div className="h-full overflow-hidden">
{diffViewMode === 'inline' ? (
<InlineDiffComparison
beforeCode={effectiveOriginalContent}
afterCode={currentContent}
language={language}
filename={selectedFile}
lightTheme="github-light"
darkTheme="github-dark"
/>
) : (
<SideBySideComparison
beforeCode={effectiveOriginalContent}
afterCode={currentContent}
language={language}
filename={selectedFile}
lightTheme="github-light"
darkTheme="github-dark"
/>
)}
</div>
);
} catch (error) {
console.error('DiffView render error:', error);
return (
<div className="flex w-full h-full justify-center items-center bg-bolt-elements-background-depth-1 text-red-400">
<div className="text-center">
<div className="i-ph:warning-circle text-4xl mb-2" />
<p>Failed to render diff view</p>
</div>
</div>
);
}
});

View File

@@ -1,15 +1,8 @@
import { useStore } from '@nanostores/react';
import { motion, type HTMLMotionProps, type Variants } from 'framer-motion';
import { computed } from 'nanostores';
import { memo, useCallback, useEffect, useState, useMemo } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { Popover, Transition } from '@headlessui/react';
import { type Change } from 'diff';
import { formatDistanceToNow as formatDistance } from 'date-fns';
import { ActionRunner } from '~/lib/runtime/action-runner';
import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension';
import type { FileHistory } from '~/types/actions';
import { DiffView } from './DiffView';
import {
type OnChangeCallback as OnEditorChange,
type OnScrollCallback as OnEditorScroll,
@@ -25,16 +18,10 @@ import { EditorPanel } from './EditorPanel';
import { Preview } from './Preview';
import useViewport from '~/lib/hooks';
import { PushToGitHubDialog } from '~/components/@settings/tabs/connections/components/PushToGitHubDialog';
import Cookies from 'js-cookie';
interface WorkspaceProps {
chatStarted?: boolean;
isStreaming?: boolean;
actionRunner: ActionRunner;
metadata?: {
gitUrl?: string;
};
updateChatMestaData?: (metadata: any) => void;
}
const viewTransition = { ease: cubicEasingFn };
@@ -44,10 +31,6 @@ const sliderOptions: SliderOptions<WorkbenchViewType> = {
value: 'code',
text: 'Code',
},
middle: {
value: 'diff',
text: 'Diff',
},
right: {
value: 'preview',
text: 'Preview',
@@ -71,171 +54,11 @@ const workbenchVariants = {
},
} satisfies Variants;
const FileModifiedDropdown = memo(({
fileHistory,
onSelectFile,
diffViewMode,
toggleDiffViewMode,
}: {
fileHistory: Record<string, FileHistory>,
onSelectFile: (filePath: string) => void,
diffViewMode: 'inline' | 'side',
toggleDiffViewMode: () => void,
}) => {
const modifiedFiles = Object.entries(fileHistory);
const hasChanges = modifiedFiles.length > 0;
const [searchQuery, setSearchQuery] = useState('');
const filteredFiles = useMemo(() => {
return modifiedFiles.filter(([filePath]) =>
filePath.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [modifiedFiles, searchQuery]);
return (
<div className="flex items-center gap-2">
<Popover className="relative">
{({ open }: { open: boolean }) => (
<>
<Popover.Button className="flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg bg-bolt-elements-background-depth-2 hover:bg-bolt-elements-background-depth-3 transition-colors text-bolt-elements-textPrimary border border-bolt-elements-borderColor">
<span className="font-medium">File Changes</span>
{hasChanges && (
<span className="w-5 h-5 rounded-full bg-accent-500/20 text-accent-500 text-xs flex items-center justify-center border border-accent-500/30">
{modifiedFiles.length}
</span>
)}
</Popover.Button>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Popover.Panel className="absolute right-0 z-20 mt-2 w-80 origin-top-right rounded-xl bg-bolt-elements-background-depth-2 shadow-xl border border-bolt-elements-borderColor">
<div className="p-2">
<div className="relative mx-2 mb-2">
<input
type="text"
placeholder="Search files..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-8 pr-3 py-1.5 text-sm rounded-lg bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor focus:outline-none focus:ring-2 focus:ring-blue-500/50"
/>
<div className="absolute left-2 top-1/2 -translate-y-1/2 text-bolt-elements-textTertiary">
<div className="i-ph:magnifying-glass" />
</div>
</div>
<div className="max-h-60 overflow-y-auto">
{filteredFiles.length > 0 ? (
filteredFiles.map(([filePath, history]) => {
const extension = filePath.split('.').pop() || '';
const language = getLanguageFromExtension(extension);
return (
<button
key={filePath}
onClick={() => onSelectFile(filePath)}
className="w-full px-3 py-2 text-left rounded-md hover:bg-bolt-elements-background-depth-1 transition-colors group bg-transparent"
>
<div className="flex items-center gap-2">
<div className="shrink-0 w-5 h-5 text-bolt-elements-textTertiary">
{['typescript', 'javascript', 'jsx', 'tsx'].includes(language) && <div className="i-ph:file-js" />}
{['css', 'scss', 'less'].includes(language) && <div className="i-ph:paint-brush" />}
{language === 'html' && <div className="i-ph:code" />}
{language === 'json' && <div className="i-ph:brackets-curly" />}
{language === 'python' && <div className="i-ph:file-text" />}
{language === 'markdown' && <div className="i-ph:article" />}
{['yaml', 'yml'].includes(language) && <div className="i-ph:file-text" />}
{language === 'sql' && <div className="i-ph:database" />}
{language === 'dockerfile' && <div className="i-ph:cube" />}
{language === 'shell' && <div className="i-ph:terminal" />}
{!['typescript', 'javascript', 'css', 'html', 'json', 'python', 'markdown', 'yaml', 'yml', 'sql', 'dockerfile', 'shell', 'jsx', 'tsx', 'scss', 'less'].includes(language) && <div className="i-ph:file-text" />}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<span className="truncate text-sm font-medium text-bolt-elements-textPrimary">
{filePath.split('/').pop()}
</span>
</div>
<div className="flex items-center gap-2 text-xs text-bolt-elements-textTertiary">
<span className="truncate">{filePath}</span>
<span className="shrink-0"></span>
<span className="shrink-0">
{formatDistance(history.lastModified)}
</span>
</div>
</div>
</div>
</button>
);
})
) : (
<div className="flex flex-col items-center justify-center p-4 text-center">
<div className="w-12 h-12 mb-2 text-bolt-elements-textTertiary">
<div className="i-ph:file-dashed" />
</div>
<p className="text-sm font-medium text-bolt-elements-textPrimary">
{searchQuery ? 'No matching files' : 'No modified files'}
</p>
<p className="text-xs text-bolt-elements-textTertiary mt-1">
{searchQuery ? 'Try another search' : 'Changes will appear here as you edit'}
</p>
</div>
)}
</div>
</div>
{hasChanges && (
<div className="border-t border-bolt-elements-borderColor p-2">
<button
onClick={() => {
navigator.clipboard.writeText(
filteredFiles.map(([filePath]) => filePath).join('\n')
);
toast('File list copied to clipboard', {
icon: <div className="i-ph:check-circle text-accent-500" />
});
}}
className="w-full flex items-center justify-center gap-2 px-3 py-1.5 text-sm rounded-lg bg-bolt-elements-background-depth-1 hover:bg-bolt-elements-background-depth-3 transition-colors text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary"
>
Copy File List
</button>
</div>
)}
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<button
onClick={(e) => { e.stopPropagation(); toggleDiffViewMode(); }}
className="flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg bg-bolt-elements-background-depth-2 hover:bg-bolt-elements-background-depth-3 transition-colors text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
>
<span className="font-medium">{diffViewMode === 'inline' ? 'Inline' : 'Side by Side'}</span>
</button>
</div>
);
});
export const Workbench = memo(({
chatStarted,
isStreaming,
actionRunner,
metadata,
updateChatMestaData
}: WorkspaceProps) => {
export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => {
renderLogger.trace('Workbench');
const [isSyncing, setIsSyncing] = useState(false);
const [isPushDialogOpen, setIsPushDialogOpen] = useState(false);
const [diffViewMode, setDiffViewMode] = useState<'inline' | 'side'>('inline');
const [fileHistory, setFileHistory] = useState<Record<string, FileHistory>>({});
const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys());
const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));
const showWorkbench = useStore(workbenchStore.showWorkbench);
@@ -298,15 +121,6 @@ export const Workbench = memo(({
}
}, []);
const handleSelectFile = useCallback((filePath: string) => {
workbenchStore.setSelectedFile(filePath);
workbenchStore.currentView.set('diff');
}, []);
const toggleDiffViewMode = useCallback(() => {
setDiffViewMode(prev => prev === 'inline' ? 'side' : 'inline');
}, []);
return (
chatStarted && (
<motion.div
@@ -361,14 +175,6 @@ export const Workbench = memo(({
</PanelHeaderButton>
</div>
)}
{selectedView === 'diff' && (
<FileModifiedDropdown
fileHistory={fileHistory}
onSelectFile={handleSelectFile}
diffViewMode={diffViewMode}
toggleDiffViewMode={toggleDiffViewMode}
/>
)}
<IconButton
icon="i-ph:x-circle"
className="-mr-1"
@@ -380,8 +186,8 @@ export const Workbench = memo(({
</div>
<div className="relative flex-1 overflow-hidden">
<View
initial={{ x: '0%' }}
animate={{ x: selectedView === 'code' ? '0%' : '-100%' }}
initial={{ x: selectedView === 'code' ? 0 : '-100%' }}
animate={{ x: selectedView === 'code' ? 0 : '-100%' }}
>
<EditorPanel
editorDocument={currentDocument}
@@ -397,19 +203,8 @@ export const Workbench = memo(({
/>
</View>
<View
initial={{ x: '100%' }}
animate={{ x: selectedView === 'diff' ? '0%' : selectedView === 'code' ? '100%' : '-100%' }}
>
<DiffView
fileHistory={fileHistory}
setFileHistory={setFileHistory}
diffViewMode={diffViewMode}
actionRunner={actionRunner}
/>
</View>
<View
initial={{ x: '100%' }}
animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}
initial={{ x: selectedView === 'preview' ? 0 : '100%' }}
animate={{ x: selectedView === 'preview' ? 0 : '100%' }}
>
<Preview />
</View>
@@ -420,24 +215,14 @@ export const Workbench = memo(({
<PushToGitHubDialog
isOpen={isPushDialogOpen}
onClose={() => setIsPushDialogOpen(false)}
onPush={async (repoName, username, token) => {
onPush={async (repoName, username, token, isPrivate) => {
try {
const commitMessage = prompt('Please enter a commit message:', 'Initial commit') || 'Initial commit';
await workbenchStore.pushToGitHub(repoName, commitMessage, username, token);
const repoUrl = `https://github.com/${username}/${repoName}`;
if (updateChatMestaData && !metadata?.gitUrl) {
updateChatMestaData({
...(metadata || {}),
gitUrl: repoUrl,
});
}
const repoUrl = await workbenchStore.pushToGitHub(repoName, undefined, username, token, isPrivate);
return repoUrl;
} catch (error) {
console.error('Error pushing to GitHub:', error);
toast.error('Failed to push to GitHub');
throw error;
throw error; // Rethrow to let PushToGitHubDialog handle the error state
}
}}
/>

View File

@@ -1,7 +1,7 @@
import type { WebContainer } from '@webcontainer/api';
import { path as nodePath } from '~/utils/path';
import { path } from '~/utils/path';
import { atom, map, type MapStore } from 'nanostores';
import type { ActionAlert, BoltAction, FileHistory } from '~/types/actions';
import type { ActionAlert, BoltAction } from '~/types/actions';
import { createScopedLogger } from '~/utils/logger';
import { unreachable } from '~/utils/unreachable';
import type { ActionCallbackData } from './message-parser';
@@ -276,9 +276,9 @@ export class ActionRunner {
}
const webcontainer = await this.#webcontainer;
const relativePath = nodePath.relative(webcontainer.workdir, action.filePath);
const relativePath = path.relative(webcontainer.workdir, action.filePath);
let folder = nodePath.dirname(relativePath);
let folder = path.dirname(relativePath);
// remove trailing slashes
folder = folder.replace(/\/+$/g, '');
@@ -304,31 +304,4 @@ export class ActionRunner {
this.actions.setKey(id, { ...actions[id], ...newState });
}
async getFileHistory(filePath: string): Promise<FileHistory | null> {
try {
const webcontainer = await this.#webcontainer;
const historyPath = this.#getHistoryPath(filePath);
const content = await webcontainer.fs.readFile(historyPath, 'utf-8');
return JSON.parse(content);
} catch (error) {
return null;
}
}
async saveFileHistory(filePath: string, history: FileHistory) {
const webcontainer = await this.#webcontainer;
const historyPath = this.#getHistoryPath(filePath);
await this.#runFileAction({
type: 'file',
filePath: historyPath,
content: JSON.stringify(history),
changeSource: 'auto-save'
} as any);
}
#getHistoryPath(filePath: string) {
return nodePath.join('.history', filePath);
}
}

View File

@@ -10,16 +10,18 @@ import { FilesStore, type FileMap } from './files';
import { PreviewsStore } from './previews';
import { TerminalStore } from './terminal';
import JSZip from 'jszip';
import pkg from 'file-saver';
const { saveAs } = pkg;
import fileSaver from 'file-saver';
import { Octokit, type RestEndpointMethodTypes } from '@octokit/rest';
import * as nodePath from 'node:path';
import { path } from '~/utils/path';
import { extractRelativePath } from '~/utils/diff';
import { description } from '~/lib/persistence';
import Cookies from 'js-cookie';
import { createSampler } from '~/utils/sampler';
import type { ActionAlert } from '~/types/actions';
// Destructure saveAs from the CommonJS module
const { saveAs } = fileSaver;
export interface ArtifactState {
id: string;
title: string;
@@ -32,7 +34,7 @@ export type ArtifactUpdateState = Pick<ArtifactState, 'title' | 'closed'>;
type Artifacts = MapStore<Record<string, ArtifactState>>;
export type WorkbenchViewType = 'code' | 'diff' | 'preview';
export type WorkbenchViewType = 'code' | 'preview';
export class WorkbenchStore {
#previewsStore = new PreviewsStore(webcontainer);
@@ -330,7 +332,7 @@ export class WorkbenchStore {
if (data.action.type === 'file') {
const wc = await webcontainer;
const fullPath = nodePath.join(wc.workdir, data.action.filePath);
const fullPath = path.join(wc.workdir, data.action.filePath);
if (this.selectedFile.value !== fullPath) {
this.setSelectedFile(fullPath);
@@ -435,7 +437,13 @@ export class WorkbenchStore {
return syncedFiles;
}
async pushToGitHub(repoName: string, commitMessage?: string, githubUsername?: string, ghToken?: string) {
async pushToGitHub(
repoName: string,
commitMessage?: string,
githubUsername?: string,
ghToken?: string,
isPrivate: boolean = false,
) {
try {
// Use cookies if username and token are not provided
const githubToken = ghToken || Cookies.get('githubToken');
@@ -459,7 +467,7 @@ export class WorkbenchStore {
// Repository doesn't exist, so create a new one
const { data: newRepo } = await octokit.repos.createForAuthenticatedUser({
name: repoName,
private: false,
private: isPrivate,
auto_init: true,
});
repo = newRepo;
@@ -537,7 +545,7 @@ export class WorkbenchStore {
sha: newCommit.sha,
});
alert(`Repository created and code pushed: ${repo.html_url}`);
return repo.html_url; // Return the URL instead of showing alert
} catch (error) {
console.error('Error pushing to GitHub:', error);
throw error; // Rethrow the error for further handling

View File

@@ -1,72 +0,0 @@
.diff-panel-content {
scrollbar-width: thin;
scrollbar-color: rgba(155, 155, 155, 0.5) transparent;
}
.diff-panel-content::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.diff-panel-content::-webkit-scrollbar-track {
background: transparent;
}
.diff-panel-content::-webkit-scrollbar-thumb {
background-color: rgba(155, 155, 155, 0.5);
border-radius: 4px;
border: 2px solid transparent;
}
.diff-panel-content::-webkit-scrollbar-thumb:hover {
background-color: rgba(155, 155, 155, 0.7);
}
/* Hide scrollbar for the left panel when not hovered */
.diff-panel:not(:hover) .diff-panel-content::-webkit-scrollbar {
display: none;
}
.diff-panel:not(:hover) .diff-panel-content {
scrollbar-width: none;
}
/* Estilos para as linhas de diff */
.diff-block-added {
@apply bg-green-500/20 border-l-4 border-green-500;
}
.diff-block-removed {
@apply bg-red-500/20 border-l-4 border-red-500;
}
/* Melhorar contraste para mudanças */
.diff-panel-content .group:hover .diff-block-added {
@apply bg-green-500/30;
}
.diff-panel-content .group:hover .diff-block-removed {
@apply bg-red-500/30;
}
/* Estilos unificados para ambas as visualizações */
.diff-line {
@apply flex group min-w-fit transition-colors duration-150;
}
.diff-line-number {
@apply w-12 shrink-0 pl-2 py-0.5 text-left font-mono text-bolt-elements-textTertiary border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1;
}
.diff-line-content {
@apply px-4 py-0.5 font-mono whitespace-pre flex-1 group-hover:bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary;
}
/* Cores específicas para adições/remoções */
.diff-added {
@apply bg-green-500/20 border-l-4 border-green-500;
}
.diff-removed {
@apply bg-red-500/20 border-l-4 border-red-500;
}

View File

@@ -1,5 +1,3 @@
import type { Change } from 'diff';
export type ActionType = 'file' | 'shell';
export interface BaseAction {
@@ -30,15 +28,3 @@ export interface ActionAlert {
content: string;
source?: 'terminal' | 'preview'; // Add source to differentiate between terminal and preview errors
}
export interface FileHistory {
originalContent: string;
lastModified: number;
changes: Change[];
versions: {
timestamp: number;
content: string;
}[];
// Novo campo para rastrear a origem das mudanças
changeSource?: 'user' | 'auto-save' | 'external';
}

View File

@@ -1,24 +0,0 @@
export const getLanguageFromExtension = (ext: string): string => {
const map: Record<string, string> = {
js: "javascript",
jsx: "jsx",
ts: "typescript",
tsx: "tsx",
json: "json",
html: "html",
css: "css",
py: "python",
java: "java",
rb: "ruby",
cpp: "cpp",
c: "c",
cs: "csharp",
go: "go",
rs: "rust",
php: "php",
swift: "swift",
md: "plaintext",
sh: "bash",
};
return map[ext] || "typescript";
};