This commit is contained in:
seb
2026-10-07 19:33:26 +02:00
parent ee2056a159
commit 4cc8c49cf1
7 changed files with 152 additions and 49 deletions

View File

@@ -77,6 +77,8 @@ const initialState = {
selectedConversationId: null, selectedConversationId: null,
/** First-open draft: no server conversation yet, created on first send. */ /** First-open draft: no server conversation yet, created on first send. */
draftConversation: false, draftConversation: false,
/** Prefilled input text waiting to be consumed by the chat UI. */
pendingInput: null,
conversationState: 'idle', conversationState: 'idle',
provider: 'openrouter', provider: 'openrouter',
model: DEFAULT_MODEL, model: DEFAULT_MODEL,
@@ -270,6 +272,33 @@ const actions = {
void chatAutomat.actions.loadConversations(); void chatAutomat.actions.loadConversations();
}, },
/**
* Open the chat drawer with a fresh draft conversation and prefilled prompt.
* @param {string} [prompt='']
*/
openWithPrompt(prompt = '') {
chatOpenedThisSession = true;
closeConversationStream();
chatAutomat.setState({
isOpen: true,
sseEstablished: true,
draftConversation: true,
selectedConversationId: null,
provider: 'openrouter',
model: DEFAULT_MODEL,
messages: [],
conversationState: 'idle',
streamPhase: 'idle',
streamingText: '',
lastUsage: null,
activeTurns: [],
sendError: null,
pendingInput: prompt,
});
ensureCompanyEventsConnected();
void chatAutomat.actions.loadConversations();
},
/** /**
* Start an implicit new conversation with the first preset model. Nothing is * Start an implicit new conversation with the first preset model. Nothing is
* created on the server until the first message is sent. * created on the server until the first message is sent.

View File

@@ -95,6 +95,7 @@ class AppDialog extends Component {
return ( return (
<Dialog <Dialog
disableEnforceFocus
{...rest} {...rest}
slots={restSlots} slots={restSlots}
slotProps={slotProps} slotProps={slotProps}

View File

@@ -11,11 +11,13 @@ import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import UploadFileIcon from '@mui/icons-material/UploadFile'; import UploadFileIcon from '@mui/icons-material/UploadFile';
import ImageSearchIcon from '@mui/icons-material/ImageSearch'; import ImageSearchIcon from '@mui/icons-material/ImageSearch';
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined'; import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import { article as articleApi } from '../../api'; import { article as articleApi } from '../../api';
import artikelAutomat from '../../automats/artikelAutomat'; import artikelAutomat from '../../automats/artikelAutomat';
import chatAutomat from '../../automats/chatAutomat';
import { requireArray, requireObject } from '../../utils/apiShape'; import { requireArray, requireObject } from '../../utils/apiShape';
import { import {
articleWithPhotoOrder, articleWithPhotoOrder,
@@ -65,6 +67,7 @@ class ArticlePhotosDialog extends Component {
this.handleSearchClose = this.handleSearchClose.bind(this); this.handleSearchClose = this.handleSearchClose.bind(this);
this.handleFileChange = this.handleFileChange.bind(this); this.handleFileChange = this.handleFileChange.bind(this);
this.uploadRawImage = this.uploadRawImage.bind(this); this.uploadRawImage = this.uploadRawImage.bind(this);
this.handleEditInChat = this.handleEditInChat.bind(this);
} }
handleClose() { handleClose() {
@@ -72,6 +75,14 @@ class ArticlePhotosDialog extends Component {
this.props.onClose(); this.props.onClose();
} }
handleEditInChat(photo, index) {
const { article: articleData } = this.props;
const sku = articleData?.sku ?? '';
const imageNr = index + 1;
const prompt = `Artikelbild Nr ${imageNr} von Artikel SKU ${sku} folgende Änderung: `;
chatAutomat.actions.openWithPrompt(prompt);
}
async uploadRawImage(rawBlob) { async uploadRawImage(rawBlob) {
const { article: articleData, onArticleUpdated } = this.props; const { article: articleData, onArticleUpdated } = this.props;
this.setState({ busy: true }); this.setState({ busy: true });
@@ -223,41 +234,58 @@ class ArticlePhotosDialog extends Component {
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
justifyContent: 'flex-end', alignItems: 'center',
gap: 0.25, justifyContent: 'space-between',
pointerEvents: 'auto', pointerEvents: 'auto',
}} }}
> >
<IconButton <IconButton
type="button" type="button"
size="small" size="small"
aria-label="Nach vorne" aria-label="Bearbeiten"
disabled={busy || index === 0}
onClick={() => void this.movePhoto(photo.fullHash, -1)}
sx={overlayIconSx}
>
<KeyboardArrowUpIcon fontSize="small" />
</IconButton>
<IconButton
type="button"
size="small"
aria-label="Nach hinten"
disabled={busy || index >= total - 1}
onClick={() => void this.movePhoto(photo.fullHash, 1)}
sx={overlayIconSx}
>
<KeyboardArrowDownIcon fontSize="small" />
</IconButton>
<IconButton
type="button"
size="small"
aria-label="Löschen"
disabled={busy} disabled={busy}
onClick={() => void this.deletePhoto(photo.fullHash)} onClick={() => this.handleEditInChat(photo, index)}
sx={overlayIconSx} sx={overlayIconSx}
> >
<DeleteOutlineOutlinedIcon fontSize="small" /> <EditOutlinedIcon fontSize="small" />
</IconButton> </IconButton>
<Box
sx={{
display: 'flex',
gap: 0.25,
}}
>
<IconButton
type="button"
size="small"
aria-label="Nach vorne"
disabled={busy || index === 0}
onClick={() => void this.movePhoto(photo.fullHash, -1)}
sx={overlayIconSx}
>
<KeyboardArrowUpIcon fontSize="small" />
</IconButton>
<IconButton
type="button"
size="small"
aria-label="Nach hinten"
disabled={busy || index >= total - 1}
onClick={() => void this.movePhoto(photo.fullHash, 1)}
sx={overlayIconSx}
>
<KeyboardArrowDownIcon fontSize="small" />
</IconButton>
<IconButton
type="button"
size="small"
aria-label="Löschen"
disabled={busy}
onClick={() => void this.deletePhoto(photo.fullHash)}
sx={overlayIconSx}
>
<DeleteOutlineOutlinedIcon fontSize="small" />
</IconButton>
</Box>
</Box> </Box>
</Box> </Box>
</Box> </Box>

View File

@@ -38,6 +38,7 @@ import { ACCEPT_STRING, readFileAttachment } from './chatAttachments';
import { ChatStagedAttachmentsTray } from './ChatAttachmentItems'; import { ChatStagedAttachmentsTray } from './ChatAttachmentItems';
import { import {
BLUE_GLOW, BLUE_GLOW,
DIALOG_FOG,
SHADOW_DRAWER, SHADOW_DRAWER,
} from '../../colors'; } from '../../colors';
@@ -69,7 +70,7 @@ export default class ChatDrawer extends PureComponent {
sseStatus: s.sseStatus, sseStatus: s.sseStatus,
sendError: s.sendError, sendError: s.sendError,
activeTurns: s.activeTurns || [], activeTurns: s.activeTurns || [],
inputText: '', inputText: s.pendingInput != null ? s.pendingInput : '',
isInputFocused: false, isInputFocused: false,
stagedAttachments: [], stagedAttachments: [],
stagedReferences: [], stagedReferences: [],
@@ -95,18 +96,39 @@ export default class ChatDrawer extends PureComponent {
}, delay); }, delay);
}; };
focusInputAtEnd = (delay = 50) => {
if (this.focusTimeout) clearTimeout(this.focusTimeout);
this.focusTimeout = setTimeout(() => {
const el = this.inputRef.current;
if (!el) return;
el.focus?.();
if (typeof el.setSelectionRange === 'function' && el.value) {
const len = el.value.length;
el.setSelectionRange(len, len);
}
}, delay);
};
componentDidMount() { componentDidMount() {
if (this.state.isOpen) { if (this.state.isOpen) {
this.focusInput(150); if (chatAutomat.state.pendingInput != null) {
this.focusInputAtEnd(150);
} else {
this.focusInput(150);
}
} }
void this.loadModelSettings(); void this.loadModelSettings();
if (chatAutomat.state.pendingInput != null) {
queueMicrotask(() => {
if (chatAutomat.state.pendingInput != null) {
chatAutomat.setState({ pendingInput: null });
}
});
}
this.unsub = chatAutomat.subscribe(this, (state) => { this.unsub = chatAutomat.subscribe(this, (state) => {
const wasOpen = this.state.isOpen; const wasOpen = this.state.isOpen;
const willOpen = state.isOpen; const willOpen = state.isOpen;
if (!wasOpen && willOpen) { const patch = {
this.focusInput(150);
}
return {
isOpen: state.isOpen, isOpen: state.isOpen,
conversations: state.conversations, conversations: state.conversations,
selectedConversationId: state.selectedConversationId, selectedConversationId: state.selectedConversationId,
@@ -123,6 +145,20 @@ export default class ChatDrawer extends PureComponent {
sendError: state.sendError, sendError: state.sendError,
activeTurns: state.activeTurns || [], activeTurns: state.activeTurns || [],
}; };
if (state.pendingInput != null) {
patch.inputText = state.pendingInput;
patch.stagedAttachments = [];
patch.stagedReferences = [];
queueMicrotask(() => {
if (chatAutomat.state.pendingInput != null) {
chatAutomat.setState({ pendingInput: null });
}
});
this.focusInputAtEnd(150);
} else if (!wasOpen && willOpen) {
this.focusInput(150);
}
return patch;
}); });
this.unsubSettings = userSettingsAutomat.subscribe(this, (state) => { this.unsubSettings = userSettingsAutomat.subscribe(this, (state) => {
@@ -1265,7 +1301,12 @@ export default class ChatDrawer extends PureComponent {
{/* Unified Copilot-style input container with context reference pills */} {/* Unified Copilot-style input container with context reference pills */}
<Box <Box
onClick={() => this.focusInput(0)} onClick={(e) => {
if (this.inputRef.current && (e.target === this.inputRef.current || this.inputRef.current.contains(e.target))) {
return;
}
this.focusInput(0);
}}
sx={{ sx={{
flex: 1, flex: 1,
minWidth: 0, minWidth: 0,
@@ -1463,8 +1504,14 @@ export default class ChatDrawer extends PureComponent {
anchor="right" anchor="right"
open={isOpen} open={isOpen}
onClose={this.handleClose} onClose={this.handleClose}
ModalProps={{
disableEnforceFocus: true,
}}
sx={(theme) => ({ sx={(theme) => ({
zIndex: theme.zIndex.drawer + 2, zIndex: theme.zIndex.modal + 10,
'& .MuiBackdrop-root': {
backgroundColor: DIALOG_FOG,
},
'& .MuiDrawer-paper': { '& .MuiDrawer-paper': {
width: { xs: '100vw', sm: `${totalWidth}px` }, width: { xs: '100vw', sm: `${totalWidth}px` },
maxWidth: '100vw', maxWidth: '100vw',

View File

@@ -61,7 +61,10 @@ export default class ChatFab extends PureComponent {
let tooltipTitle = 'KI-Chat öffnen'; let tooltipTitle = 'KI-Chat öffnen';
let ariaLabel = 'KI-Chat'; let ariaLabel = 'KI-Chat';
if (hasPendingAsk) { if (isOpen) {
tooltipTitle = 'KI-Chat schließen';
ariaLabel = 'KI-Chat schließen';
} else if (hasPendingAsk) {
tooltipTitle = pendingAsk?.question tooltipTitle = pendingAsk?.question
? `Antwort erforderlich: „${pendingAsk.question}“` ? `Antwort erforderlich: „${pendingAsk.question}“`
: 'Antwort erforderlich: Das Modell wartet auf Ihre Eingabe'; : 'Antwort erforderlich: Das Modell wartet auf Ihre Eingabe';
@@ -76,12 +79,8 @@ export default class ChatFab extends PureComponent {
sx={(theme) => ({ sx={(theme) => ({
position: 'fixed', position: 'fixed',
bottom: { xs: 20, sm: 28 }, bottom: { xs: 20, sm: 28 },
right: { xs: 20, sm: 28 }, left: { xs: 20, sm: 164 },
zIndex: theme.zIndex.speedDial || 1050, zIndex: theme.zIndex.speedDial || 1050,
opacity: isOpen ? 0 : 1,
transform: isOpen ? 'scale(0)' : 'scale(1)',
pointerEvents: isOpen ? 'none' : 'auto',
transition: 'transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s linear',
})} })}
> >
<Box sx={{ position: 'relative' }}> <Box sx={{ position: 'relative' }}>
@@ -144,7 +143,7 @@ export default class ChatFab extends PureComponent {
/> />
)} )}
<Tooltip title={tooltipTitle} placement="left" arrow disableHoverListener={isOpen}> <Tooltip title={tooltipTitle} placement="right" arrow>
<Badge <Badge
badgeContent={hasPendingAsk ? '?' : undefined} badgeContent={hasPendingAsk ? '?' : undefined}
color={hasPendingAsk ? 'error' : isBusy ? 'warning' : 'default'} color={hasPendingAsk ? 'error' : isBusy ? 'warning' : 'default'}
@@ -184,13 +183,6 @@ export default class ChatFab extends PureComponent {
: isBusy : isBusy
? CHAT_FAB_SHADOW_BUSY ? CHAT_FAB_SHADOW_BUSY
: CHAT_FAB_SHADOW_IDLE, : CHAT_FAB_SHADOW_IDLE,
transition: 'transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.2s',
'&:hover': {
transform: 'scale(1.06)',
},
'&:active': {
transform: 'scale(0.96)',
},
background: hasPendingAsk background: hasPendingAsk
? CHAT_FAB_PENDING ? CHAT_FAB_PENDING
: isBusy : isBusy

View File

@@ -22,6 +22,9 @@ import WebIcon from '@mui/icons-material/Web';
import EscalatorWarningIcon from '@mui/icons-material/EscalatorWarning'; import EscalatorWarningIcon from '@mui/icons-material/EscalatorWarning';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import ManageSearchIcon from '@mui/icons-material/ManageSearch'; import ManageSearchIcon from '@mui/icons-material/ManageSearch';
import LensBlurIcon from '@mui/icons-material/LensBlur';
import CalculateIcon from '@mui/icons-material/Calculate';
import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner';
import HealingMarkdown from './HealingMarkdown'; import HealingMarkdown from './HealingMarkdown';
import { VfsAttachmentLink, parseAttachmentsFromText } from './ChatAttachmentItems'; import { VfsAttachmentLink, parseAttachmentsFromText } from './ChatAttachmentItems';
import { import {
@@ -80,6 +83,9 @@ const TOOL_ICON_MAP = {
sub_agent: EscalatorWarningIcon, sub_agent: EscalatorWarningIcon,
pdf_read: PictureAsPdfIcon, pdf_read: PictureAsPdfIcon,
search_articles: ManageSearchIcon, search_articles: ManageSearchIcon,
edit_image: LensBlurIcon,
net_gross_calc: CalculateIcon,
ean_checksum: QrCodeScannerIcon,
}; };
function formatMessageTime(isoString) { function formatMessageTime(isoString) {

View File

@@ -430,12 +430,12 @@ class AutomatInspector extends PureComponent {
invisible={names.length === 0} invisible={names.length === 0}
sx={{ sx={{
position: 'fixed', position: 'fixed',
right: { xs: 27, sm: 35 }, left: { xs: 27, sm: 171 },
bottom: { xs: 84, sm: 94 }, bottom: { xs: 84, sm: 94 },
zIndex: (theme) => theme.zIndex.drawer - 1, zIndex: (theme) => theme.zIndex.drawer - 1,
}} }}
> >
<Tooltip title="Automat-Inspector (Alt+Shift+A)"> <Tooltip title="Automat-Inspector (Alt+Shift+A)" placement="right">
<Fab <Fab
size="small" size="small"
color="primary" color="primary"