This commit is contained in:
seb
2026-10-07 13:10:23 +02:00
parent e4927f5d8a
commit 6c0b1503e8
14 changed files with 2644 additions and 86 deletions

View File

@@ -26,10 +26,13 @@ import ElderlyRoundedIcon from '@mui/icons-material/ElderlyRounded';
import DirectionsWalkRoundedIcon from '@mui/icons-material/DirectionsWalkRounded';
import DirectionsRunRoundedIcon from '@mui/icons-material/DirectionsRunRounded';
import PsychologyRoundedIcon from '@mui/icons-material/PsychologyRounded';
import FolderOpenRoundedIcon from '@mui/icons-material/FolderOpenRounded';
import { setting as settingApi } from '../../api';
import chatAutomat, { selectPendingAskUser } from '../../automats/chatAutomat';
import userSettingsAutomat from '../../automats/userSettingsAutomat';
import vfsAutomat from '../../automats/vfsAutomat';
import { VfsEditorPanel } from '../vfs';
import ChatFlowList from './ChatFlowList';
import AskUserPrompt from './AskUserPrompt';
import { ACCEPT_STRING, readFileAttachment } from './chatAttachments';
@@ -43,12 +46,16 @@ const DEFAULT_DRAWER_WIDTH = 460;
const MIN_DRAWER_WIDTH = 320;
export default class ChatDrawer extends PureComponent {
constructor(props) {
super(props);
const s = chatAutomat.state;
const settings = userSettingsAutomat.state;
const vfs = vfsAutomat.state;
this.state = {
isOpen: s.isOpen,
isVfsOpen: vfs.isOpen,
vfsWidth: vfs.vfsWidth || 480,
conversations: s.conversations,
selectedConversationId: s.selectedConversationId,
draftConversation: s.draftConversation,
@@ -123,12 +130,18 @@ export default class ChatDrawer extends PureComponent {
}
return null;
});
this.unsubVfs = vfsAutomat.subscribe(this, (state) => ({
isVfsOpen: state.isOpen,
vfsWidth: state.vfsWidth,
}));
}
componentWillUnmount() {
if (this.focusTimeout) clearTimeout(this.focusTimeout);
this.unsub?.();
this.unsubSettings?.();
this.unsubVfs?.();
this.cleanupResizeListeners();
}
@@ -136,7 +149,14 @@ export default class ChatDrawer extends PureComponent {
chatAutomat.actions.closeChat();
};
handleResizeStart = (e) => {
handleInsertToChat = (text) => {
this.setState((prev) => ({
inputText: prev.inputText ? `${prev.inputText}\n${text}` : text,
}));
this.focusInput(50);
};
handleVfsResizerStart = (e) => {
if (e.button !== undefined && e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
@@ -144,25 +164,36 @@ export default class ChatDrawer extends PureComponent {
const startX = e.clientX ?? (e.touches && e.touches[0]?.clientX);
if (startX === undefined) return;
const startWidth = this.state.drawerWidth;
this.setState({ isResizing: true });
const startVfsWidth = this.state.vfsWidth;
const startChatWidth = this.state.drawerWidth;
const totalPairWidth = startVfsWidth + startChatWidth;
this.setState({ isResizing: true });
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
let currentWidth = startWidth;
let currentChatWidth = startChatWidth;
let currentVfsWidth = startVfsWidth;
const onMove = (moveEvt) => {
moveEvt.preventDefault();
const clientX = moveEvt.clientX ?? (moveEvt.touches && moveEvt.touches[0]?.clientX);
if (clientX === undefined) return;
const deltaX = startX - clientX;
const maxWidth = Math.min(window.innerWidth - 40, 1400);
const newWidth = Math.max(MIN_DRAWER_WIDTH, Math.min(maxWidth, Math.round(startWidth + deltaX)));
const deltaX = clientX - startX;
const minVfs = 260;
const minChat = MIN_DRAWER_WIDTH; // 320
currentWidth = newWidth;
this.setState({ drawerWidth: newWidth });
// Drag right (deltaX > 0): VFS increases, Chat decreases
// Drag left (deltaX < 0): VFS decreases, Chat increases
const clampedVfs = Math.max(minVfs, Math.min(totalPairWidth - minChat, Math.round(startVfsWidth + deltaX)));
const clampedChat = totalPairWidth - clampedVfs;
currentVfsWidth = clampedVfs;
currentChatWidth = clampedChat;
vfsAutomat.actions.setVfsWidth(clampedVfs);
this.setState({ drawerWidth: clampedChat });
};
const onEnd = () => {
@@ -177,7 +208,75 @@ export default class ChatDrawer extends PureComponent {
window.removeEventListener('touchend', onEnd);
this.setState({ isResizing: false });
userSettingsAutomat.actions.setChatDrawerWidth(currentWidth);
vfsAutomat.actions.setVfsWidth(currentVfsWidth);
userSettingsAutomat.actions.setChatDrawerWidth(currentChatWidth);
};
window.addEventListener('pointermove', onMove, { passive: false });
window.addEventListener('pointerup', onEnd);
window.addEventListener('pointercancel', onEnd);
window.addEventListener('mousemove', onMove, { passive: false });
window.addEventListener('mouseup', onEnd);
window.addEventListener('touchmove', onMove, { passive: false });
window.addEventListener('touchend', onEnd);
};
handleResizeStart = (e) => {
if (e.button !== undefined && e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
const startX = e.clientX ?? (e.touches && e.touches[0]?.clientX);
if (startX === undefined) return;
const startWidth = this.state.drawerWidth;
const startVfsWidth = this.state.vfsWidth;
const isVfs = this.state.isVfsOpen;
this.setState({ isResizing: true });
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
let currentChatWidth = startWidth;
let currentVfsWidth = startVfsWidth;
const onMove = (moveEvt) => {
moveEvt.preventDefault();
const clientX = moveEvt.clientX ?? (moveEvt.touches && moveEvt.touches[0]?.clientX);
if (clientX === undefined) return;
const deltaX = startX - clientX;
if (isVfs) {
const maxVfs = Math.min(window.innerWidth - startWidth - 40, 1200);
const newVfsWidth = Math.max(260, Math.min(maxVfs, Math.round(startVfsWidth + deltaX)));
currentVfsWidth = newVfsWidth;
vfsAutomat.actions.setVfsWidth(newVfsWidth);
} else {
const maxWidth = Math.min(window.innerWidth - 40, 1400);
const newWidth = Math.max(MIN_DRAWER_WIDTH, Math.min(maxWidth, Math.round(startWidth + deltaX)));
currentChatWidth = newWidth;
this.setState({ drawerWidth: newWidth });
}
};
const onEnd = () => {
document.body.style.cursor = '';
document.body.style.userSelect = '';
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onEnd);
window.removeEventListener('pointercancel', onEnd);
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onEnd);
window.removeEventListener('touchmove', onMove);
window.removeEventListener('touchend', onEnd);
this.setState({ isResizing: false });
if (isVfs) {
vfsAutomat.actions.setVfsWidth(currentVfsWidth);
} else {
userSettingsAutomat.actions.setChatDrawerWidth(currentChatWidth);
}
};
window.addEventListener('pointermove', onMove, { passive: false });
@@ -199,6 +298,7 @@ export default class ChatDrawer extends PureComponent {
document.body.style.userSelect = '';
};
handleSelectConversation = (e) => {
const id = Number(e.target.value);
if (id) {
@@ -427,6 +527,7 @@ export default class ChatDrawer extends PureComponent {
modelslugflag,
modelreasoning,
modelslug,
isVfsOpen,
} = this.state;
return (
@@ -695,11 +796,36 @@ export default class ChatDrawer extends PureComponent {
</Box>
)}
<IconButton size="small" onClick={this.handleClose} aria-label="Chat schließen" sx={{ ml: 1, flexShrink: 0 }}>
<CloseRoundedIcon fontSize="small" />
</IconButton>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, ml: 1, flexShrink: 0 }}>
<Tooltip title={isVfsOpen ? 'VFS-Dateisystem schließen' : 'VFS-Dateisystem parallel öffnen'}>
<IconButton
size="small"
onClick={() => vfsAutomat.actions.toggleVfs()}
aria-label="VFS Dateisystem"
sx={{
p: 0.5,
color: isVfsOpen ? 'primary.main' : 'text.secondary',
bgcolor: isVfsOpen ? 'action.selected' : 'transparent',
borderRadius: 1.5,
border: '1px solid',
borderColor: isVfsOpen ? 'primary.main' : 'transparent',
'&:hover': {
color: 'primary.main',
bgcolor: 'action.hover',
},
}}
>
<FolderOpenRoundedIcon sx={{ fontSize: 19 }} />
</IconButton>
</Tooltip>
<IconButton size="small" onClick={this.handleClose} aria-label="Chat schließen" sx={{ p: 0.5 }}>
<CloseRoundedIcon fontSize="small" />
</IconButton>
</Box>
</Box>
{/* Conversation Selector Bar */}
{conversations.length > 0 && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
@@ -1095,7 +1221,8 @@ export default class ChatDrawer extends PureComponent {
}
render() {
const { isOpen, drawerWidth, isResizing } = this.state;
const { isOpen, drawerWidth, isResizing, isVfsOpen, vfsWidth } = this.state;
const totalWidth = isVfsOpen ? drawerWidth + vfsWidth : drawerWidth;
return (
<>
@@ -1106,10 +1233,10 @@ export default class ChatDrawer extends PureComponent {
sx={(theme) => ({
zIndex: theme.zIndex.drawer + 2,
'& .MuiDrawer-paper': {
width: { xs: '100vw', sm: `${drawerWidth}px` },
width: { xs: '100vw', sm: `${totalWidth}px` },
maxWidth: '100vw',
display: 'flex',
flexDirection: 'column',
flexDirection: 'row',
boxShadow: SHADOW_DRAWER,
position: 'fixed',
overflow: 'visible',
@@ -1122,10 +1249,10 @@ export default class ChatDrawer extends PureComponent {
},
paper: {
sx: {
width: { xs: '100vw', sm: `${drawerWidth}px` },
width: { xs: '100vw', sm: `${totalWidth}px` },
maxWidth: '100vw',
display: 'flex',
flexDirection: 'column',
flexDirection: 'row',
boxShadow: SHADOW_DRAWER,
position: 'fixed',
overflow: 'visible',
@@ -1177,11 +1304,79 @@ export default class ChatDrawer extends PureComponent {
/>
</Box>
{this.renderHeader()}
{this.renderBody()}
{this.renderFooter()}
{/* Left: VFS Filesystem Editor Panel (parallel to chat) */}
{isVfsOpen && (
<Box
sx={{
width: { xs: '100%', sm: `${vfsWidth}px` },
minWidth: { sm: 340 },
maxWidth: { sm: 'calc(100vw - 320px)' },
height: '100%',
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
<VfsEditorPanel onInsertToChat={this.handleInsertToChat} />
</Box>
)}
{/* Vertical Split Resizer between VFS and Chat */}
{isVfsOpen && (
<Box
onPointerDown={this.handleVfsResizerStart}
onMouseDown={this.handleVfsResizerStart}
title="Ziehen zum Anpassen der Spaltenbreiten"
sx={{
width: 8,
cursor: 'col-resize',
bgcolor: 'divider',
flexShrink: 0,
display: { xs: 'none', sm: 'flex' },
alignItems: 'center',
justifyContent: 'center',
touchAction: 'none',
userSelect: 'none',
transition: 'background-color 0.15s, box-shadow 0.15s',
'&:hover, &:active': {
bgcolor: 'primary.main',
boxShadow: `0 0 8px ${BLUE_GLOW}`,
},
}}
>
<Box
sx={{
width: 2,
height: 36,
borderRadius: 1,
bgcolor: (theme) =>
theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.25)',
}}
/>
</Box>
)}
{/* Right: Chat Panel */}
<Box
sx={{
width: isVfsOpen ? { xs: '100%', sm: `${drawerWidth}px` } : '100%',
flexGrow: 1,
minWidth: { sm: 320 },
display: isVfsOpen ? { xs: 'none', sm: 'flex' } : 'flex',
flexDirection: 'column',
height: '100%',
overflow: 'hidden',
bgcolor: 'background.paper',
}}
>
{this.renderHeader()}
{this.renderBody()}
{this.renderFooter()}
</Box>
</Drawer>
</>
);
}
}