fix: paste-as-a-file wurde korregiert, dass lange prompt dennoch in prompt fenster eingefügt wird und somit als prompt erkannt werden; stattdessen anzeige als truncated nachricht (ab 1000 wörter)
All checks were successful
Deploy Nyla Frontend to Integration / deploy (push) Successful in 1m22s
All checks were successful
Deploy Nyla Frontend to Integration / deploy (push) Successful in 1m22s
This commit is contained in:
parent
a01ebed7af
commit
d787e2b2b7
3 changed files with 108 additions and 62 deletions
|
|
@ -33,6 +33,108 @@ interface ChatStreamProps {
|
||||||
onOpenEditor?: () => void;
|
onOpenEditor?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LONG_MESSAGE_WORD_LIMIT = 1000;
|
||||||
|
const COLLAPSED_PREVIEW_WORDS = 200;
|
||||||
|
|
||||||
|
function _countWords(text: string): number {
|
||||||
|
const trimmed = text.trim();
|
||||||
|
if (!trimmed) return 0;
|
||||||
|
return trimmed.split(/\s+/).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _truncateToWords(text: string, maxWords: number): string {
|
||||||
|
const words = text.trim().split(/\s+/);
|
||||||
|
if (words.length <= maxWords) return text;
|
||||||
|
return `${words.slice(0, maxWords).join(' ')}…`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _MARKDOWN_COMPONENTS = {
|
||||||
|
code: _CodeBlock,
|
||||||
|
table: ({ children }: { children?: React.ReactNode }) => (
|
||||||
|
<div style={{ overflowX: 'auto', margin: '8px 0' }}>
|
||||||
|
<table style={{
|
||||||
|
borderCollapse: 'collapse',
|
||||||
|
width: '100%',
|
||||||
|
fontSize: 13,
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
th: ({ children }: { children?: React.ReactNode }) => (
|
||||||
|
<th style={{
|
||||||
|
borderBottom: '2px solid #ddd',
|
||||||
|
padding: '6px 10px',
|
||||||
|
textAlign: 'left',
|
||||||
|
fontWeight: 600,
|
||||||
|
background: '#f8f9fa',
|
||||||
|
fontSize: 12,
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
),
|
||||||
|
td: ({ children }: { children?: React.ReactNode }) => (
|
||||||
|
<td style={{
|
||||||
|
borderBottom: '1px solid #eee',
|
||||||
|
padding: '5px 10px',
|
||||||
|
fontSize: 12,
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
a: ({ href, children }: { href?: string; children?: React.ReactNode }) => (
|
||||||
|
<a href={href} target="_blank" rel="noopener noreferrer" style={{ color: 'var(--primary-color, #F25843)' }}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
img: ({ src, alt, ...rest }: React.ImgHTMLAttributes<HTMLImageElement>) =>
|
||||||
|
src ? <img src={src} alt={alt || ''} {...rest} style={{ maxWidth: '100%', borderRadius: 6 }} /> : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function _CollapsibleMessageMarkdown({
|
||||||
|
text,
|
||||||
|
allowCollapse,
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
allowCollapse: boolean;
|
||||||
|
}) {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const wordCount = _countWords(text);
|
||||||
|
const isLong = wordCount > LONG_MESSAGE_WORD_LIMIT;
|
||||||
|
const shouldTruncate = isLong && allowCollapse && !expanded;
|
||||||
|
const displayText = shouldTruncate ? _truncateToWords(text, COLLAPSED_PREVIEW_WORDS) : text;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={_MARKDOWN_COMPONENTS}>
|
||||||
|
{displayText}
|
||||||
|
</ReactMarkdown>
|
||||||
|
{isLong && allowCollapse && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setExpanded(prev => !prev)}
|
||||||
|
style={{
|
||||||
|
marginTop: 6,
|
||||||
|
padding: 0,
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
color: 'var(--primary-color, #F25843)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
textAlign: 'left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{expanded
|
||||||
|
? t('Weniger anzeigen')
|
||||||
|
: t('Mehr anzeigen ({n} Wörter)', { n: String(wordCount) })}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const ChatStream: React.FC<ChatStreamProps> = ({ messages,
|
export const ChatStream: React.FC<ChatStreamProps> = ({ messages,
|
||||||
agentProgress,
|
agentProgress,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
|
|
@ -49,6 +151,8 @@ export const ChatStream: React.FC<ChatStreamProps> = ({ messages,
|
||||||
const enqueuedIdsRef = useRef<Set<string>>(new Set());
|
const enqueuedIdsRef = useRef<Set<string>>(new Set());
|
||||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const lastMessageId = messages.length > 0 ? messages[messages.length - 1].id : null;
|
||||||
|
|
||||||
const _handleCopy = useCallback((msgId: string, text: string) => {
|
const _handleCopy = useCallback((msgId: string, text: string) => {
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
setCopiedId(msgId);
|
setCopiedId(msgId);
|
||||||
|
|
@ -157,53 +261,10 @@ export const ChatStream: React.FC<ChatStreamProps> = ({ messages,
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{msg.message && (
|
{msg.message && (
|
||||||
<ReactMarkdown
|
<_CollapsibleMessageMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
text={msg.message}
|
||||||
components={{
|
allowCollapse={!(isProcessing && msg.id === lastMessageId && msg.role === 'assistant')}
|
||||||
code: _CodeBlock,
|
/>
|
||||||
table: ({ children }) => (
|
|
||||||
<div style={{ overflowX: 'auto', margin: '8px 0' }}>
|
|
||||||
<table style={{
|
|
||||||
borderCollapse: 'collapse',
|
|
||||||
width: '100%',
|
|
||||||
fontSize: 13,
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
th: ({ children }) => (
|
|
||||||
<th style={{
|
|
||||||
borderBottom: '2px solid #ddd',
|
|
||||||
padding: '6px 10px',
|
|
||||||
textAlign: 'left',
|
|
||||||
fontWeight: 600,
|
|
||||||
background: '#f8f9fa',
|
|
||||||
fontSize: 12,
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</th>
|
|
||||||
),
|
|
||||||
td: ({ children }) => (
|
|
||||||
<td style={{
|
|
||||||
borderBottom: '1px solid #eee',
|
|
||||||
padding: '5px 10px',
|
|
||||||
fontSize: 12,
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</td>
|
|
||||||
),
|
|
||||||
a: ({ href, children }) => (
|
|
||||||
<a href={href} target="_blank" rel="noopener noreferrer" style={{ color: 'var(--primary-color, #F25843)' }}>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
img: ({ src, alt, ...rest }) =>
|
|
||||||
src ? <img src={src} alt={alt || ''} {...rest} style={{ maxWidth: '100%', borderRadius: 6 }} /> : null,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{msg.message}
|
|
||||||
</ReactMarkdown>
|
|
||||||
)}
|
)}
|
||||||
{msg.documents && msg.documents.length > 0 && (
|
{msg.documents && msg.documents.length > 0 && (
|
||||||
<div style={{ marginTop: msg.message ? 8 : 0, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ marginTop: msg.message ? 8 : 0, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,6 @@ interface WorkspaceInputProps {
|
||||||
onPendingAttachDsConsumed?: () => void;
|
onPendingAttachDsConsumed?: () => void;
|
||||||
pendingAttachFdsId?: string;
|
pendingAttachFdsId?: string;
|
||||||
onPendingAttachFdsConsumed?: () => void;
|
onPendingAttachFdsConsumed?: () => void;
|
||||||
onPasteAsFile?: (file: File) => void;
|
|
||||||
draftAppend?: string;
|
draftAppend?: string;
|
||||||
onDraftAppendConsumed?: () => void;
|
onDraftAppendConsumed?: () => void;
|
||||||
workflowId?: string | null;
|
workflowId?: string | null;
|
||||||
|
|
@ -136,7 +135,6 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
||||||
onPendingAttachDsConsumed,
|
onPendingAttachDsConsumed,
|
||||||
pendingAttachFdsId,
|
pendingAttachFdsId,
|
||||||
onPendingAttachFdsConsumed,
|
onPendingAttachFdsConsumed,
|
||||||
onPasteAsFile,
|
|
||||||
draftAppend,
|
draftAppend,
|
||||||
onDraftAppendConsumed,
|
onDraftAppendConsumed,
|
||||||
workflowId,
|
workflowId,
|
||||||
|
|
@ -508,17 +506,6 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
||||||
const hasAttachments = hasFileOrSourceAttachments;
|
const hasAttachments = hasFileOrSourceAttachments;
|
||||||
const _controlSize = isMobile ? 38 : 40;
|
const _controlSize = isMobile ? 38 : 40;
|
||||||
|
|
||||||
const _handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (!onPasteAsFile) return;
|
|
||||||
const text = e.clipboardData.getData('text/plain');
|
|
||||||
if (text && text.length >= 1000) {
|
|
||||||
e.preventDefault();
|
|
||||||
const blob = new Blob([text], { type: 'text/plain' });
|
|
||||||
const file = new File([blob], `pasted-text-${Date.now()}.txt`, { type: 'text/plain' });
|
|
||||||
onPasteAsFile(file);
|
|
||||||
}
|
|
||||||
}, [onPasteAsFile]);
|
|
||||||
|
|
||||||
const _isTreeMimeDrag = useCallback((e: React.DragEvent) => {
|
const _isTreeMimeDrag = useCallback((e: React.DragEvent) => {
|
||||||
const types = e.dataTransfer.types;
|
const types = e.dataTransfer.types;
|
||||||
return (
|
return (
|
||||||
|
|
@ -768,7 +755,6 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
||||||
value={prompt}
|
value={prompt}
|
||||||
onChange={_handleChange}
|
onChange={_handleChange}
|
||||||
onKeyDown={_handleKeyDown}
|
onKeyDown={_handleKeyDown}
|
||||||
onPaste={_handlePaste}
|
|
||||||
onDragEnter={_handleTextareaDragEnter}
|
onDragEnter={_handleTextareaDragEnter}
|
||||||
onDragLeave={_handleTextareaDragLeave}
|
onDragLeave={_handleTextareaDragLeave}
|
||||||
onDragOver={_handleTextareaDragOver}
|
onDragOver={_handleTextareaDragOver}
|
||||||
|
|
|
||||||
|
|
@ -503,7 +503,6 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
||||||
onPendingAttachDsConsumed={() => { setPendingAttachDsId(''); setPendingAttachDsLabel(''); }}
|
onPendingAttachDsConsumed={() => { setPendingAttachDsId(''); setPendingAttachDsLabel(''); }}
|
||||||
pendingAttachFdsId={pendingAttachFdsId}
|
pendingAttachFdsId={pendingAttachFdsId}
|
||||||
onPendingAttachFdsConsumed={() => setPendingAttachFdsId('')}
|
onPendingAttachFdsConsumed={() => setPendingAttachFdsId('')}
|
||||||
onPasteAsFile={_uploadAndAttach}
|
|
||||||
draftAppend={draftAppend}
|
draftAppend={draftAppend}
|
||||||
onDraftAppendConsumed={() => setDraftAppend('')}
|
onDraftAppendConsumed={() => setDraftAppend('')}
|
||||||
workflowId={workspace.workflowId}
|
workflowId={workspace.workflowId}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue