fix: fix enhance prompt to stop implementing full project instead of enhancing (#1383) #release
* fix: enhance prompt fix * fix: added error capture on api error * fix: replaced error with log for wrong files selected by bolt
This commit is contained in:
@@ -4,11 +4,6 @@ import { classNames } from '~/utils/classNames';
|
||||
import { cubicEasingFn } from '~/utils/easings';
|
||||
import { genericMemo } from '~/utils/react';
|
||||
|
||||
interface SliderOption<T> {
|
||||
value: T;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type SliderOptions<T> = {
|
||||
left: { value: T; text: string };
|
||||
middle?: { value: T; text: string };
|
||||
@@ -31,14 +26,17 @@ export const Slider = genericMemo(<T,>({ selected, options, setSelected }: Slide
|
||||
<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 && !isMiddleSelected}
|
||||
setSelected={() => setSelected?.(options.right.value)}
|
||||
>
|
||||
{options.right.text}
|
||||
</SliderButton>
|
||||
</div>
|
||||
|
||||
@@ -41,15 +41,17 @@ 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"}
|
||||
title={isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}
|
||||
>
|
||||
<div className={isFullscreen ? "i-ph:corners-in" : "i-ph:corners-out"} />
|
||||
<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}</>;
|
||||
|
||||
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">
|
||||
@@ -75,7 +77,7 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
hasChanges: false,
|
||||
lineChanges: { before: new Set(), after: new Set() },
|
||||
unifiedBlocks: [],
|
||||
isBinary: true
|
||||
isBinary: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,7 +86,7 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
return content
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
.map(line => line.trimEnd());
|
||||
.map((line) => line.trimEnd());
|
||||
};
|
||||
|
||||
const beforeLines = normalizeContent(beforeCode);
|
||||
@@ -98,19 +100,21 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
hasChanges: false,
|
||||
lineChanges: { before: new Set(), after: new Set() },
|
||||
unifiedBlocks: [],
|
||||
isBinary: false
|
||||
isBinary: false,
|
||||
};
|
||||
}
|
||||
|
||||
const lineChanges = {
|
||||
before: new Set<number>(),
|
||||
after: new Set<number>()
|
||||
after: new Set<number>(),
|
||||
};
|
||||
|
||||
const unifiedBlocks: DiffBlock[] = [];
|
||||
|
||||
// Compare lines directly for more accurate diff
|
||||
let i = 0, j = 0;
|
||||
let i = 0,
|
||||
j = 0;
|
||||
|
||||
while (i < beforeLines.length || j < afterLines.length) {
|
||||
if (i < beforeLines.length && j < afterLines.length && beforeLines[i] === afterLines[j]) {
|
||||
// Unchanged line
|
||||
@@ -118,7 +122,7 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
lineNumber: j,
|
||||
content: afterLines[j],
|
||||
type: 'unchanged',
|
||||
correspondingLine: i
|
||||
correspondingLine: i,
|
||||
});
|
||||
i++;
|
||||
j++;
|
||||
@@ -138,7 +142,7 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
content: beforeLines[i + l],
|
||||
type: 'removed',
|
||||
correspondingLine: j,
|
||||
charChanges: [{ value: beforeLines[i + l], type: 'removed' }]
|
||||
charChanges: [{ value: beforeLines[i + l], type: 'removed' }],
|
||||
});
|
||||
}
|
||||
i += k;
|
||||
@@ -153,7 +157,7 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
content: afterLines[j + l],
|
||||
type: 'added',
|
||||
correspondingLine: i,
|
||||
charChanges: [{ value: afterLines[j + l], type: 'added' }]
|
||||
charChanges: [{ value: afterLines[j + l], type: 'added' }],
|
||||
});
|
||||
}
|
||||
j += k;
|
||||
@@ -167,20 +171,25 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
if (i < beforeLines.length && j < afterLines.length) {
|
||||
const beforeLine = beforeLines[i];
|
||||
const afterLine = afterLines[j];
|
||||
|
||||
|
||||
// Find common prefix and suffix
|
||||
let prefixLength = 0;
|
||||
while (prefixLength < beforeLine.length &&
|
||||
prefixLength < afterLine.length &&
|
||||
beforeLine[prefixLength] === afterLine[prefixLength]) {
|
||||
|
||||
while (
|
||||
prefixLength < beforeLine.length &&
|
||||
prefixLength < afterLine.length &&
|
||||
beforeLine[prefixLength] === afterLine[prefixLength]
|
||||
) {
|
||||
prefixLength++;
|
||||
}
|
||||
|
||||
|
||||
let suffixLength = 0;
|
||||
while (suffixLength < beforeLine.length - prefixLength &&
|
||||
suffixLength < afterLine.length - prefixLength &&
|
||||
beforeLine[beforeLine.length - 1 - suffixLength] ===
|
||||
afterLine[afterLine.length - 1 - suffixLength]) {
|
||||
|
||||
while (
|
||||
suffixLength < beforeLine.length - prefixLength &&
|
||||
suffixLength < afterLine.length - prefixLength &&
|
||||
beforeLine[beforeLine.length - 1 - suffixLength] === afterLine[afterLine.length - 1 - suffixLength]
|
||||
) {
|
||||
suffixLength++;
|
||||
}
|
||||
|
||||
@@ -201,11 +210,12 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
charChanges: [
|
||||
{ value: prefix, type: 'unchanged' },
|
||||
{ value: beforeMiddle, type: 'removed' },
|
||||
{ value: suffix, type: 'unchanged' }
|
||||
]
|
||||
{ value: suffix, type: 'unchanged' },
|
||||
],
|
||||
});
|
||||
i++;
|
||||
}
|
||||
|
||||
if (afterMiddle) {
|
||||
lineChanges.after.add(j);
|
||||
unifiedBlocks.push({
|
||||
@@ -216,8 +226,8 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
charChanges: [
|
||||
{ value: prefix, type: 'unchanged' },
|
||||
{ value: afterMiddle, type: 'added' },
|
||||
{ value: suffix, type: 'unchanged' }
|
||||
]
|
||||
{ value: suffix, type: 'unchanged' },
|
||||
],
|
||||
});
|
||||
j++;
|
||||
}
|
||||
@@ -230,10 +240,11 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
content: beforeLines[i],
|
||||
type: 'removed',
|
||||
correspondingLine: j,
|
||||
charChanges: [{ value: beforeLines[i], type: 'removed' }]
|
||||
charChanges: [{ value: beforeLines[i], type: 'removed' }],
|
||||
});
|
||||
i++;
|
||||
}
|
||||
|
||||
if (j < afterLines.length) {
|
||||
lineChanges.after.add(j);
|
||||
unifiedBlocks.push({
|
||||
@@ -241,7 +252,7 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
content: afterLines[j],
|
||||
type: 'added',
|
||||
correspondingLine: i - 1,
|
||||
charChanges: [{ value: afterLines[j], type: 'added' }]
|
||||
charChanges: [{ value: afterLines[j], type: 'added' }],
|
||||
});
|
||||
j++;
|
||||
}
|
||||
@@ -255,10 +266,11 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
content: beforeLines[i],
|
||||
type: 'removed',
|
||||
correspondingLine: j,
|
||||
charChanges: [{ value: beforeLines[i], type: 'removed' }]
|
||||
charChanges: [{ value: beforeLines[i], type: 'removed' }],
|
||||
});
|
||||
i++;
|
||||
}
|
||||
|
||||
if (j < afterLines.length) {
|
||||
lineChanges.after.add(j);
|
||||
unifiedBlocks.push({
|
||||
@@ -266,7 +278,7 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
content: afterLines[j],
|
||||
type: 'added',
|
||||
correspondingLine: i - 1,
|
||||
charChanges: [{ value: afterLines[j], type: 'added' }]
|
||||
charChanges: [{ value: afterLines[j], type: 'added' }],
|
||||
});
|
||||
j++;
|
||||
}
|
||||
@@ -284,7 +296,7 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
hasChanges: lineChanges.before.size > 0 || lineChanges.after.size > 0,
|
||||
lineChanges,
|
||||
unifiedBlocks: processedBlocks,
|
||||
isBinary: false
|
||||
isBinary: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error processing changes:', error);
|
||||
@@ -295,26 +307,28 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
lineChanges: { before: new Set(), after: new Set() },
|
||||
unifiedBlocks: [],
|
||||
error: true,
|
||||
isBinary: false
|
||||
isBinary: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const lineNumberStyles = "w-9 shrink-0 pl-2 py-1 text-left font-mono text-bolt-elements-textTertiary border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1";
|
||||
const lineContentStyles = "px-1 py-1 font-mono whitespace-pre flex-1 group-hover:bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary";
|
||||
const diffPanelStyles = "h-full overflow-auto diff-panel-content";
|
||||
const lineNumberStyles =
|
||||
'w-9 shrink-0 pl-2 py-1 text-left font-mono text-bolt-elements-textTertiary border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1';
|
||||
const lineContentStyles =
|
||||
'px-1 py-1 font-mono whitespace-pre flex-1 group-hover:bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary';
|
||||
const diffPanelStyles = 'h-full overflow-auto diff-panel-content';
|
||||
|
||||
// Updated color styles for better consistency
|
||||
const diffLineStyles = {
|
||||
added: 'bg-green-500/10 dark:bg-green-500/20 border-l-4 border-green-500',
|
||||
removed: 'bg-red-500/10 dark:bg-red-500/20 border-l-4 border-red-500',
|
||||
unchanged: ''
|
||||
unchanged: '',
|
||||
};
|
||||
|
||||
const changeColorStyles = {
|
||||
added: 'text-green-700 dark:text-green-500 bg-green-500/10 dark:bg-green-500/20',
|
||||
removed: 'text-red-700 dark:text-red-500 bg-red-500/10 dark:bg-red-500/20',
|
||||
unchanged: 'text-bolt-elements-textPrimary'
|
||||
unchanged: 'text-bolt-elements-textPrimary',
|
||||
};
|
||||
|
||||
const renderContentWarning = (type: 'binary' | 'error') => (
|
||||
@@ -325,50 +339,61 @@ const renderContentWarning = (type: 'binary' | 'error') => (
|
||||
{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'}
|
||||
{type === 'binary' ? 'Diff view is not available for binary files' : 'Could not generate diff preview'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const NoChangesView = memo(({ beforeCode, language, highlighter, theme }: {
|
||||
beforeCode: string;
|
||||
language: string;
|
||||
highlighter: any;
|
||||
theme: string;
|
||||
}) => (
|
||||
<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
|
||||
const NoChangesView = memo(
|
||||
({
|
||||
beforeCode,
|
||||
language,
|
||||
highlighter,
|
||||
theme,
|
||||
}: {
|
||||
beforeCode: string;
|
||||
language: string;
|
||||
highlighter: any;
|
||||
theme: string;
|
||||
}) => (
|
||||
<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="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: theme === 'dark' ? 'github-dark' : 'github-light' })
|
||||
.replace(/<\/?pre[^>]*>/g, '')
|
||||
.replace(/<\/?code[^>]*>/g, '')
|
||||
: line
|
||||
}} />
|
||||
<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: theme === 'dark' ? 'github-dark' : 'github-light',
|
||||
})
|
||||
.replace(/<\/?pre[^>]*>/g, '')
|
||||
.replace(/<\/?code[^>]*>/g, '')
|
||||
: line,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
),
|
||||
);
|
||||
|
||||
// Otimização do processamento de diferenças com memoização
|
||||
const useProcessChanges = (beforeCode: string, afterCode: string) => {
|
||||
@@ -376,150 +401,154 @@ const useProcessChanges = (beforeCode: string, afterCode: string) => {
|
||||
};
|
||||
|
||||
// Componente otimizado para renderização de linhas de código
|
||||
const CodeLine = memo(({
|
||||
lineNumber,
|
||||
content,
|
||||
type,
|
||||
highlighter,
|
||||
language,
|
||||
block,
|
||||
theme
|
||||
}: {
|
||||
lineNumber: number;
|
||||
content: string;
|
||||
type: 'added' | 'removed' | 'unchanged';
|
||||
highlighter: any;
|
||||
language: string;
|
||||
block: DiffBlock;
|
||||
theme: string;
|
||||
}) => {
|
||||
const bgColor = diffLineStyles[type];
|
||||
const CodeLine = memo(
|
||||
({
|
||||
lineNumber,
|
||||
content,
|
||||
type,
|
||||
highlighter,
|
||||
language,
|
||||
block,
|
||||
theme,
|
||||
}: {
|
||||
lineNumber: number;
|
||||
content: string;
|
||||
type: 'added' | 'removed' | 'unchanged';
|
||||
highlighter: any;
|
||||
language: string;
|
||||
block: DiffBlock;
|
||||
theme: string;
|
||||
}) => {
|
||||
const bgColor = diffLineStyles[type];
|
||||
|
||||
const renderContent = () => {
|
||||
if (type === 'unchanged' || !block.charChanges) {
|
||||
const highlightedCode = highlighter ?
|
||||
highlighter.codeToHtml(content, { lang: language, theme: theme === 'dark' ? 'github-dark' : 'github-light' })
|
||||
.replace(/<\/?pre[^>]*>/g, '')
|
||||
.replace(/<\/?code[^>]*>/g, '')
|
||||
: content;
|
||||
return <span dangerouslySetInnerHTML={{ __html: highlightedCode }} />;
|
||||
}
|
||||
const renderContent = () => {
|
||||
if (type === 'unchanged' || !block.charChanges) {
|
||||
const highlightedCode = highlighter
|
||||
? highlighter
|
||||
.codeToHtml(content, { lang: language, theme: theme === 'dark' ? 'github-dark' : 'github-light' })
|
||||
.replace(/<\/?pre[^>]*>/g, '')
|
||||
.replace(/<\/?code[^>]*>/g, '')
|
||||
: content;
|
||||
return <span dangerouslySetInnerHTML={{ __html: highlightedCode }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{block.charChanges.map((change, index) => {
|
||||
const changeClass = changeColorStyles[change.type];
|
||||
|
||||
const highlightedCode = highlighter
|
||||
? highlighter
|
||||
.codeToHtml(change.value, {
|
||||
lang: language,
|
||||
theme: theme === 'dark' ? 'github-dark' : 'github-light',
|
||||
})
|
||||
.replace(/<\/?pre[^>]*>/g, '')
|
||||
.replace(/<\/?code[^>]*>/g, '')
|
||||
: change.value;
|
||||
|
||||
return <span key={index} className={changeClass} dangerouslySetInnerHTML={{ __html: highlightedCode }} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{block.charChanges.map((change, index) => {
|
||||
const changeClass = changeColorStyles[change.type];
|
||||
|
||||
const highlightedCode = highlighter ?
|
||||
highlighter.codeToHtml(change.value, { lang: language, theme: theme === 'dark' ? 'github-dark' : 'github-light' })
|
||||
.replace(/<\/?pre[^>]*>/g, '')
|
||||
.replace(/<\/?code[^>]*>/g, '')
|
||||
: change.value;
|
||||
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
className={changeClass}
|
||||
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex group min-w-fit">
|
||||
<div className={lineNumberStyles}>{lineNumber + 1}</div>
|
||||
<div className={`${lineContentStyles} ${bgColor}`}>
|
||||
<span className="mr-2 text-bolt-elements-textTertiary">
|
||||
{type === 'added' && <span className="text-green-700 dark:text-green-500">+</span>}
|
||||
{type === 'removed' && <span className="text-red-700 dark:text-red-500">-</span>}
|
||||
{type === 'unchanged' && ' '}
|
||||
</span>
|
||||
{renderContent()}
|
||||
<div className="flex group min-w-fit">
|
||||
<div className={lineNumberStyles}>{lineNumber + 1}</div>
|
||||
<div className={`${lineContentStyles} ${bgColor}`}>
|
||||
<span className="mr-2 text-bolt-elements-textTertiary">
|
||||
{type === 'added' && <span className="text-green-700 dark:text-green-500">+</span>}
|
||||
{type === 'removed' && <span className="text-red-700 dark:text-red-500">-</span>}
|
||||
{type === 'unchanged' && ' '}
|
||||
</span>
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Componente para exibir informações sobre o arquivo
|
||||
const FileInfo = memo(({
|
||||
filename,
|
||||
hasChanges,
|
||||
onToggleFullscreen,
|
||||
isFullscreen,
|
||||
beforeCode,
|
||||
afterCode
|
||||
}: {
|
||||
filename: string;
|
||||
hasChanges: boolean;
|
||||
onToggleFullscreen: () => void;
|
||||
isFullscreen: boolean;
|
||||
beforeCode: string;
|
||||
afterCode: string;
|
||||
}) => {
|
||||
// Calculate additions and deletions from the current document
|
||||
const { additions, deletions } = useMemo(() => {
|
||||
if (!hasChanges) return { additions: 0, deletions: 0 };
|
||||
|
||||
const changes = diffLines(beforeCode, afterCode, {
|
||||
newlineIsToken: false,
|
||||
ignoreWhitespace: true,
|
||||
ignoreCase: false
|
||||
});
|
||||
|
||||
return changes.reduce((acc: { additions: number; deletions: number }, change: Change) => {
|
||||
if (change.added) {
|
||||
acc.additions += change.value.split('\n').length;
|
||||
const FileInfo = memo(
|
||||
({
|
||||
filename,
|
||||
hasChanges,
|
||||
onToggleFullscreen,
|
||||
isFullscreen,
|
||||
beforeCode,
|
||||
afterCode,
|
||||
}: {
|
||||
filename: string;
|
||||
hasChanges: boolean;
|
||||
onToggleFullscreen: () => void;
|
||||
isFullscreen: boolean;
|
||||
beforeCode: string;
|
||||
afterCode: string;
|
||||
}) => {
|
||||
// Calculate additions and deletions from the current document
|
||||
const { additions, deletions } = useMemo(() => {
|
||||
if (!hasChanges) {
|
||||
return { additions: 0, deletions: 0 };
|
||||
}
|
||||
if (change.removed) {
|
||||
acc.deletions += change.value.split('\n').length;
|
||||
}
|
||||
return acc;
|
||||
}, { additions: 0, deletions: 0 });
|
||||
}, [hasChanges, beforeCode, afterCode]);
|
||||
|
||||
const showStats = additions > 0 || deletions > 0;
|
||||
const changes = diffLines(beforeCode, afterCode, {
|
||||
newlineIsToken: false,
|
||||
ignoreWhitespace: true,
|
||||
ignoreCase: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<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 gap-2">
|
||||
{hasChanges ? (
|
||||
<>
|
||||
{showStats && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
{additions > 0 && (
|
||||
<span className="text-green-700 dark:text-green-500">+{additions}</span>
|
||||
)}
|
||||
{deletions > 0 && (
|
||||
<span className="text-red-700 dark:text-red-500">-{deletions}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-yellow-600 dark:text-yellow-400">Modified</span>
|
||||
<span className="text-bolt-elements-textTertiary text-xs">
|
||||
{new Date().toLocaleTimeString()}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-green-700 dark:text-green-400">No Changes</span>
|
||||
)}
|
||||
<FullscreenButton onClick={onToggleFullscreen} isFullscreen={isFullscreen} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return changes.reduce(
|
||||
(acc: { additions: number; deletions: number }, change: Change) => {
|
||||
if (change.added) {
|
||||
acc.additions += change.value.split('\n').length;
|
||||
}
|
||||
|
||||
const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language, lightTheme, darkTheme }: CodeComparisonProps) => {
|
||||
if (change.removed) {
|
||||
acc.deletions += change.value.split('\n').length;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ additions: 0, deletions: 0 },
|
||||
);
|
||||
}, [hasChanges, beforeCode, afterCode]);
|
||||
|
||||
const showStats = additions > 0 || deletions > 0;
|
||||
|
||||
return (
|
||||
<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 gap-2">
|
||||
{hasChanges ? (
|
||||
<>
|
||||
{showStats && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
{additions > 0 && <span className="text-green-700 dark:text-green-500">+{additions}</span>}
|
||||
{deletions > 0 && <span className="text-red-700 dark:text-red-500">-{deletions}</span>}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-yellow-600 dark:text-yellow-400">Modified</span>
|
||||
<span className="text-bolt-elements-textTertiary text-xs">{new Date().toLocaleTimeString()}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-green-700 dark:text-green-400">No Changes</span>
|
||||
)}
|
||||
<FullscreenButton onClick={onToggleFullscreen} isFullscreen={isFullscreen} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }: CodeComparisonProps) => {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [highlighter, setHighlighter] = useState<any>(null);
|
||||
const theme = useStore(themeStore);
|
||||
|
||||
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
setIsFullscreen(prev => !prev);
|
||||
setIsFullscreen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const { unifiedBlocks, hasChanges, isBinary, error } = useProcessChanges(beforeCode, afterCode);
|
||||
@@ -527,16 +556,18 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language,
|
||||
useEffect(() => {
|
||||
getHighlighter({
|
||||
themes: ['github-dark', 'github-light'],
|
||||
langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx']
|
||||
langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx'],
|
||||
}).then(setHighlighter);
|
||||
}, []);
|
||||
|
||||
if (isBinary || error) return renderContentWarning(isBinary ? 'binary' : 'error');
|
||||
if (isBinary || error) {
|
||||
return renderContentWarning(isBinary ? 'binary' : 'error');
|
||||
}
|
||||
|
||||
return (
|
||||
<FullscreenOverlay isFullscreen={isFullscreen}>
|
||||
<div className="w-full h-full flex flex-col">
|
||||
<FileInfo
|
||||
<FileInfo
|
||||
filename={filename}
|
||||
hasChanges={hasChanges}
|
||||
onToggleFullscreen={toggleFullscreen}
|
||||
@@ -561,12 +592,7 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language,
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<NoChangesView
|
||||
beforeCode={beforeCode}
|
||||
language={language}
|
||||
highlighter={highlighter}
|
||||
theme={theme}
|
||||
/>
|
||||
<NoChangesView beforeCode={beforeCode} language={language} highlighter={highlighter} theme={theme} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -580,7 +606,7 @@ interface DiffViewProps {
|
||||
actionRunner: ActionRunner;
|
||||
}
|
||||
|
||||
export const DiffView = memo(({ fileHistory, setFileHistory, actionRunner }: DiffViewProps) => {
|
||||
export const DiffView = memo(({ fileHistory, setFileHistory }: DiffViewProps) => {
|
||||
const files = useStore(workbenchStore.files) as FileMap;
|
||||
const selectedFile = useStore(workbenchStore.selectedFile);
|
||||
const currentDocument = useStore(workbenchStore.currentDocument) as EditorDocument;
|
||||
@@ -589,82 +615,80 @@ export const DiffView = memo(({ fileHistory, setFileHistory, actionRunner }: Dif
|
||||
useEffect(() => {
|
||||
if (selectedFile && currentDocument) {
|
||||
const file = files[selectedFile];
|
||||
if (!file || !('content' in file)) return;
|
||||
|
||||
if (!file || !('content' in file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingHistory = fileHistory[selectedFile];
|
||||
const currentContent = currentDocument.value;
|
||||
|
||||
|
||||
// Normalizar o conteúdo para comparação
|
||||
const normalizedCurrentContent = currentContent.replace(/\r\n/g, '\n').trim();
|
||||
const normalizedOriginalContent = (existingHistory?.originalContent || file.content).replace(/\r\n/g, '\n').trim();
|
||||
|
||||
const normalizedOriginalContent = (existingHistory?.originalContent || file.content)
|
||||
.replace(/\r\n/g, '\n')
|
||||
.trim();
|
||||
|
||||
// Se não há histórico existente, criar um novo apenas se houver diferenças
|
||||
if (!existingHistory) {
|
||||
if (normalizedCurrentContent !== normalizedOriginalContent) {
|
||||
const newChanges = diffLines(file.content, currentContent);
|
||||
setFileHistory(prev => ({
|
||||
setFileHistory((prev) => ({
|
||||
...prev,
|
||||
[selectedFile]: {
|
||||
originalContent: file.content,
|
||||
lastModified: Date.now(),
|
||||
changes: newChanges,
|
||||
versions: [{
|
||||
timestamp: Date.now(),
|
||||
content: currentContent
|
||||
}],
|
||||
changeSource: 'auto-save'
|
||||
}
|
||||
versions: [
|
||||
{
|
||||
timestamp: Date.now(),
|
||||
content: currentContent,
|
||||
},
|
||||
],
|
||||
changeSource: 'auto-save',
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Se já existe histórico, verificar se há mudanças reais desde a última versão
|
||||
const lastVersion = existingHistory.versions[existingHistory.versions.length - 1];
|
||||
const normalizedLastContent = lastVersion?.content.replace(/\r\n/g, '\n').trim();
|
||||
|
||||
|
||||
if (normalizedCurrentContent === normalizedLastContent) {
|
||||
return; // Não criar novo histórico se o conteúdo é o mesmo
|
||||
}
|
||||
|
||||
// Verificar se há mudanças significativas usando diffFiles
|
||||
const relativePath = extractRelativePath(selectedFile);
|
||||
const unifiedDiff = diffFiles(
|
||||
relativePath,
|
||||
existingHistory.originalContent,
|
||||
currentContent
|
||||
);
|
||||
const unifiedDiff = diffFiles(relativePath, existingHistory.originalContent, currentContent);
|
||||
|
||||
if (unifiedDiff) {
|
||||
const newChanges = diffLines(
|
||||
existingHistory.originalContent,
|
||||
currentContent
|
||||
);
|
||||
const newChanges = diffLines(existingHistory.originalContent, currentContent);
|
||||
|
||||
// Verificar se as mudanças são significativas
|
||||
const hasSignificantChanges = newChanges.some(change =>
|
||||
(change.added || change.removed) && change.value.trim().length > 0
|
||||
const hasSignificantChanges = newChanges.some(
|
||||
(change) => (change.added || change.removed) && change.value.trim().length > 0,
|
||||
);
|
||||
|
||||
if (hasSignificantChanges) {
|
||||
const newHistory: FileHistory = {
|
||||
originalContent: existingHistory.originalContent,
|
||||
lastModified: Date.now(),
|
||||
changes: [
|
||||
...existingHistory.changes,
|
||||
...newChanges
|
||||
].slice(-100), // Limitar histórico de mudanças
|
||||
changes: [...existingHistory.changes, ...newChanges].slice(-100), // Limitar histórico de mudanças
|
||||
versions: [
|
||||
...existingHistory.versions,
|
||||
{
|
||||
timestamp: Date.now(),
|
||||
content: currentContent
|
||||
}
|
||||
content: currentContent,
|
||||
},
|
||||
].slice(-10), // Manter apenas as 10 últimas versões
|
||||
changeSource: 'auto-save'
|
||||
changeSource: 'auto-save',
|
||||
};
|
||||
|
||||
setFileHistory(prev => ({ ...prev, [selectedFile]: newHistory }));
|
||||
|
||||
setFileHistory((prev) => ({ ...prev, [selectedFile]: newHistory }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,15 +274,19 @@ function File({
|
||||
fileHistory = {},
|
||||
}: FileProps) {
|
||||
const fileModifications = fileHistory[fullPath];
|
||||
const hasModifications = fileModifications !== undefined;
|
||||
|
||||
// const hasModifications = fileModifications !== undefined;
|
||||
|
||||
// Calculate added and removed lines from the most recent changes
|
||||
const { additions, deletions } = useMemo(() => {
|
||||
if (!fileModifications?.originalContent) return { additions: 0, deletions: 0 };
|
||||
if (!fileModifications?.originalContent) {
|
||||
return { additions: 0, deletions: 0 };
|
||||
}
|
||||
|
||||
// Usar a mesma lógica do DiffView para processar as mudanças
|
||||
const normalizedOriginal = fileModifications.originalContent.replace(/\r\n/g, '\n');
|
||||
const normalizedCurrent = fileModifications.versions[fileModifications.versions.length - 1]?.content.replace(/\r\n/g, '\n') || '';
|
||||
const normalizedCurrent =
|
||||
fileModifications.versions[fileModifications.versions.length - 1]?.content.replace(/\r\n/g, '\n') || '';
|
||||
|
||||
if (normalizedOriginal === normalizedCurrent) {
|
||||
return { additions: 0, deletions: 0 };
|
||||
@@ -291,18 +295,23 @@ function File({
|
||||
const changes = diffLines(normalizedOriginal, normalizedCurrent, {
|
||||
newlineIsToken: false,
|
||||
ignoreWhitespace: true,
|
||||
ignoreCase: false
|
||||
ignoreCase: false,
|
||||
});
|
||||
|
||||
return changes.reduce((acc: { additions: number; deletions: number }, change: Change) => {
|
||||
if (change.added) {
|
||||
acc.additions += change.value.split('\n').length;
|
||||
}
|
||||
if (change.removed) {
|
||||
acc.deletions += change.value.split('\n').length;
|
||||
}
|
||||
return acc;
|
||||
}, { additions: 0, deletions: 0 });
|
||||
return changes.reduce(
|
||||
(acc: { additions: number; deletions: number }, change: Change) => {
|
||||
if (change.added) {
|
||||
acc.additions += change.value.split('\n').length;
|
||||
}
|
||||
|
||||
if (change.removed) {
|
||||
acc.deletions += change.value.split('\n').length;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ additions: 0, deletions: 0 },
|
||||
);
|
||||
}, [fileModifications]);
|
||||
|
||||
const showStats = additions > 0 || deletions > 0;
|
||||
@@ -330,17 +339,11 @@ function File({
|
||||
<div className="flex items-center gap-1">
|
||||
{showStats && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
{additions > 0 && (
|
||||
<span className="text-green-500">+{additions}</span>
|
||||
)}
|
||||
{deletions > 0 && (
|
||||
<span className="text-red-500">-{deletions}</span>
|
||||
)}
|
||||
{additions > 0 && <span className="text-green-500">+{additions}</span>}
|
||||
{deletions > 0 && <span className="text-red-500">-{deletions}</span>}
|
||||
</div>
|
||||
)}
|
||||
{unsavedChanges && (
|
||||
<span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />
|
||||
)}
|
||||
{unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />}
|
||||
</div>
|
||||
</div>
|
||||
</NodeButton>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { memo, useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Popover, Transition } from '@headlessui/react';
|
||||
import { diffLines, 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';
|
||||
@@ -25,7 +24,6 @@ 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;
|
||||
@@ -71,403 +69,413 @@ const workbenchVariants = {
|
||||
},
|
||||
} satisfies Variants;
|
||||
|
||||
const FileModifiedDropdown = memo(({
|
||||
fileHistory,
|
||||
onSelectFile,
|
||||
}: {
|
||||
fileHistory: Record<string, FileHistory>,
|
||||
onSelectFile: (filePath: string) => void,
|
||||
}) => {
|
||||
const modifiedFiles = Object.entries(fileHistory);
|
||||
const hasChanges = modifiedFiles.length > 0;
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const FileModifiedDropdown = memo(
|
||||
({
|
||||
fileHistory,
|
||||
onSelectFile,
|
||||
}: {
|
||||
fileHistory: Record<string, FileHistory>;
|
||||
onSelectFile: (filePath: string) => 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]);
|
||||
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" />
|
||||
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">
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="truncate text-sm font-medium text-bolt-elements-textPrimary">
|
||||
{filePath.split('/').pop()}
|
||||
</span>
|
||||
<span className="truncate text-xs text-bolt-elements-textTertiary">
|
||||
{filePath}
|
||||
</span>
|
||||
</div>
|
||||
{(() => {
|
||||
// Calculate diff stats
|
||||
const { additions, deletions } = (() => {
|
||||
if (!history.originalContent) {
|
||||
return { additions: 0, deletions: 0 };
|
||||
}
|
||||
|
||||
const normalizedOriginal = history.originalContent.replace(/\r\n/g, '\n');
|
||||
const normalizedCurrent =
|
||||
history.versions[history.versions.length - 1]?.content.replace(
|
||||
/\r\n/g,
|
||||
'\n',
|
||||
) || '';
|
||||
|
||||
if (normalizedOriginal === normalizedCurrent) {
|
||||
return { additions: 0, deletions: 0 };
|
||||
}
|
||||
|
||||
const changes = diffLines(normalizedOriginal, normalizedCurrent, {
|
||||
newlineIsToken: false,
|
||||
ignoreWhitespace: true,
|
||||
ignoreCase: false,
|
||||
});
|
||||
|
||||
return changes.reduce(
|
||||
(acc: { additions: number; deletions: number }, change: Change) => {
|
||||
if (change.added) {
|
||||
acc.additions += change.value.split('\n').length;
|
||||
}
|
||||
|
||||
if (change.removed) {
|
||||
acc.deletions += change.value.split('\n').length;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ additions: 0, deletions: 0 },
|
||||
);
|
||||
})();
|
||||
|
||||
const showStats = additions > 0 || deletions > 0;
|
||||
|
||||
return (
|
||||
showStats && (
|
||||
<div className="flex items-center gap-1 text-xs shrink-0">
|
||||
{additions > 0 && <span className="text-green-500">+{additions}</span>}
|
||||
{deletions > 0 && <span className="text-red-500">-{deletions}</span>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})()}
|
||||
</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>
|
||||
|
||||
<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">
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="truncate text-sm font-medium text-bolt-elements-textPrimary">
|
||||
{filePath.split('/').pop()}
|
||||
</span>
|
||||
<span className="truncate text-xs text-bolt-elements-textTertiary">
|
||||
{filePath}
|
||||
</span>
|
||||
</div>
|
||||
{(() => {
|
||||
// Calculate diff stats
|
||||
const { additions, deletions } = (() => {
|
||||
if (!history.originalContent) return { additions: 0, deletions: 0 };
|
||||
|
||||
const normalizedOriginal = history.originalContent.replace(/\r\n/g, '\n');
|
||||
const normalizedCurrent = history.versions[history.versions.length - 1]?.content.replace(/\r\n/g, '\n') || '';
|
||||
|
||||
if (normalizedOriginal === normalizedCurrent) {
|
||||
return { additions: 0, deletions: 0 };
|
||||
}
|
||||
|
||||
const changes = diffLines(normalizedOriginal, normalizedCurrent, {
|
||||
newlineIsToken: false,
|
||||
ignoreWhitespace: true,
|
||||
ignoreCase: false
|
||||
});
|
||||
|
||||
return changes.reduce((acc: { additions: number; deletions: number }, change: Change) => {
|
||||
if (change.added) {
|
||||
acc.additions += change.value.split('\n').length;
|
||||
}
|
||||
if (change.removed) {
|
||||
acc.deletions += change.value.split('\n').length;
|
||||
}
|
||||
return acc;
|
||||
}, { additions: 0, deletions: 0 });
|
||||
})();
|
||||
|
||||
const showStats = additions > 0 || deletions > 0;
|
||||
|
||||
return showStats && (
|
||||
<div className="flex items-center gap-1 text-xs shrink-0">
|
||||
{additions > 0 && (
|
||||
<span className="text-green-500">+{additions}</span>
|
||||
)}
|
||||
{deletions > 0 && (
|
||||
<span className="text-red-500">-{deletions}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const Workbench = memo(({
|
||||
chatStarted,
|
||||
isStreaming,
|
||||
actionRunner,
|
||||
metadata,
|
||||
updateChatMestaData
|
||||
}: WorkspaceProps) => {
|
||||
renderLogger.trace('Workbench');
|
||||
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [isPushDialogOpen, setIsPushDialogOpen] = useState(false);
|
||||
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);
|
||||
const selectedFile = useStore(workbenchStore.selectedFile);
|
||||
const currentDocument = useStore(workbenchStore.currentDocument);
|
||||
const unsavedFiles = useStore(workbenchStore.unsavedFiles);
|
||||
const files = useStore(workbenchStore.files);
|
||||
const selectedView = useStore(workbenchStore.currentView);
|
||||
|
||||
const isSmallViewport = useViewport(1024);
|
||||
|
||||
const setSelectedView = (view: WorkbenchViewType) => {
|
||||
workbenchStore.currentView.set(view);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasPreview) {
|
||||
setSelectedView('preview');
|
||||
}
|
||||
}, [hasPreview]);
|
||||
|
||||
useEffect(() => {
|
||||
workbenchStore.setDocuments(files);
|
||||
}, [files]);
|
||||
|
||||
const onEditorChange = useCallback<OnEditorChange>((update) => {
|
||||
workbenchStore.setCurrentDocumentContent(update.content);
|
||||
}, []);
|
||||
|
||||
const onEditorScroll = useCallback<OnEditorScroll>((position) => {
|
||||
workbenchStore.setCurrentDocumentScrollPosition(position);
|
||||
}, []);
|
||||
|
||||
const onFileSelect = useCallback((filePath: string | undefined) => {
|
||||
workbenchStore.setSelectedFile(filePath);
|
||||
}, []);
|
||||
|
||||
const onFileSave = useCallback(() => {
|
||||
workbenchStore.saveCurrentDocument().catch(() => {
|
||||
toast.error('Failed to update file content');
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onFileReset = useCallback(() => {
|
||||
workbenchStore.resetCurrentDocument();
|
||||
}, []);
|
||||
|
||||
const handleSyncFiles = useCallback(async () => {
|
||||
setIsSyncing(true);
|
||||
|
||||
try {
|
||||
const directoryHandle = await window.showDirectoryPicker();
|
||||
await workbenchStore.syncFiles(directoryHandle);
|
||||
toast.success('Files synced successfully');
|
||||
} catch (error) {
|
||||
console.error('Error syncing files:', error);
|
||||
toast.error('Failed to sync files');
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSelectFile = useCallback((filePath: string) => {
|
||||
workbenchStore.setSelectedFile(filePath);
|
||||
workbenchStore.currentView.set('diff');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
chatStarted && (
|
||||
<motion.div
|
||||
initial="closed"
|
||||
animate={showWorkbench ? 'open' : 'closed'}
|
||||
variants={workbenchVariants}
|
||||
className="z-workbench"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[var(--workbench-inner-width)] mr-4 z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
|
||||
{
|
||||
'w-full': isSmallViewport,
|
||||
'left-0': showWorkbench && isSmallViewport,
|
||||
'left-[var(--workbench-left)]': showWorkbench,
|
||||
'left-[100%]': !showWorkbench,
|
||||
},
|
||||
{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>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const Workbench = memo(
|
||||
({ chatStarted, isStreaming, actionRunner, metadata, updateChatMestaData }: WorkspaceProps) => {
|
||||
renderLogger.trace('Workbench');
|
||||
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [isPushDialogOpen, setIsPushDialogOpen] = useState(false);
|
||||
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);
|
||||
const selectedFile = useStore(workbenchStore.selectedFile);
|
||||
const currentDocument = useStore(workbenchStore.currentDocument);
|
||||
const unsavedFiles = useStore(workbenchStore.unsavedFiles);
|
||||
const files = useStore(workbenchStore.files);
|
||||
const selectedView = useStore(workbenchStore.currentView);
|
||||
|
||||
const isSmallViewport = useViewport(1024);
|
||||
|
||||
const setSelectedView = (view: WorkbenchViewType) => {
|
||||
workbenchStore.currentView.set(view);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasPreview) {
|
||||
setSelectedView('preview');
|
||||
}
|
||||
}, [hasPreview]);
|
||||
|
||||
useEffect(() => {
|
||||
workbenchStore.setDocuments(files);
|
||||
}, [files]);
|
||||
|
||||
const onEditorChange = useCallback<OnEditorChange>((update) => {
|
||||
workbenchStore.setCurrentDocumentContent(update.content);
|
||||
}, []);
|
||||
|
||||
const onEditorScroll = useCallback<OnEditorScroll>((position) => {
|
||||
workbenchStore.setCurrentDocumentScrollPosition(position);
|
||||
}, []);
|
||||
|
||||
const onFileSelect = useCallback((filePath: string | undefined) => {
|
||||
workbenchStore.setSelectedFile(filePath);
|
||||
}, []);
|
||||
|
||||
const onFileSave = useCallback(() => {
|
||||
workbenchStore.saveCurrentDocument().catch(() => {
|
||||
toast.error('Failed to update file content');
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onFileReset = useCallback(() => {
|
||||
workbenchStore.resetCurrentDocument();
|
||||
}, []);
|
||||
|
||||
const handleSyncFiles = useCallback(async () => {
|
||||
setIsSyncing(true);
|
||||
|
||||
try {
|
||||
const directoryHandle = await window.showDirectoryPicker();
|
||||
await workbenchStore.syncFiles(directoryHandle);
|
||||
toast.success('Files synced successfully');
|
||||
} catch (error) {
|
||||
console.error('Error syncing files:', error);
|
||||
toast.error('Failed to sync files');
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSelectFile = useCallback((filePath: string) => {
|
||||
workbenchStore.setSelectedFile(filePath);
|
||||
workbenchStore.currentView.set('diff');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
chatStarted && (
|
||||
<motion.div
|
||||
initial="closed"
|
||||
animate={showWorkbench ? 'open' : 'closed'}
|
||||
variants={workbenchVariants}
|
||||
className="z-workbench"
|
||||
>
|
||||
<div className="absolute inset-0 px-2 lg:px-6">
|
||||
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
|
||||
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
|
||||
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
||||
<div className="ml-auto" />
|
||||
{selectedView === 'code' && (
|
||||
<div className="flex overflow-y-auto">
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={() => {
|
||||
workbenchStore.downloadZip();
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:code" />
|
||||
Download Code
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton className="mr-1 text-sm" onClick={handleSyncFiles} disabled={isSyncing}>
|
||||
{isSyncing ? <div className="i-ph:spinner" /> : <div className="i-ph:cloud-arrow-down" />}
|
||||
{isSyncing ? 'Syncing...' : 'Sync Files'}
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={() => {
|
||||
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:terminal" />
|
||||
Toggle Terminal
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton className="mr-1 text-sm" onClick={() => setIsPushDialogOpen(true)}>
|
||||
<div className="i-ph:git-branch" />
|
||||
Push to GitHub
|
||||
</PanelHeaderButton>
|
||||
</div>
|
||||
)}
|
||||
{selectedView === 'diff' && (
|
||||
<FileModifiedDropdown
|
||||
fileHistory={fileHistory}
|
||||
onSelectFile={handleSelectFile}
|
||||
<div
|
||||
className={classNames(
|
||||
'fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[var(--workbench-inner-width)] mr-4 z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
|
||||
{
|
||||
'w-full': isSmallViewport,
|
||||
'left-0': showWorkbench && isSmallViewport,
|
||||
'left-[var(--workbench-left)]': showWorkbench,
|
||||
'left-[100%]': !showWorkbench,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0 px-2 lg:px-6">
|
||||
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
|
||||
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
|
||||
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
||||
<div className="ml-auto" />
|
||||
{selectedView === 'code' && (
|
||||
<div className="flex overflow-y-auto">
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={() => {
|
||||
workbenchStore.downloadZip();
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:code" />
|
||||
Download Code
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton className="mr-1 text-sm" onClick={handleSyncFiles} disabled={isSyncing}>
|
||||
{isSyncing ? <div className="i-ph:spinner" /> : <div className="i-ph:cloud-arrow-down" />}
|
||||
{isSyncing ? 'Syncing...' : 'Sync Files'}
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={() => {
|
||||
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:terminal" />
|
||||
Toggle Terminal
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton className="mr-1 text-sm" onClick={() => setIsPushDialogOpen(true)}>
|
||||
<div className="i-ph:git-branch" />
|
||||
Push to GitHub
|
||||
</PanelHeaderButton>
|
||||
</div>
|
||||
)}
|
||||
{selectedView === 'diff' && (
|
||||
<FileModifiedDropdown fileHistory={fileHistory} onSelectFile={handleSelectFile} />
|
||||
)}
|
||||
<IconButton
|
||||
icon="i-ph:x-circle"
|
||||
className="-mr-1"
|
||||
size="xl"
|
||||
onClick={() => {
|
||||
workbenchStore.showWorkbench.set(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon="i-ph:x-circle"
|
||||
className="-mr-1"
|
||||
size="xl"
|
||||
onClick={() => {
|
||||
workbenchStore.showWorkbench.set(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
<View
|
||||
initial={{ x: '0%' }}
|
||||
animate={{ x: selectedView === 'code' ? '0%' : '-100%' }}
|
||||
>
|
||||
<EditorPanel
|
||||
editorDocument={currentDocument}
|
||||
isStreaming={isStreaming}
|
||||
selectedFile={selectedFile}
|
||||
files={files}
|
||||
unsavedFiles={unsavedFiles}
|
||||
fileHistory={fileHistory}
|
||||
onFileSelect={onFileSelect}
|
||||
onEditorScroll={onEditorScroll}
|
||||
onEditorChange={onEditorChange}
|
||||
onFileSave={onFileSave}
|
||||
onFileReset={onFileReset}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
initial={{ x: '100%' }}
|
||||
animate={{ x: selectedView === 'diff' ? '0%' : selectedView === 'code' ? '100%' : '-100%' }}
|
||||
>
|
||||
<DiffView
|
||||
fileHistory={fileHistory}
|
||||
setFileHistory={setFileHistory}
|
||||
actionRunner={actionRunner}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
initial={{ x: '100%' }}
|
||||
animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}
|
||||
>
|
||||
<Preview />
|
||||
</View>
|
||||
</div>
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
<View initial={{ x: '0%' }} animate={{ x: selectedView === 'code' ? '0%' : '-100%' }}>
|
||||
<EditorPanel
|
||||
editorDocument={currentDocument}
|
||||
isStreaming={isStreaming}
|
||||
selectedFile={selectedFile}
|
||||
files={files}
|
||||
unsavedFiles={unsavedFiles}
|
||||
fileHistory={fileHistory}
|
||||
onFileSelect={onFileSelect}
|
||||
onEditorScroll={onEditorScroll}
|
||||
onEditorChange={onEditorChange}
|
||||
onFileSave={onFileSave}
|
||||
onFileReset={onFileReset}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
initial={{ x: '100%' }}
|
||||
animate={{ x: selectedView === 'diff' ? '0%' : selectedView === 'code' ? '100%' : '-100%' }}
|
||||
>
|
||||
<DiffView fileHistory={fileHistory} setFileHistory={setFileHistory} actionRunner={actionRunner} />
|
||||
</View>
|
||||
<View initial={{ x: '100%' }} animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}>
|
||||
<Preview />
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PushToGitHubDialog
|
||||
isOpen={isPushDialogOpen}
|
||||
onClose={() => setIsPushDialogOpen(false)}
|
||||
onPush={async (repoName, username, token) => {
|
||||
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,
|
||||
});
|
||||
<PushToGitHubDialog
|
||||
isOpen={isPushDialogOpen}
|
||||
onClose={() => setIsPushDialogOpen(false)}
|
||||
onPush={async (repoName, username, token) => {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
return repoUrl;
|
||||
} catch (error) {
|
||||
console.error('Error pushing to GitHub:', error);
|
||||
toast.error('Failed to push to GitHub');
|
||||
throw error;
|
||||
}
|
||||
|
||||
return repoUrl;
|
||||
} catch (error) {
|
||||
console.error('Error pushing to GitHub:', error);
|
||||
toast.error('Failed to push to GitHub');
|
||||
throw error;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// View component for rendering content with motion transitions
|
||||
interface ViewProps extends HTMLMotionProps<'div'> {
|
||||
|
||||
Reference in New Issue
Block a user