Merge branch 'int'
All checks were successful
Deploy Nyla Frontend to Production / deploy (push) Successful in 42s

This commit is contained in:
Ida 2026-06-12 14:29:50 +02:00
commit 28d64e0a18
3 changed files with 108 additions and 62 deletions

View file

@ -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 }}>

View file

@ -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}

View file

@ -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}