fix(chat): inject preview inline in chat tab + resizable note list panel
This commit is contained in:
@@ -507,6 +507,56 @@ export function ContextualAIChat({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Enrich-in-progress indicator */}
|
||||
{resourceEnriching && !isLoading && (
|
||||
<div className="flex gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-emerald-500/10 text-emerald-600 flex items-center justify-center flex-shrink-0 border border-emerald-500/20">
|
||||
<Wand2 className="h-3 w-3" />
|
||||
</div>
|
||||
<div className="bg-muted/40 border border-border/40 p-3 rounded-2xl rounded-tl-sm flex items-center gap-2">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-emerald-600" />
|
||||
<span className="text-xs text-muted-foreground">{t('ai.resource.enriching') || 'Traitement IA...'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Inline preview from inject buttons */}
|
||||
{resourcePreview && !resourceEnriching && (
|
||||
<div className="rounded-xl border border-primary/30 bg-primary/5 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-primary/20">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-primary">
|
||||
{resourcePreview.source === 'chat' ? (t('ai.resource.fromChat') || 'Remplacement')
|
||||
: resourcePreview.source === 'replace' ? (t('ai.resource.replacement') || 'Remplacement')
|
||||
: resourcePreview.source === 'complete' ? (t('ai.resource.completedByAI') || 'Complété par IA')
|
||||
: (t('ai.resource.mergedByAI') || 'Fusionné par IA')}
|
||||
</span>
|
||||
<button onClick={() => setResourcePreview(null)} className="text-muted-foreground hover:text-foreground">
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3 py-2 max-h-48 overflow-y-auto">
|
||||
<MarkdownContent content={resourcePreview.text} />
|
||||
</div>
|
||||
<div className="flex gap-2 px-3 py-2 border-t border-primary/20">
|
||||
<button
|
||||
onClick={() => setResourcePreview(null)}
|
||||
className="flex-1 text-[11px] py-1.5 rounded-lg border border-border/60 text-muted-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
{t('common.cancel') || 'Annuler'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { if (onApplyToNote && resourcePreview) { onApplyToNote(resourcePreview.text); setResourcePreview(null); setResourceText(''); toast.success(t('ai.appliedToNote')) } }}
|
||||
disabled={!onApplyToNote}
|
||||
className="flex-1 text-[11px] py-1.5 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 flex items-center justify-center gap-1"
|
||||
>
|
||||
<Check className="h-3 w-3" />
|
||||
{t('ai.resource.applyToNote') || 'Appliquer'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -756,6 +756,27 @@ export function NotesTabsView({
|
||||
const selected = items.find((n) => n.id === selectedId) ?? null
|
||||
const colorKey = selected ? getColorKey(selected) : 'default'
|
||||
|
||||
// Resizable left panel (180px min, 420px max, default 256px)
|
||||
const [listPanelWidth, setListPanelWidth] = useState(256)
|
||||
const isDraggingRef = useRef(false)
|
||||
const handleResizeStart = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
isDraggingRef.current = true
|
||||
const startX = e.clientX
|
||||
const startW = listPanelWidth
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
if (!isDraggingRef.current) return
|
||||
setListPanelWidth(Math.min(420, Math.max(180, startW + (ev.clientX - startX))))
|
||||
}
|
||||
const onUp = () => {
|
||||
isDraggingRef.current = false
|
||||
window.removeEventListener('mousemove', onMove)
|
||||
window.removeEventListener('mouseup', onUp)
|
||||
}
|
||||
window.addEventListener('mousemove', onMove)
|
||||
window.addEventListener('mouseup', onUp)
|
||||
}
|
||||
|
||||
const handleCreateNote = (noteType: NoteType = 'richtext') => {
|
||||
startCreating(async () => {
|
||||
try {
|
||||
@@ -815,8 +836,8 @@ export function NotesTabsView({
|
||||
style={{ height: 'max(360px, min(85vh, calc(100vh - 9rem)))' }}
|
||||
data-testid="notes-grid-tabs"
|
||||
>
|
||||
{/* ── Left panel: note list ── */}
|
||||
<div className="flex w-80 shrink-0 flex-col border-r border-border/60 bg-background">
|
||||
{/* ── Left panel: note list — resizable ── */}
|
||||
<div className="flex shrink-0 flex-col border-r border-border/60 bg-background" style={{ width: listPanelWidth }}>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border/60 bg-background/95 px-4 py-3.5">
|
||||
@@ -941,6 +962,15 @@ export function NotesTabsView({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resize handle */}
|
||||
<div
|
||||
onMouseDown={handleResizeStart}
|
||||
className="w-1 shrink-0 cursor-col-resize bg-border/40 hover:bg-primary/40 transition-colors active:bg-primary/60 relative group"
|
||||
title="Redimensionner"
|
||||
>
|
||||
<div className="absolute inset-y-0 -left-1 -right-1 group-hover:bg-primary/10 transition-colors" />
|
||||
</div>
|
||||
|
||||
{/* ── Right content panel ── */}
|
||||
{selected ? (
|
||||
<div className="flex min-w-0 flex-1 overflow-hidden">
|
||||
|
||||
60
memento-note/scripts/add-resize.js
Normal file
60
memento-note/scripts/add-resize.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const filePath = path.join(__dirname, '..', 'components', 'notes-tabs-view.tsx');
|
||||
let src = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const insertCode = `
|
||||
// Resizable left panel
|
||||
const [listPanelWidth, setListPanelWidth] = useState(256)
|
||||
const isDraggingRef = useRef(false)
|
||||
|
||||
const handleResizeStart = (e) => {
|
||||
e.preventDefault()
|
||||
isDraggingRef.current = true
|
||||
const startX = e.clientX
|
||||
const startW = listPanelWidth
|
||||
const onMove = (ev) => {
|
||||
if (!isDraggingRef.current) return
|
||||
setListPanelWidth(Math.min(420, Math.max(180, startW + (ev.clientX - startX))))
|
||||
}
|
||||
const onUp = () => {
|
||||
isDraggingRef.current = false
|
||||
window.removeEventListener('mousemove', onMove)
|
||||
window.removeEventListener('mouseup', onUp)
|
||||
}
|
||||
window.addEventListener('mousemove', onMove)
|
||||
window.addEventListener('mouseup', onUp)
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
// Insert before the return statement
|
||||
const MARKER = ' return (\r\n <div\r\n className="flex min-h-0 flex-1 gap-0';
|
||||
const ALT_MARKER = ' return (\n <div\n className="flex min-h-0 flex-1 gap-0';
|
||||
|
||||
if (src.includes(MARKER) && !src.includes('listPanelWidth')) {
|
||||
src = src.replace(MARKER, insertCode + MARKER);
|
||||
console.log('Inserted using CRLF marker');
|
||||
} else if (src.includes(ALT_MARKER) && !src.includes('listPanelWidth')) {
|
||||
src = src.replace(ALT_MARKER, insertCode + ALT_MARKER);
|
||||
console.log('Inserted using LF marker');
|
||||
} else if (src.includes('listPanelWidth')) {
|
||||
console.log('State already exists, skipping insert');
|
||||
} else {
|
||||
console.error('Marker not found!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Fix left panel width - CRLF version
|
||||
const OLD_CRLF = 'className="flex w-80 shrink-0 flex-col border-r border-border/60 bg-background">';
|
||||
const NEW = 'className="flex shrink-0 flex-col border-r border-border/60 bg-background" style={{ width: listPanelWidth }}>';
|
||||
if (src.includes(OLD_CRLF)) {
|
||||
src = src.replace(OLD_CRLF, NEW);
|
||||
console.log('Replaced panel width');
|
||||
} else {
|
||||
console.log('Panel width already updated or marker not found');
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, src);
|
||||
console.log('Done!');
|
||||
Reference in New Issue
Block a user