u
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user