This commit is contained in:
seb
2026-10-18 23:32:53 +02:00
parent f7ee9182ec
commit 9b40d1db87
8 changed files with 1176 additions and 64 deletions

View File

@@ -47,6 +47,8 @@ import ChatFlowList from './ChatFlowList';
import AskUserPrompt from './AskUserPrompt';
import { ACCEPT_STRING, readFileAttachment } from './chatAttachments';
import { ChatStagedAttachmentsTray } from './ChatAttachmentItems';
import ChatMentionAutocomplete from './ChatMentionAutocomplete';
import { getToolMentionToken, getFileMentionToken, searchTools } from './chatTools.jsx';
import {
BLUE_GLOW,
DIALOG_FOG,
@@ -172,6 +174,12 @@ export default class ChatDrawer extends PureComponent {
modelslugflag: 'floor',
modelreasoning: 'medium',
modelslug: 'openai/gpt-6-luna',
vfsFiles: vfs.files || [],
mentionOpen: false,
mentionType: null,
mentionQuery: '',
mentionStartPos: 0,
mentionSelectedIndex: 0,
};
this.inputRef = React.createRef();
this.fileInputRef = React.createRef();
@@ -295,6 +303,7 @@ export default class ChatDrawer extends PureComponent {
this.unsubVfs = vfsAutomat.subscribe(this, (state) => ({
isVfsOpen: state.isOpen,
vfsWidth: state.vfsWidth,
vfsFiles: state.files,
}));
}
@@ -641,6 +650,10 @@ export default class ChatDrawer extends PureComponent {
stagedReferences: [],
stagedExtractions: [],
attachmentError: null,
mentionOpen: false,
mentionType: null,
mentionQuery: '',
mentionSelectedIndex: 0,
});
try {
@@ -746,7 +759,140 @@ export default class ChatDrawer extends PureComponent {
}
};
updateMentionState = (value, cursorPos) => {
const textBeforeCursor = value.slice(0, cursorPos);
const mentionMatch = textBeforeCursor.match(/(?:^|\s)([@§#])(?:\[([^\]]*)|\"([^\"]*)|([^\s@§#]*))$/);
if (mentionMatch) {
const symbol = mentionMatch[1];
const query = mentionMatch[2] ?? mentionMatch[3] ?? mentionMatch[4] ?? '';
const symbolIndex = textBeforeCursor.lastIndexOf(symbol);
const isFile = symbol === '@';
const isTool = symbol === '§' || symbol === '#';
if (isFile && (!this.state.vfsFiles || this.state.vfsFiles.length === 0)) {
void vfsAutomat.actions.loadFiles();
}
this.setState({
inputText: value,
mentionOpen: true,
mentionType: isFile ? 'file' : isTool ? 'tool' : null,
mentionQuery: query,
mentionStartPos: symbolIndex,
mentionSelectedIndex: 0,
});
} else {
this.setState({
inputText: value,
mentionOpen: false,
mentionType: null,
mentionQuery: '',
mentionSelectedIndex: 0,
});
}
};
handleInputChange = (e) => {
const value = e.target.value;
const cursorPos = e.target.selectionStart ?? value.length;
this.updateMentionState(value, cursorPos);
};
handleInputSelect = (e) => {
const cursorPos = e.target.selectionStart;
if (cursorPos != null && this.state.mentionOpen) {
this.updateMentionState(this.state.inputText, cursorPos);
}
};
handleMentionSelect = (item) => {
if (!item) return;
const { inputText, mentionType, mentionStartPos } = this.state;
const inputEl = this.inputRef.current;
const cursorPos = inputEl?.selectionStart ?? inputText.length;
let token = '';
if (mentionType === 'tool') {
token = getToolMentionToken(item);
} else if (mentionType === 'file') {
token = getFileMentionToken(item.name || item);
}
const before = inputText.slice(0, mentionStartPos);
const after = inputText.slice(cursorPos);
const replacement = `${token} `;
const newText = `${before}${replacement}${after}`;
const newCursorPos = before.length + replacement.length;
this.setState(
{
inputText: newText,
mentionOpen: false,
mentionType: null,
mentionQuery: '',
mentionSelectedIndex: 0,
},
() => {
if (inputEl) {
inputEl.focus();
try {
inputEl.selectionStart = newCursorPos;
inputEl.selectionEnd = newCursorPos;
} catch {}
}
}
);
};
handleKeyDown = (e) => {
const { mentionOpen, mentionType, mentionQuery, mentionSelectedIndex, vfsFiles } = this.state;
if (mentionOpen && mentionType) {
let items = [];
if (mentionType === 'tool') {
items = searchTools(mentionQuery);
} else {
const q = (mentionQuery || '').toLowerCase().trim();
items = (vfsFiles || []).filter((f) => !q || (f.name || '').toLowerCase().includes(q));
}
if (e.key === 'ArrowDown') {
e.preventDefault();
if (items.length > 0) {
this.setState({
mentionSelectedIndex: (mentionSelectedIndex + 1) % items.length,
});
}
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
if (items.length > 0) {
this.setState({
mentionSelectedIndex: (mentionSelectedIndex - 1 + items.length) % items.length,
});
}
return;
}
if (e.key === 'Enter' || e.key === 'Tab') {
if (items.length > 0) {
e.preventDefault();
const selected = items[mentionSelectedIndex] || items[0];
this.handleMentionSelect(selected);
return;
}
}
if (e.key === 'Escape') {
e.preventDefault();
this.setState({ mentionOpen: false, mentionType: null });
return;
}
}
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
void this.handleSend();
@@ -1507,7 +1653,18 @@ export default class ChatDrawer extends PureComponent {
</Box>
)}
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1, position: 'relative' }}>
<ChatMentionAutocomplete
open={this.state.mentionOpen}
type={this.state.mentionType}
query={this.state.mentionQuery}
files={this.state.vfsFiles}
selectedIndex={this.state.mentionSelectedIndex}
onSelect={this.handleMentionSelect}
onHoverIndex={(idx) => this.setState({ mentionSelectedIndex: idx })}
onClose={() => this.setState({ mentionOpen: false, mentionType: null })}
/>
{/* Attachment Icon Button */}
<Tooltip title="Dateien anhängen (Bilder, PDFs, CSV, Text, XML, HTML)">
<span>
@@ -1752,11 +1909,16 @@ export default class ChatDrawer extends PureComponent {
maxRows={5}
fullWidth
value={inputText}
onChange={(e) => this.setState({ inputText: e.target.value })}
onChange={this.handleInputChange}
onSelect={this.handleInputSelect}
onKeyDown={this.handleKeyDown}
onPaste={this.handlePaste}
onFocus={() => this.setState({ isInputFocused: true })}
onBlur={() => this.setState({ isInputFocused: false })}
onBlur={() => {
setTimeout(() => {
this.setState({ isInputFocused: false });
}, 200);
}}
disabled={!canChat}
inputProps={{
spellCheck: false,
@@ -1769,7 +1931,7 @@ export default class ChatDrawer extends PureComponent {
? 'Bitte Frage oben beantworten…'
: stagedAttachments.length > 0
? 'Nachricht hinzufügen oder direkt absenden…'
: undefined
: '@ Datei · § Werkzeug erwähnen…'
}
sx={{
px: 0.5,