From 8f46e59f08748fc401b35c18866b2745f81e8e4b Mon Sep 17 00:00:00 2001 From: seb Date: Wed, 7 Oct 2026 13:36:16 +0200 Subject: [PATCH] u --- src/automats/chatAutomat.js | 18 +- src/automats/vfsAutomat.js | 2 +- src/components/article/ArticleSearch.jsx | 2 + src/components/article/ArticleUnifyDialog.jsx | 2 - .../article/boxes/ArticleViewLists.jsx | 1 - src/components/article/searchFormat.js | 2 +- .../article/tabs/ArticleSettingsTab.jsx | 1 - src/components/article/withSearchParams.jsx | 3 +- src/components/chat/ChatAttachmentItems.jsx | 2 - src/components/chat/ChatDrawer.jsx | 288 ++++++++++++++++-- src/components/chat/HealingMarkdown.jsx | 2 - src/components/manifest/ManifestFormLine.jsx | 2 +- src/components/tiles/ArticleTile.jsx | 1 - src/components/vfs/VfsEditorPanel.jsx | 26 +- src/components/vfs/VfsFileExplorer.jsx | 2 - src/components/vfs/VfsFilePreview.jsx | 2 +- src/components/vfs/VfsNewFileDialog.jsx | 2 +- src/components/vfs/VfsTextEditor.jsx | 5 +- src/components/vfs/vfsUtils.js | 2 +- 19 files changed, 299 insertions(+), 66 deletions(-) diff --git a/src/automats/chatAutomat.js b/src/automats/chatAutomat.js index 3f28982..b47fa52 100644 --- a/src/automats/chatAutomat.js +++ b/src/automats/chatAutomat.js @@ -966,16 +966,14 @@ const actions = { * @param {object[]} [maybeAttachments] */ async sendMessage(textOrPayload, maybeAttachments = []) { - let trimmed = ''; - let fileAttachments = []; - - if (typeof textOrPayload === 'object' && textOrPayload !== null) { - trimmed = (textOrPayload.text || '').trim(); - fileAttachments = Array.isArray(textOrPayload.attachments) ? textOrPayload.attachments : []; - } else { - trimmed = (textOrPayload || '').trim(); - fileAttachments = Array.isArray(maybeAttachments) ? maybeAttachments : []; - } + const trimmed = + typeof textOrPayload === 'object' && textOrPayload !== null + ? (textOrPayload.text || '').trim() + : (textOrPayload || '').trim(); + const fileAttachments = + typeof textOrPayload === 'object' && textOrPayload !== null + ? (Array.isArray(textOrPayload.attachments) ? textOrPayload.attachments : []) + : (Array.isArray(maybeAttachments) ? maybeAttachments : []); if (!trimmed && fileAttachments.length === 0) return; diff --git a/src/automats/vfsAutomat.js b/src/automats/vfsAutomat.js index 05899e4..368b21b 100644 --- a/src/automats/vfsAutomat.js +++ b/src/automats/vfsAutomat.js @@ -246,7 +246,7 @@ const actions = { * @param {string} [initialContent=''] */ async createFile(name, initialContent = '') { - const clean = String(name || '').trim().replace(/^[\/\\]+/, ''); + const clean = String(name || '').trim().replace(/^[/\\]+/, ''); if (!clean) throw new Error('Dateiname darf nicht leer sein'); vfsAutomat.setState({ isSaving: true, saveError: null, newFileDialogOpen: false }); diff --git a/src/components/article/ArticleSearch.jsx b/src/components/article/ArticleSearch.jsx index 5e8a329..b7a912e 100644 --- a/src/components/article/ArticleSearch.jsx +++ b/src/components/article/ArticleSearch.jsx @@ -890,6 +890,7 @@ export default class ArticleSearch extends PureComponent { return ( { + // 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}](${ref.path || ref.fileName})` + : `[${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 ( - 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 */} + 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 && ( + + {stagedReferences.map((ref) => ( + { + 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', + }, + }} + > + + + {ref.fileName} + {ref.startLine && ( + + :{ref.startLine === ref.endLine ? ref.startLine : `${ref.startLine}-${ref.endLine}`} + + )} + + { + 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', + }, + }} + > + + + + ))} + + )} + + 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, + }, + }} + /> + - + )} diff --git a/src/components/chat/HealingMarkdown.jsx b/src/components/chat/HealingMarkdown.jsx index 4a576a3..ada8f7e 100644 --- a/src/components/chat/HealingMarkdown.jsx +++ b/src/components/chat/HealingMarkdown.jsx @@ -25,8 +25,6 @@ import { VfsAttachmentLink, parseAttachmentTag, classifyLinkTarget, resolveVfsUr * ~~strike~~, [links](url) and ![images](url). */ -const SAFE_URL = /^(https?:\/\/|mailto:|\/|#)/i; - /** * Resolve an image target to something the browser may load. * diff --git a/src/components/manifest/ManifestFormLine.jsx b/src/components/manifest/ManifestFormLine.jsx index bf666fe..d87f6f1 100644 --- a/src/components/manifest/ManifestFormLine.jsx +++ b/src/components/manifest/ManifestFormLine.jsx @@ -523,7 +523,7 @@ class ManifestFormLine extends PureComponent { } renderDeliverySecondLine(rawSku) { - const { line, supplierArticle, loading } = this.props; + const { supplierArticle, loading } = this.props; if (loading) { return ( diff --git a/src/components/tiles/ArticleTile.jsx b/src/components/tiles/ArticleTile.jsx index 74b7312..cdea611 100644 --- a/src/components/tiles/ArticleTile.jsx +++ b/src/components/tiles/ArticleTile.jsx @@ -16,7 +16,6 @@ import { import { BrandLabel } from '../brand'; import { thumb } from '../../api'; import { - GREEN, GREEN_BRIGHT, INK_STRONG, SCRIM_LABEL, diff --git a/src/components/vfs/VfsEditorPanel.jsx b/src/components/vfs/VfsEditorPanel.jsx index 5ecce1f..adbaedc 100644 --- a/src/components/vfs/VfsEditorPanel.jsx +++ b/src/components/vfs/VfsEditorPanel.jsx @@ -62,18 +62,24 @@ export default class VfsEditorPanel extends PureComponent { handleMentionInChat = (file, lineInfo = null) => { if (!file) return; + if (this.props.onMentionReference) { + this.props.onMentionReference({ + fileName: file.name, + path: file.name, + startLine: lineInfo?.startLine || null, + endLine: lineInfo?.endLine || null, + selectedText: lineInfo?.selectedText || '', + isImage: file.isImage || false, + }); + return; + } if (this.props.onInsertToChat) { const isImg = file.isImage; - let tag = ''; - if (lineInfo?.startLine) { - const lineRef = - lineInfo.startLine === lineInfo.endLine - ? `L${lineInfo.startLine}` - : `L${lineInfo.startLine}-L${lineInfo.endLine}`; - tag = `[${file.name}#${lineRef}](${file.name}#${lineRef})`; - } else { - tag = isImg ? `![${file.name}](${file.name})` : `[${file.name}](${file.name})`; - } + const tag = lineInfo?.startLine + ? `[${file.name}#${lineInfo.startLine === lineInfo.endLine ? `L${lineInfo.startLine}` : `L${lineInfo.startLine}-L${lineInfo.endLine}`}](${file.name}#${lineInfo.startLine === lineInfo.endLine ? `L${lineInfo.startLine}` : `L${lineInfo.startLine}-L${lineInfo.endLine}`})` + : isImg + ? `![${file.name}](${file.name})` + : `[${file.name}](${file.name})`; this.props.onInsertToChat(tag); } }; diff --git a/src/components/vfs/VfsFileExplorer.jsx b/src/components/vfs/VfsFileExplorer.jsx index ada6e12..fc5564b 100644 --- a/src/components/vfs/VfsFileExplorer.jsx +++ b/src/components/vfs/VfsFileExplorer.jsx @@ -466,11 +466,9 @@ export default class VfsFileExplorer extends PureComponent { render() { const { loading, - onRefresh, onOpenNewDialog, searchQuery, onSearchChange, - files, } = this.props; const { menuAnchor, menuFile, isDraggingOver } = this.state; diff --git a/src/components/vfs/VfsFilePreview.jsx b/src/components/vfs/VfsFilePreview.jsx index fc38916..0ce26ba 100644 --- a/src/components/vfs/VfsFilePreview.jsx +++ b/src/components/vfs/VfsFilePreview.jsx @@ -125,7 +125,7 @@ export default class VfsFilePreview extends PureComponent { } render() { - const { fileName, file, onClose, onDownload, onMentionInChat } = this.props; + const { fileName, file, onClose, onMentionInChat } = this.props; const vfsUrl = `/api/vfs/${encodeURIComponent(fileName).replace(/%2F/g, '/')}?raw=1`; return ( diff --git a/src/components/vfs/VfsNewFileDialog.jsx b/src/components/vfs/VfsNewFileDialog.jsx index bc51a46..e661376 100644 --- a/src/components/vfs/VfsNewFileDialog.jsx +++ b/src/components/vfs/VfsNewFileDialog.jsx @@ -56,7 +56,7 @@ export default class VfsNewFileDialog extends PureComponent { handleSubmit = (e) => { e?.preventDefault(); - const clean = this.state.filename.trim().replace(/^[\/\\]+/, ''); + const clean = this.state.filename.trim().replace(/^[/\\]+/, ''); if (!clean) { this.setState({ error: 'Bitte gib einen Dateinamen ein.' }); return; diff --git a/src/components/vfs/VfsTextEditor.jsx b/src/components/vfs/VfsTextEditor.jsx index a6f2d1a..af0acc8 100644 --- a/src/components/vfs/VfsTextEditor.jsx +++ b/src/components/vfs/VfsTextEditor.jsx @@ -152,7 +152,6 @@ export default class VfsTextEditor extends PureComponent { loadingContent, onSave, onClose, - onDownload, onMentionInChat, } = this.props; @@ -316,7 +315,7 @@ export default class VfsTextEditor extends PureComponent { autoCorrect="off" autoCapitalize="off" wrap={this.state.wrapLines ? 'soft' : 'off'} - sx={(theme) => ({ + sx={{ flexGrow: 1, height: '100%', p: 1.5, @@ -337,7 +336,7 @@ export default class VfsTextEditor extends PureComponent { '&:focus': { outline: 'none', }, - })} + }} /> )} diff --git a/src/components/vfs/vfsUtils.js b/src/components/vfs/vfsUtils.js index 70f0788..b1dc879 100644 --- a/src/components/vfs/vfsUtils.js +++ b/src/components/vfs/vfsUtils.js @@ -85,7 +85,7 @@ export function getVfsCategory(filePath, mime = '') { * @returns {{ dir: string, base: string }} */ export function splitVfsPath(path) { - const clean = String(path || '').replace(/^[\/\\]+/, ''); + const clean = String(path || '').replace(/^[/\\]+/, ''); const lastSlash = clean.lastIndexOf('/'); if (lastSlash === -1) { return { dir: '', base: clean };