u
This commit is contained in:
@@ -5,7 +5,6 @@ import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Button from '@mui/material/Button';
|
||||
import InputBase from '@mui/material/InputBase';
|
||||
import TextInput from '../TextInput';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
@@ -32,7 +31,7 @@ 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 { VfsEditorPanel, VfsFileIcon } from '../vfs';
|
||||
import ChatFlowList from './ChatFlowList';
|
||||
import AskUserPrompt from './AskUserPrompt';
|
||||
import { ACCEPT_STRING, readFileAttachment } from './chatAttachments';
|
||||
@@ -71,7 +70,9 @@ export default class ChatDrawer extends PureComponent {
|
||||
sendError: s.sendError,
|
||||
activeTurns: s.activeTurns || [],
|
||||
inputText: '',
|
||||
isInputFocused: false,
|
||||
stagedAttachments: [],
|
||||
stagedReferences: [],
|
||||
isProcessingFiles: false,
|
||||
isDraggingFiles: false,
|
||||
attachmentError: null,
|
||||
@@ -150,12 +151,68 @@ export default class ChatDrawer extends PureComponent {
|
||||
};
|
||||
|
||||
handleInsertToChat = (text) => {
|
||||
// If it's a file / line tag, e.g. [file.js#L10-L20](file.js#L10-L20) or [file.js](file.js)
|
||||
const match =
|
||||
typeof text === 'string' &&
|
||||
text.trim().match(/^(!?)\[([^#\]]+)(?:#L?(\d+)(?:-L?(\d+))?)?\]\(([^)]+)\)$/);
|
||||
if (match) {
|
||||
const [, isImg, fileName, startLine, endLine, path] = match;
|
||||
this.handleMentionReference({
|
||||
fileName,
|
||||
path,
|
||||
startLine: startLine ? parseInt(startLine, 10) : null,
|
||||
endLine: endLine ? parseInt(endLine, 10) : (startLine ? parseInt(startLine, 10) : null),
|
||||
isImage: Boolean(isImg),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState((prev) => ({
|
||||
inputText: prev.inputText ? `${prev.inputText}\n${text}` : text,
|
||||
}));
|
||||
this.focusInput(50);
|
||||
};
|
||||
|
||||
handleMentionReference = (refData) => {
|
||||
if (!refData || !refData.fileName) return;
|
||||
const {
|
||||
fileName,
|
||||
path = refData.fileName,
|
||||
startLine = null,
|
||||
endLine = null,
|
||||
selectedText = '',
|
||||
isImage = false,
|
||||
} = refData;
|
||||
|
||||
this.setState((prev) => {
|
||||
const exists = prev.stagedReferences.some(
|
||||
(r) => r.fileName === fileName && r.startLine === startLine && r.endLine === endLine
|
||||
);
|
||||
if (exists) {
|
||||
return null;
|
||||
}
|
||||
const newRef = {
|
||||
id: `ref-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
fileName,
|
||||
path,
|
||||
startLine,
|
||||
endLine,
|
||||
selectedText,
|
||||
isImage,
|
||||
};
|
||||
return {
|
||||
stagedReferences: [...prev.stagedReferences, newRef],
|
||||
};
|
||||
});
|
||||
this.focusInput(50);
|
||||
};
|
||||
|
||||
handleRemoveReference = (id) => {
|
||||
this.setState((prev) => ({
|
||||
stagedReferences: prev.stagedReferences.filter((r) => r.id !== id),
|
||||
}));
|
||||
this.focusInput(50);
|
||||
};
|
||||
|
||||
handleVfsResizerStart = (e) => {
|
||||
if (e.button !== undefined && e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
@@ -327,12 +384,51 @@ export default class ChatDrawer extends PureComponent {
|
||||
|
||||
handleSend = async (e) => {
|
||||
e?.preventDefault();
|
||||
const text = this.state.inputText.trim();
|
||||
const rawText = this.state.inputText.trim();
|
||||
const attachments = this.state.stagedAttachments || [];
|
||||
if ((!text && attachments.length === 0) || this.state.conversationState === 'busy' || this.state.isSending) {
|
||||
const references = this.state.stagedReferences || [];
|
||||
|
||||
if (
|
||||
(!rawText && attachments.length === 0 && references.length === 0) ||
|
||||
this.state.conversationState === 'busy' ||
|
||||
this.state.isSending
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.setState({ inputText: '', stagedAttachments: [], attachmentError: null });
|
||||
|
||||
let text = rawText;
|
||||
if (references.length > 0) {
|
||||
const refMarkdown = references
|
||||
.map((ref) => {
|
||||
const lineRef = ref.startLine
|
||||
? (ref.startLine === ref.endLine ? `L${ref.startLine}` : `L${ref.startLine}-L${ref.endLine}`)
|
||||
: null;
|
||||
const lineLabel = ref.startLine
|
||||
? (ref.startLine === ref.endLine ? `:${ref.startLine}` : `:${ref.startLine}-${ref.endLine}`)
|
||||
: '';
|
||||
const link =
|
||||
ref.isImage && !lineRef
|
||||
? ``
|
||||
: `[${ref.fileName}${lineLabel}](${ref.path || ref.fileName}${lineRef ? `#${lineRef}` : ''})`;
|
||||
|
||||
if (ref.selectedText && ref.selectedText.trim()) {
|
||||
const ext = ref.fileName.split('.').pop() || '';
|
||||
return `${link}\n\`\`\`${ext}\n${ref.selectedText.trim()}\n\`\`\``;
|
||||
}
|
||||
return link;
|
||||
})
|
||||
.join('\n\n');
|
||||
|
||||
text = text ? `${refMarkdown}\n\n${text}` : refMarkdown;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
inputText: '',
|
||||
stagedAttachments: [],
|
||||
stagedReferences: [],
|
||||
attachmentError: null,
|
||||
});
|
||||
|
||||
try {
|
||||
await chatAutomat.actions.sendMessage(text, attachments);
|
||||
} catch {
|
||||
@@ -1009,8 +1105,10 @@ export default class ChatDrawer extends PureComponent {
|
||||
selectedConversationId,
|
||||
draftConversation,
|
||||
inputText,
|
||||
isInputFocused,
|
||||
sendError,
|
||||
stagedAttachments,
|
||||
stagedReferences,
|
||||
isProcessingFiles,
|
||||
isDraggingFiles,
|
||||
attachmentError,
|
||||
@@ -1020,7 +1118,10 @@ export default class ChatDrawer extends PureComponent {
|
||||
const isBusy = conversationState === 'busy';
|
||||
const isWorking = isBusy || isSending;
|
||||
const canChat = (Boolean(selectedConversationId) || draftConversation) && !pendingAsk;
|
||||
const canSend = !isWorking && canChat && (Boolean(inputText.trim()) || stagedAttachments.length > 0);
|
||||
const canSend =
|
||||
!isWorking &&
|
||||
canChat &&
|
||||
(Boolean(inputText.trim()) || stagedAttachments.length > 0 || stagedReferences.length > 0);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -1164,24 +1265,158 @@ export default class ChatDrawer extends PureComponent {
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
inputRef={this.inputRef}
|
||||
multiline
|
||||
maxRows={4}
|
||||
fullWidth
|
||||
value={inputText}
|
||||
onChange={(e) => this.setState({ inputText: e.target.value })}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onPaste={this.handlePaste}
|
||||
disabled={!canChat}
|
||||
placeholder={
|
||||
pendingAsk
|
||||
? 'Bitte Frage oben beantworten…'
|
||||
: stagedAttachments.length > 0
|
||||
? 'Nachricht hinzufügen oder direkt absenden…'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{/* Unified Copilot-style input container with context reference pills */}
|
||||
<Box
|
||||
onClick={() => this.focusInput(0)}
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
bgcolor: 'background.paper',
|
||||
border: '1px solid',
|
||||
borderColor: isInputFocused ? 'primary.main' : 'divider',
|
||||
boxShadow: isInputFocused ? (theme) => `0 0 0 1px ${theme.palette.primary.main}` : 'none',
|
||||
borderRadius: 2.5,
|
||||
p: 0.75,
|
||||
gap: 0.5,
|
||||
cursor: 'text',
|
||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||||
minHeight: 40,
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
{stagedReferences.length > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 0.6,
|
||||
alignItems: 'center',
|
||||
mb: 0.25,
|
||||
}}
|
||||
>
|
||||
{stagedReferences.map((ref) => (
|
||||
<Box
|
||||
key={ref.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
vfsAutomat.actions.openVfs();
|
||||
vfsAutomat.actions.selectFile(ref.fileName);
|
||||
}}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.6,
|
||||
py: 0.2,
|
||||
pl: 0.7,
|
||||
pr: 0.4,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.08)'
|
||||
: 'rgba(0, 0, 0, 0.05)',
|
||||
border: '1px solid',
|
||||
borderColor: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.12)'
|
||||
: 'rgba(0, 0, 0, 0.1)',
|
||||
color: 'text.primary',
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: 1.2,
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
'&:hover': {
|
||||
bgcolor: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.14)'
|
||||
: 'rgba(0, 0, 0, 0.09)',
|
||||
borderColor: 'primary.light',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VfsFileIcon fileName={ref.fileName} sx={{ fontSize: 14 }} />
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
fontFamily: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
|
||||
fontSize: '0.74rem',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '-0.2px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{ref.fileName}
|
||||
{ref.startLine && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
fontWeight: 700,
|
||||
ml: 0.3,
|
||||
}}
|
||||
>
|
||||
:{ref.startLine === ref.endLine ? ref.startLine : `${ref.startLine}-${ref.endLine}`}
|
||||
</Box>
|
||||
)}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
this.handleRemoveReference(ref.id);
|
||||
}}
|
||||
aria-label="Referenz entfernen"
|
||||
sx={{
|
||||
p: 0.2,
|
||||
ml: 0.1,
|
||||
color: 'text.secondary',
|
||||
'&:hover': {
|
||||
color: 'error.main',
|
||||
bgcolor: 'action.hover',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseRoundedIcon sx={{ fontSize: 13 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<InputBase
|
||||
inputRef={this.inputRef}
|
||||
multiline
|
||||
minRows={1}
|
||||
maxRows={5}
|
||||
fullWidth
|
||||
value={inputText}
|
||||
onChange={(e) => this.setState({ inputText: e.target.value })}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onPaste={this.handlePaste}
|
||||
onFocus={() => this.setState({ isInputFocused: true })}
|
||||
onBlur={() => this.setState({ isInputFocused: false })}
|
||||
disabled={!canChat}
|
||||
placeholder={
|
||||
pendingAsk
|
||||
? 'Bitte Frage oben beantworten…'
|
||||
: stagedAttachments.length > 0
|
||||
? 'Nachricht hinzufügen oder direkt absenden…'
|
||||
: undefined
|
||||
}
|
||||
sx={{
|
||||
px: 0.5,
|
||||
py: 0.1,
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: 1.45,
|
||||
'& .MuiInputBase-input': {
|
||||
p: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<IconButton
|
||||
type="submit"
|
||||
@@ -1318,7 +1553,10 @@ export default class ChatDrawer extends PureComponent {
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<VfsEditorPanel onInsertToChat={this.handleInsertToChat} />
|
||||
<VfsEditorPanel
|
||||
onInsertToChat={this.handleInsertToChat}
|
||||
onMentionReference={this.handleMentionReference}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user