mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-04-29 02:11:54 +00:00
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:
parent
b98485d99f
commit
3c28e8ad88
@ -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 };
|
||||
@ -38,7 +33,10 @@ export const Slider = genericMemo(<T,>({ selected, options, setSelected }: Slide
|
||||
</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,14 +41,16 @@ 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">
|
||||
@ -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;
|
||||
@ -170,17 +174,22 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
||||
|
||||
// Find common prefix and suffix
|
||||
let prefixLength = 0;
|
||||
while (prefixLength < beforeLine.length &&
|
||||
|
||||
while (
|
||||
prefixLength < beforeLine.length &&
|
||||
prefixLength < afterLine.length &&
|
||||
beforeLine[prefixLength] === afterLine[prefixLength]) {
|
||||
beforeLine[prefixLength] === afterLine[prefixLength]
|
||||
) {
|
||||
prefixLength++;
|
||||
}
|
||||
|
||||
let suffixLength = 0;
|
||||
while (suffixLength < beforeLine.length - prefixLength &&
|
||||
|
||||
while (
|
||||
suffixLength < beforeLine.length - prefixLength &&
|
||||
suffixLength < afterLine.length - prefixLength &&
|
||||
beforeLine[beforeLine.length - 1 - suffixLength] ===
|
||||
afterLine[afterLine.length - 1 - suffixLength]) {
|
||||
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,20 +339,24 @@ 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 }: {
|
||||
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" />
|
||||
@ -355,20 +373,27 @@ const NoChangesView = memo(({ beforeCode, language, highlighter, theme }: {
|
||||
<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' })
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: highlighter
|
||||
? highlighter
|
||||
.codeToHtml(line, {
|
||||
lang: language,
|
||||
theme: theme === 'dark' ? 'github-dark' : 'github-light',
|
||||
})
|
||||
.replace(/<\/?pre[^>]*>/g, '')
|
||||
.replace(/<\/?code[^>]*>/g, '')
|
||||
: line
|
||||
}} />
|
||||
: line,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
),
|
||||
);
|
||||
|
||||
// Otimização do processamento de diferenças com memoização
|
||||
const useProcessChanges = (beforeCode: string, afterCode: string) => {
|
||||
@ -376,15 +401,16 @@ const useProcessChanges = (beforeCode: string, afterCode: string) => {
|
||||
};
|
||||
|
||||
// Componente otimizado para renderização de linhas de código
|
||||
const CodeLine = memo(({
|
||||
const CodeLine = memo(
|
||||
({
|
||||
lineNumber,
|
||||
content,
|
||||
type,
|
||||
highlighter,
|
||||
language,
|
||||
block,
|
||||
theme
|
||||
}: {
|
||||
theme,
|
||||
}: {
|
||||
lineNumber: number;
|
||||
content: string;
|
||||
type: 'added' | 'removed' | 'unchanged';
|
||||
@ -392,13 +418,14 @@ const CodeLine = memo(({
|
||||
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' })
|
||||
const highlightedCode = highlighter
|
||||
? highlighter
|
||||
.codeToHtml(content, { lang: language, theme: theme === 'dark' ? 'github-dark' : 'github-light' })
|
||||
.replace(/<\/?pre[^>]*>/g, '')
|
||||
.replace(/<\/?code[^>]*>/g, '')
|
||||
: content;
|
||||
@ -410,19 +437,17 @@ const CodeLine = memo(({
|
||||
{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' })
|
||||
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 <span key={index} className={changeClass} dangerouslySetInnerHTML={{ __html: highlightedCode }} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
@ -441,43 +466,52 @@ const CodeLine = memo(({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Componente para exibir informações sobre o arquivo
|
||||
const FileInfo = memo(({
|
||||
const FileInfo = memo(
|
||||
({
|
||||
filename,
|
||||
hasChanges,
|
||||
onToggleFullscreen,
|
||||
isFullscreen,
|
||||
beforeCode,
|
||||
afterCode
|
||||
}: {
|
||||
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 (!hasChanges) {
|
||||
return { additions: 0, deletions: 0 };
|
||||
}
|
||||
|
||||
const changes = diffLines(beforeCode, afterCode, {
|
||||
newlineIsToken: false,
|
||||
ignoreWhitespace: true,
|
||||
ignoreCase: false
|
||||
ignoreCase: false,
|
||||
});
|
||||
|
||||
return changes.reduce((acc: { additions: number; deletions: number }, change: Change) => {
|
||||
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 });
|
||||
},
|
||||
{ additions: 0, deletions: 0 },
|
||||
);
|
||||
}, [hasChanges, beforeCode, afterCode]);
|
||||
|
||||
const showStats = additions > 0 || deletions > 0;
|
||||
@ -491,18 +525,12 @@ const FileInfo = memo(({
|
||||
<>
|
||||
{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>
|
||||
)}
|
||||
{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-bolt-elements-textTertiary text-xs">{new Date().toLocaleTimeString()}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-green-700 dark:text-green-400">No Changes</span>
|
||||
@ -511,15 +539,16 @@ const FileInfo = memo(({
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language, lightTheme, darkTheme }: CodeComparisonProps) => {
|
||||
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,11 +556,13 @@ 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}>
|
||||
@ -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,33 +615,41 @@ 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: [{
|
||||
versions: [
|
||||
{
|
||||
timestamp: Date.now(),
|
||||
content: currentContent
|
||||
}],
|
||||
changeSource: 'auto-save'
|
||||
}
|
||||
content: currentContent,
|
||||
},
|
||||
],
|
||||
changeSource: 'auto-save',
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -629,42 +663,32 @@ export const DiffView = memo(({ fileHistory, setFileHistory, actionRunner }: Dif
|
||||
|
||||
// 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) => {
|
||||
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 });
|
||||
},
|
||||
{ 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,21 +69,20 @@ const workbenchVariants = {
|
||||
},
|
||||
} satisfies Variants;
|
||||
|
||||
const FileModifiedDropdown = memo(({
|
||||
const FileModifiedDropdown = memo(
|
||||
({
|
||||
fileHistory,
|
||||
onSelectFile,
|
||||
}: {
|
||||
fileHistory: Record<string, FileHistory>,
|
||||
onSelectFile: (filePath: string) => void,
|
||||
}) => {
|
||||
}: {
|
||||
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())
|
||||
);
|
||||
return modifiedFiles.filter(([filePath]) => filePath.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
}, [modifiedFiles, searchQuery]);
|
||||
|
||||
return (
|
||||
@ -139,7 +136,9 @@ const FileModifiedDropdown = memo(({
|
||||
>
|
||||
<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" />}
|
||||
{['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" />}
|
||||
@ -149,7 +148,24 @@ const FileModifiedDropdown = memo(({
|
||||
{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" />}
|
||||
{![
|
||||
'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">
|
||||
@ -164,10 +180,16 @@ const FileModifiedDropdown = memo(({
|
||||
{(() => {
|
||||
// Calculate diff stats
|
||||
const { additions, deletions } = (() => {
|
||||
if (!history.originalContent) return { additions: 0, deletions: 0 };
|
||||
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') || '';
|
||||
const normalizedCurrent =
|
||||
history.versions[history.versions.length - 1]?.content.replace(
|
||||
/\r\n/g,
|
||||
'\n',
|
||||
) || '';
|
||||
|
||||
if (normalizedOriginal === normalizedCurrent) {
|
||||
return { additions: 0, deletions: 0 };
|
||||
@ -176,31 +198,34 @@ const FileModifiedDropdown = memo(({
|
||||
const changes = diffLines(normalizedOriginal, normalizedCurrent, {
|
||||
newlineIsToken: false,
|
||||
ignoreWhitespace: true,
|
||||
ignoreCase: false
|
||||
ignoreCase: false,
|
||||
});
|
||||
|
||||
return changes.reduce((acc: { additions: number; deletions: number }, change: Change) => {
|
||||
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 });
|
||||
},
|
||||
{ additions: 0, deletions: 0 },
|
||||
);
|
||||
})();
|
||||
|
||||
const showStats = additions > 0 || deletions > 0;
|
||||
|
||||
return showStats && (
|
||||
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>
|
||||
)}
|
||||
{additions > 0 && <span className="text-green-500">+{additions}</span>}
|
||||
{deletions > 0 && <span className="text-red-500">-{deletions}</span>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
@ -229,11 +254,9 @@ const FileModifiedDropdown = memo(({
|
||||
<div className="border-t border-bolt-elements-borderColor p-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
filteredFiles.map(([filePath]) => filePath).join('\n')
|
||||
);
|
||||
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" />
|
||||
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"
|
||||
@ -249,22 +272,18 @@ const FileModifiedDropdown = memo(({
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const Workbench = memo(({
|
||||
chatStarted,
|
||||
isStreaming,
|
||||
actionRunner,
|
||||
metadata,
|
||||
updateChatMestaData
|
||||
}: WorkspaceProps) => {
|
||||
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 modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys());
|
||||
|
||||
const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));
|
||||
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
||||
@ -387,10 +406,7 @@ export const Workbench = memo(({
|
||||
</div>
|
||||
)}
|
||||
{selectedView === 'diff' && (
|
||||
<FileModifiedDropdown
|
||||
fileHistory={fileHistory}
|
||||
onSelectFile={handleSelectFile}
|
||||
/>
|
||||
<FileModifiedDropdown fileHistory={fileHistory} onSelectFile={handleSelectFile} />
|
||||
)}
|
||||
<IconButton
|
||||
icon="i-ph:x-circle"
|
||||
@ -402,10 +418,7 @@ export const Workbench = memo(({
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
<View
|
||||
initial={{ x: '0%' }}
|
||||
animate={{ x: selectedView === 'code' ? '0%' : '-100%' }}
|
||||
>
|
||||
<View initial={{ x: '0%' }} animate={{ x: selectedView === 'code' ? '0%' : '-100%' }}>
|
||||
<EditorPanel
|
||||
editorDocument={currentDocument}
|
||||
isStreaming={isStreaming}
|
||||
@ -424,16 +437,9 @@ export const Workbench = memo(({
|
||||
initial={{ x: '100%' }}
|
||||
animate={{ x: selectedView === 'diff' ? '0%' : selectedView === 'code' ? '100%' : '-100%' }}
|
||||
>
|
||||
<DiffView
|
||||
fileHistory={fileHistory}
|
||||
setFileHistory={setFileHistory}
|
||||
actionRunner={actionRunner}
|
||||
/>
|
||||
<DiffView fileHistory={fileHistory} setFileHistory={setFileHistory} actionRunner={actionRunner} />
|
||||
</View>
|
||||
<View
|
||||
initial={{ x: '100%' }}
|
||||
animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}
|
||||
>
|
||||
<View initial={{ x: '100%' }} animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}>
|
||||
<Preview />
|
||||
</View>
|
||||
</div>
|
||||
@ -447,6 +453,7 @@ export const Workbench = memo(({
|
||||
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) {
|
||||
@ -467,7 +474,8 @@ export const Workbench = memo(({
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// View component for rendering content with motion transitions
|
||||
interface ViewProps extends HTMLMotionProps<'div'> {
|
||||
|
@ -204,7 +204,10 @@ export async function selectContext(props: {
|
||||
}
|
||||
|
||||
if (!filePaths.includes(fullPath)) {
|
||||
throw new Error(`File ${path} is not in the list of files above.`);
|
||||
logger.error(`File ${path} is not in the list of files above.`);
|
||||
return;
|
||||
|
||||
// throw new Error(`File ${path} is not in the list of files above.`);
|
||||
}
|
||||
|
||||
if (currrentFiles.includes(path)) {
|
||||
@ -218,6 +221,13 @@ export async function selectContext(props: {
|
||||
onFinish(resp);
|
||||
}
|
||||
|
||||
const totalFiles = Object.keys(filteredFiles).length;
|
||||
logger.info(`Total files: ${totalFiles}`);
|
||||
|
||||
if (totalFiles == 0) {
|
||||
throw new Error(`Bolt failed to select files`);
|
||||
}
|
||||
|
||||
return filteredFiles;
|
||||
|
||||
// generateText({
|
||||
|
@ -319,21 +319,23 @@ export class ActionRunner {
|
||||
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) {
|
||||
logger.error('Failed to get file history:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async saveFileHistory(filePath: string, history: FileHistory) {
|
||||
const webcontainer = await this.#webcontainer;
|
||||
// const webcontainer = await this.#webcontainer;
|
||||
const historyPath = this.#getHistoryPath(filePath);
|
||||
|
||||
await this.#runFileAction({
|
||||
type: 'file',
|
||||
filePath: historyPath,
|
||||
content: JSON.stringify(history),
|
||||
changeSource: 'auto-save'
|
||||
changeSource: 'auto-save',
|
||||
} as any);
|
||||
}
|
||||
|
||||
|
@ -3,11 +3,14 @@ import { streamText } from '~/lib/.server/llm/stream-text';
|
||||
import { stripIndents } from '~/utils/stripIndent';
|
||||
import type { ProviderInfo } from '~/types/model';
|
||||
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
export async function action(args: ActionFunctionArgs) {
|
||||
return enhancerAction(args);
|
||||
}
|
||||
|
||||
const logger = createScopedLogger('api.enhancher');
|
||||
|
||||
async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
||||
const { message, model, provider } = await request.json<{
|
||||
message: string;
|
||||
@ -77,8 +80,32 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
||||
env: context.cloudflare?.env as any,
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
options: {
|
||||
system:
|
||||
'You are a senior software principal architect, you should help the user analyse the user query and enrich it with the necessary context and constraints to make it more specific, actionable, and effective. You should also ensure that the prompt is self-contained and uses professional language. Your response should ONLY contain the enhanced prompt text. Do not include any explanations, metadata, or wrapper tags.',
|
||||
|
||||
/*
|
||||
* onError: (event) => {
|
||||
* throw new Response(null, {
|
||||
* status: 500,
|
||||
* statusText: 'Internal Server Error',
|
||||
* });
|
||||
* }
|
||||
*/
|
||||
},
|
||||
});
|
||||
|
||||
(async () => {
|
||||
for await (const part of result.fullStream) {
|
||||
if (part.type === 'error') {
|
||||
const error: any = part.error;
|
||||
logger.error(error);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return new Response(result.textStream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
|
@ -43,6 +43,7 @@ export interface FileHistory {
|
||||
timestamp: number;
|
||||
content: string;
|
||||
}[];
|
||||
|
||||
// Novo campo para rastrear a origem das mudanças
|
||||
changeSource?: 'user' | 'auto-save' | 'external';
|
||||
}
|
||||
|
@ -1,24 +1,24 @@
|
||||
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",
|
||||
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";
|
||||
return map[ext] || 'typescript';
|
||||
};
|
Loading…
Reference in New Issue
Block a user