mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
- Add additions and deletions fields to FileHistory interface - Calculate diff stats when creating/updating file history - Use pre-calculated stats in UI instead of recalculating - Add fallback for contextFiles to continue with the llm request in event of an error. - Remove FileModifiedDropdown to its own component - Add refreshAllPreviews class in previews store for refreshing previews on file change - Add auto switch to preview once isStreaming goes from true to false - Add actionType to files to now show whether a file is being created or updated and modified message-parser and artifact.tsx accordingly
173 lines
9.4 KiB
TypeScript
173 lines
9.4 KiB
TypeScript
import { memo, useState, useMemo } from 'react';
|
|
import { Popover, Transition } from '@headlessui/react';
|
|
import { getLanguageFromExtension } from '~/shared/utils/getLanguageFromExtension';
|
|
import type { FileHistory } from '~/shared/types/actions';
|
|
import { toast } from '~/shared/components/ui/use-toast';
|
|
|
|
export 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]);
|
|
|
|
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-item-contentDefault">
|
|
<span>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>
|
|
{/* Use pre-calculated stats from history object */}
|
|
{(history.additions !== undefined || history.deletions !== undefined) &&
|
|
(history.additions! > 0 || history.deletions! > 0) ? (
|
|
<div className="flex items-center gap-1 text-xs shrink-0">
|
|
{history.additions! > 0 && (
|
|
<span className="text-green-500">+{history.additions}</span>
|
|
)}
|
|
{history.deletions! > 0 && (
|
|
<span className="text-red-500">-{history.deletions}</span>
|
|
)}
|
|
</div>
|
|
) : null}
|
|
</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>
|
|
);
|
|
},
|
|
);
|