minor bug fixes and new features

This commit is contained in:
idittrich-valueon 2025-06-19 17:42:59 +02:00
parent 09bfff5409
commit b827c3e00b
28 changed files with 822 additions and 180 deletions

View file

@ -1,4 +1,5 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { useEffect } from 'react';
// Import global CSS reset first
import './index.css';
@ -10,13 +11,27 @@ import { AuthProvider } from './auth/authProvider';
import { ProtectedRoute } from './auth/ProtectedRoute';
import Home from './pages/Home';
import Dateien from './pages/Dateien/Dateien';
import Mitglieder from './pages/Mitglieder/Mitglieder';
import TeamBereich from './pages/Mitglieder/TeamBereich';
import Dashboard from './pages/Dashboard';
import Einstellungen from './pages/Einstellungen/Einstellungen';
// Import the global light theme CSS variables as default
import './assets/styles/light.css';
function App() {
// Load saved theme preference on app mount
useEffect(() => {
const savedTheme = localStorage.getItem('theme');
const prefersDark = savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (prefersDark) {
document.documentElement.classList.add('dark-theme');
document.documentElement.classList.remove('light-theme');
} else {
document.documentElement.classList.add('light-theme');
document.documentElement.classList.remove('dark-theme');
}
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
}, []);
return (
<AuthProvider>
<Router>
@ -31,7 +46,7 @@ function App() {
}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="dateien" element={<Dateien />} />
<Route path="mitglieder" element={<Mitglieder />} />
<Route path="team-bereich" element={<TeamBereich />} />
<Route path="einstellungen" element={<Einstellungen />} />
</Route>
</Routes>

View file

@ -27,12 +27,12 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
onWorkflowCompletedChange,
onWorkflowResume
}) => {
const [activeTab, setActiveTab] = useState("Chat Area");
const [activeTab, setActiveTab] = useState("Chatbereich");
const [resumeWorkflowId, setResumeWorkflowId] = useState<string | null>(null);
const handleWorkflowResume = (workflowId: string) => {
// Switch to Chat Area tab first
setActiveTab("Chat Area");
setActiveTab("Chatbereich");
// Set the workflow ID to resume
setResumeWorkflowId(workflowId);
// Then call the parent's resume handler
@ -57,7 +57,7 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
transition={{ duration: 0.3, ease: "easeOut" }}
>
<div className={styles.chat_button_div}>
{["Chat Area", "Workflow History"].map((tab) => (
{["Chatbereich", "Workflow-Verlauf"].map((tab) => (
<div key={tab} className={styles.buttonWrapper}>
<motion.button
key={tab}
@ -116,7 +116,7 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
}}
style={{ overflow: "hidden" }}
>
{activeTab === "Chat Area" ? (
{activeTab === "Chatbereich" ? (
<DashboardChatArea
selectedPrompt={selectedPrompt}
onPromptUsed={onPromptUsed}

View file

@ -1,4 +1,5 @@
import React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { WorkflowStatusDisplayProps } from "./dashboardChatAreaTypes";
import styles from './DashboardChatArea.module.css';
@ -8,30 +9,93 @@ const WorkflowStatusDisplay: React.FC<WorkflowStatusDisplayProps> = ({
workflowCompleted,
onStartNewWorkflow
}) => {
if (!currentWorkflowId) return null;
return (
<>
{!workflowCompleted && (
<div className={styles.workflow_status}>
<p>
Workflow {currentWorkflowId.substring(0, 8)}... is {workflowStatus?.status || 'running'}
{workflowStatus?.currentRound && ` (Round ${workflowStatus.currentRound})`}
</p>
</div>
)}
{workflowCompleted && (
<div className={styles.completion_message}>
<p>Workflow completed! You can continue the conversation or start a new workflow.</p>
<button
className={styles.new_workflow_button}
onClick={onStartNewWorkflow}
<AnimatePresence>
{currentWorkflowId && !workflowCompleted && (
<motion.div
className={styles.workflow_status}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<div
className={styles.dots_container}
style={{ display: 'flex', alignItems: 'center', gap: '4px', height: '20px' }}
>
Start New Workflow
</button>
</div>
<motion.span
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: 1,
scale: 1,
y: [0, -7, 0]
}}
exit={{ opacity: 0, scale: 0 }}
transition={{
opacity: { duration: 0.4, ease: "easeOut" },
scale: { duration: 0.4, ease: "easeOut" },
y: {
duration: 1.2,
repeat: Infinity,
ease: "easeInOut",
times: [0, 0.5, 1],
delay: 0.2
}
}}
style={{ fontSize: '20px', display: 'inline-block' }}
>
</motion.span>
<motion.span
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: 1,
scale: 1,
y: [0, -7, 0]
}}
exit={{ opacity: 0, scale: 0 }}
transition={{
opacity: { duration: 0.4, ease: "easeOut", delay: 0.1 },
scale: { duration: 0.4, ease: "easeOut", delay: 0.1 },
y: {
duration: 1.2,
repeat: Infinity,
ease: "easeInOut",
times: [0, 0.5, 1],
delay: 0.4
}
}}
style={{ fontSize: '20px', display: 'inline-block' }}
>
</motion.span>
<motion.span
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: 1,
scale: 1,
y: [0, -7, 0]
}}
exit={{ opacity: 0, scale: 0 }}
transition={{
opacity: { duration: 0.4, ease: "easeOut", delay: 0.2 },
scale: { duration: 0.4, ease: "easeOut", delay: 0.2 },
y: {
duration: 1.2,
repeat: Infinity,
ease: "easeInOut",
times: [0, 0.5, 1],
delay: 0.6
}
}}
style={{ fontSize: '20px', display: 'inline-block' }}
>
</motion.span>
</div>
</motion.div>
)}
</>
</AnimatePresence>
);
};

View file

@ -40,7 +40,7 @@
display: flex;
flex-direction: column;
justify-content: flex-start;
padding-bottom: 10px;
}
.messages_spacer {
@ -54,12 +54,20 @@
align-items: flex-end;
flex-shrink: 0;
flex-direction: column;
transition: all 0.2s ease;
}
.chat_input.drag_over {
background-color: var(--color-secondary-disabled);
border: 2px dashed var(--color-secondary);
border-radius: 12px;
padding: 8px;
}
.input_row {
display: flex;
gap: 10px;
align-items: center;
align-items: flex-end;
width: 100%;
}
@ -86,7 +94,8 @@
}
.attachment_button {
padding: 11px 11px;
height: 48px;
width: 48px;
background-color: var(--color-secondary-disabled);
color: var(--color-secondary);
border: none;
@ -134,7 +143,7 @@
}
.attached_file_icon {
font-size: 14px;
font-size: 16px;
}
.attached_file_name {
@ -166,7 +175,8 @@
}
.send_button {
padding: 12px 12px;
height: 48px;
width: 48px;
background-color: var(--color-secondary);
color: var(--color-bg);
border: 1px solid var(--color-secondary);
@ -185,8 +195,8 @@
}
.send_button_icon {
height: 100%;
width: 100%;
height: 60%;
width: 60%;
margin: none;
padding: none;
}
@ -200,6 +210,8 @@
.stop_button {
padding: 12px 12px;
height: 48px;
width: 48px;
background-color: var(--color-red);
color: var(--color-bg);
border: none;
@ -315,11 +327,11 @@
}
.workflow_status {
padding: 8px 12px;
background-color: var(--color-secondary-disabled);
border-left: 4px solid var(--color-secondary);
border-radius: 4px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
color: var(--color-secondary);
}
.workflow_status p {
@ -403,6 +415,8 @@
font-size: 16px;
color: var(--color-secondary);
flex-shrink: 0;
}
.document_info {

View file

@ -58,7 +58,7 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
}
}, [workflowCompleted, onWorkflowCompletedChange]);
const placeholder = workflowCompleted ? "Continue the conversation..." : "Type your message...";
const placeholder = workflowCompleted ? "Gespräch fortsetzen..." : "Nachricht eingeben...";
return (
<div className={styles.chat_area}>

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { LuSendHorizontal } from "react-icons/lu";
import { FaStop } from "react-icons/fa";
@ -45,6 +45,29 @@ const ChatInput: React.FC<ChatInputProps> = ({
onFilesSelect
}) => {
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
// Auto-resize textarea functionality
useEffect(() => {
if (inputRef?.current) {
const textarea = inputRef.current;
textarea.style.height = 'auto';
// Calculate line height - approximately 1.5em per line
const lineHeight = 24; // Adjust this value based on your CSS line-height
const maxHeight = lineHeight * 8; // 8 lines maximum
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
textarea.style.height = `${newHeight}px`;
// Enable/disable scroll based on content height
if (textarea.scrollHeight > maxHeight) {
textarea.style.overflowY = 'auto';
} else {
textarea.style.overflowY = 'hidden';
}
}
}, [inputValue, inputRef]);
const handleAttachmentClick = () => {
setIsUploadModalOpen(true);
@ -59,12 +82,78 @@ const ChatInput: React.FC<ChatInputProps> = ({
onFileRemove(fileId);
};
// Handle Enter key press for sending message (without Shift)
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!isDisabled && (inputValue.trim() || attachedFiles.length > 0)) {
onSend();
}
}
// Call original onKeyPress if it exists (for backward compatibility)
if (onKeyPress && e.key !== 'Enter') {
onKeyPress(e as any);
}
};
// Drag and drop handlers
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isDisabled && !isWorkflowRunning) {
setIsDragOver(true);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
// Only set drag over to false if we're leaving the entire input area
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragOver(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
if (isDisabled || isWorkflowRunning) {
return;
}
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
// Convert File objects to FileInfo objects
const fileInfos: FileInfo[] = files.map((file, index) => ({
id: Date.now() + index, // Generate unique IDs
name: file.name,
mimeType: file.type,
size: file.size,
creationDate: new Date().toISOString(),
source: 'user_uploaded'
}));
onFilesSelect(fileInfos);
}
};
return (
<motion.div
className={styles.chat_input}
className={`${styles.chat_input} ${isDragOver ? styles.drag_over : ''}`}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* Show attached files if any */}
{attachedFiles.length > 0 && (
@ -91,15 +180,20 @@ const ChatInput: React.FC<ChatInputProps> = ({
{/* Input row with text input, attachment button, and send button */}
<div className={styles.input_row}>
<input
<textarea
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={onKeyPress}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className={styles.message_input}
disabled={isDisabled}
rows={1}
style={{
resize: 'none',
minHeight: '24px',
lineHeight: '24px'
}}
/>
{/* Attachment button */}
@ -111,7 +205,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
transition={{ duration: 0.2, ease: "easeOut" }}
title="Datei anhängen"
>
<IoAttach size={18} />
<IoAttach size={26} />
</motion.button>
{/* Send/Stop button */}

View file

@ -3,6 +3,7 @@ import { FaDownload } from "react-icons/fa";
import { MdOutlineRemoveRedEye } from "react-icons/md";
import { Message, Document } from "./dashboardChatAreaTypes";
import FilePreviewPopup from "./FilePreviewPopup";
import { useFileDownload } from "../../../../hooks/useWorkflows";
import styles from './DashboardChatArea.module.css';
interface MessageItemProps {
@ -63,15 +64,9 @@ const getFileIcon = (type?: string, ext?: string): string => {
const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const { downloadFile, isDownloading, error: downloadError } = useFileDownload();
// Debug logging to see if documents are present
console.log('MessageItem rendering:', {
messageId: message.id,
role: message.role,
hasDocuments: !!message.documents,
documentsLength: message.documents?.length || 0,
documents: message.documents
});
const handleDocumentClick = (document: Document) => {
// If there's a downloadUrl, use it; otherwise try the url
@ -85,19 +80,14 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
const handlePreview = (document: Document, e: React.MouseEvent) => {
e.stopPropagation();
console.log('handlePreview called with document:', document);
console.log('document.id:', document.id, 'document.fileId:', document.fileId);
// Use fileId if available, otherwise try to use id as fallback
const fileId = document.fileId || document.id;
if (!fileId) {
console.error('Neither fileId nor id is available on document:', document);
return;
}
console.log('Using fileId for preview:', fileId, 'type:', typeof fileId);
setPreviewDocument(document);
setIsPreviewOpen(true);
};
@ -107,10 +97,20 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
setPreviewDocument(null);
};
const handleDownload = (document: Document, e: React.MouseEvent) => {
const handleDownload = async (document: Document, e: React.MouseEvent) => {
e.stopPropagation();
// TODO: Implement download functionality
console.log('Download document:', document.name);
// Use fileId if available, otherwise try to use id as fallback
const fileId = document.fileId || document.id;
if (!fileId) {
return;
}
// Construct filename with extension if available
const fileName = document.ext ? `${document.name}.${document.ext}` : document.name;
await downloadFile(fileId, fileName);
};
return (
@ -119,8 +119,7 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
className={`${styles.message} ${styles[`message_${message.role}`]}`}
>
<div className={styles.message_role}>
{message.role === 'user' ? 'You' :
message.role === 'assistant' ? 'Assistant' : 'System'}
{message.role === 'user' ? 'You' : message.agentName}
</div>
<div className={styles.message_content}>
{message.content}

View file

@ -54,7 +54,7 @@ const MessageList: React.FC<MessageListProps> = ({
/>
))
) : !currentWorkflowId ? (
<p className={styles.placeholder_text}>Start a conversation by typing a message, selecting a prompt or continuing a previous workflow...</p>
<p className={styles.placeholder_text}>Beginne ein Gespräch, indem du eine Nachricht eingibst, eine Vorlage auswählst oder einen vorherigen Workflow fortsetzt </p>
) : null}
{/* Spacer to push workflow status to bottom when there are fewer messages */}

View file

@ -18,7 +18,7 @@
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 90vw;
height: 90vh;
width: 800px;
width: 80vw;
display: flex;
flex-direction: column;
overflow: hidden;

View file

@ -73,12 +73,12 @@ const FilePreviewPopup: React.FC<FilePreviewPopupProps> = ({ document, isOpen, o
);
}
// Use metadata from backend response
// Use metadata from backend response, but prioritize file extension over potentially incorrect MIME type
const mimeType = fileMetadata?.mimeType;
const isBase64Encoded = fileMetadata?.base64Encoded;
const fileExtension = document.ext?.toLowerCase();
// Check if this is a markdown file by extension/MIME type first
// Check if this is a markdown file by extension first (more reliable than backend MIME type)
const isMarkdownByType = fileExtension === 'md' ||
fileExtension === 'markdown' ||
mimeType === 'text/markdown' ||
@ -110,7 +110,20 @@ const FilePreviewPopup: React.FC<FilePreviewPopupProps> = ({ document, isOpen, o
previewContent.includes('[') && previewContent.includes('](') // Links
);
const isMarkdown = isMarkdownByType || (mimeType === 'text/plain' && hasMarkdownContent);
// For .txt files or text MIME types, check for markdown content
const isTxtWithMarkdown = (fileExtension === 'txt' || mimeType?.startsWith('text/')) && hasMarkdownContent;
const isMarkdown = isMarkdownByType || isTxtWithMarkdown;
// Debug logging
console.log('FilePreviewPopup preview detection:', {
fileExtension,
mimeType,
isMarkdownByType,
hasMarkdownContent,
isTxtWithMarkdown,
isMarkdown,
isCodeFile
});
if (mimeType?.startsWith('image/')) {
// Image preview
@ -162,8 +175,8 @@ const FilePreviewPopup: React.FC<FilePreviewPopupProps> = ({ document, isOpen, o
</pre>
</div>
);
} else if (mimeType?.startsWith('text/') || fileExtension === 'txt') {
// Enhanced text preview for all text files
} else if ((mimeType?.startsWith('text/') || fileExtension === 'txt') && !isMarkdown) {
// Enhanced text preview for text files that are not markdown
return (
<div className={styles.enhanced_text_preview}>
{previewContent?.split('\n').map((line, index) => {

View file

@ -23,6 +23,7 @@ export interface Document {
export interface Message {
id?: string;
role: 'user' | 'assistant' | 'system';
agentName: string;
content: string;
timestamp?: string;
documents?: Document[];
@ -40,7 +41,7 @@ export interface ChatInputProps {
onKeyPress: (e: React.KeyboardEvent) => void;
isDisabled: boolean;
placeholder: string;
inputRef: React.RefObject<HTMLInputElement | null>;
inputRef: React.RefObject<HTMLTextAreaElement | null>;
isWorkflowRunning: boolean;
onStopWorkflow: () => void;
isStoppingWorkflow: boolean;

View file

@ -28,7 +28,7 @@ const DashboardChatHistory: React.FC<DashboardChatHistoryProps> = ({ onWorkflowR
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
>
<div className={styles.loadingContainer}>
<div className={styles.loadingText}>Loading workflows...</div>
<div className={styles.loadingText}>Workflows werden geladen...</div>
</div>
</motion.div>
);
@ -43,7 +43,7 @@ const DashboardChatHistory: React.FC<DashboardChatHistoryProps> = ({ onWorkflowR
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
>
<div className={styles.errorContainer}>
<div className={styles.errorText}>Error loading workflows: {error}</div>
<div className={styles.errorText}>Fehler beim Laden der Workflows: {error}</div>
<button
onClick={refetch}
className={styles.retryButton}
@ -64,7 +64,7 @@ const DashboardChatHistory: React.FC<DashboardChatHistoryProps> = ({ onWorkflowR
>
<div className={styles.container}>
<div className={styles.header}>
<h2 className={styles.history_title}>Workflow History</h2>
<h2 className={styles.history_title}>Workflow-Verlauf</h2>
<div className={styles.workflowCount}>
{workflows.length} {workflows.length === 1 ? 'Workflow' : 'Workflows'}
</div>
@ -73,7 +73,7 @@ const DashboardChatHistory: React.FC<DashboardChatHistoryProps> = ({ onWorkflowR
<div className={styles.scrollableContent}>
{workflows.length === 0 ? (
<div className={styles.emptyState}>
No workflows available
Keine Workflows verfügbar
</div>
) : (
<div className={styles.workflowsList}>

View file

@ -61,14 +61,13 @@
.horizontalLine {
width: 100%;
background-color: var(--color-gray);
height: 2px;
margin-top: 19px;
}
.horizontalLineLight {
width: calc(100%);
background-color: var(--color-gray);
background-color: var(--color-gray-disabled);
height: 2px;
margin-top: 39px;
margin-left: -20px;
@ -101,7 +100,6 @@
align-items: flex-start;
gap: 8px;
padding: 4px 0;
border-bottom: 1px solid rgba(0, 255, 0, 0.1);
animation: fadeIn 0.3s ease-in;
}

View file

@ -19,7 +19,7 @@ const DashboardPrompt: React.FC<DashboardPromptProps> = ({
isCollapsed,
onToggleCollapse
}) => {
const [activeTab, setActiveTab] = useState("Prompt Set");
const [activeTab, setActiveTab] = useState("Prompt Vorlage");
const [searchParams] = useSearchParams();
useEffect(() => {
@ -27,7 +27,7 @@ const DashboardPrompt: React.FC<DashboardPromptProps> = ({
const promptId = searchParams.get('promptId');
if (expandedPrompt) {
setActiveTab("Prompt Set");
setActiveTab("Prompt Vorlage");
} else if (promptId) {
setActiveTab("Einstellungen");
}
@ -38,7 +38,7 @@ const DashboardPrompt: React.FC<DashboardPromptProps> = ({
<div className={ styles.prompt_header }>
<div className={ styles.prompt_button_div }>
{[
"Prompt Set",
"Prompt Vorlage",
"Einstellungen"
].map((tab) => (
<div key={tab} className={styles.buttonWrapper}>
@ -83,7 +83,7 @@ const DashboardPrompt: React.FC<DashboardPromptProps> = ({
}}
>
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
{activeTab === "Prompt Set" ? (
{activeTab === "Prompt Vorlage" ? (
<DashboardPromptSet onPromptRun={onPromptRun} />
) : (
<DashboardPromptSettings />

View file

@ -1,6 +1,7 @@
import React from 'react';
import { usePrompts, Prompt } from '../../../../hooks/usePrompts';
import React, { useState } from 'react';
import { usePrompts, usePromptOperations, Prompt } from '../../../../hooks/usePrompts';
import DashboardPromptSetItem from './DashboardPromptSetItem';
import DashboardPromptSetModal from './DashboardPromptSetModal';
import styles from './DashboardPromptSet.module.css';
import { FaPlus } from 'react-icons/fa';
@ -10,6 +11,18 @@ interface DashboardPromptSetProps {
function DashboardPromptSet({ onPromptRun }: DashboardPromptSetProps) {
const { prompts, loading, error, refetch } = usePrompts();
const { handlePromptCreate, creatingPrompt } = usePromptOperations();
const [isModalOpen, setIsModalOpen] = useState(false);
const handleCreatePrompt = async (promptData: { name: string; content: string }) => {
const result = await handlePromptCreate(promptData);
if (result.success) {
await refetch(); // Refresh the prompts list
setIsModalOpen(false);
} else {
throw new Error(result.error);
}
};
if (loading) {
return (
@ -37,7 +50,7 @@ function DashboardPromptSet({ onPromptRun }: DashboardPromptSetProps) {
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.headerButtons}>
<button className={styles.addButton} onClick={() => console.log('add prompt')}>
<button className={styles.addButton} onClick={() => setIsModalOpen(true)}>
<FaPlus />
Neuer Prompt
</button>
@ -66,6 +79,13 @@ function DashboardPromptSet({ onPromptRun }: DashboardPromptSetProps) {
</div>
)}
</div>
<DashboardPromptSetModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSubmit={handleCreatePrompt}
isLoading={creatingPrompt}
/>
</div>
);
}

View file

@ -114,6 +114,46 @@
color: var(--color-bg);
}
.deleteButton.confirm {
background-color: var(--color-red-disabled);
color: var(--color-red);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.deleteButton.confirm:hover:not(:disabled) {
background-color: var(--color-red-hover);
color: var(--color-bg);
}
.actionText {
font-size: 12px;
color: var(--color-gray);
animation: pulse 1.5s infinite;
white-space: nowrap;
font-family: var(--font-family);
margin-left: 4px;
}
.deleteButton.confirm .actionText {
color: var(--color-red);
animation: none;
}
@keyframes pulse {
0% {
opacity: 0.6;
}
50% {
opacity: 1;
}
100% {
opacity: 0.6;
}
}
.promptContent {
display: flex;
flex-direction: column;

View file

@ -14,18 +14,26 @@ interface DashboardPromptSetItemProps {
function DashboardPromptSetItem({ prompt, onDelete, onRun }: DashboardPromptSetItemProps) {
const { handlePromptDelete, deletingPrompts, deleteError } = usePromptOperations();
const contentRef = useRef<HTMLDivElement>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const isDeleting = deletingPrompts.has(prompt.id);
const handleDelete = async () => {
if (window.confirm(`Möchten Sie den Prompt "${prompt.name}" wirklich löschen?`)) {
const handleDeleteClick = async () => {
if (showDeleteConfirm) {
const success = await handlePromptDelete(prompt.id);
if (success && onDelete) {
onDelete();
}
setShowDeleteConfirm(false);
} else {
setShowDeleteConfirm(true);
}
};
const handleCancelDelete = () => {
setShowDeleteConfirm(false);
};
const handleRun = () => {
onRun(prompt);
};
@ -76,12 +84,15 @@ function DashboardPromptSetItem({ prompt, onDelete, onRun }: DashboardPromptSetI
</button>
<button
onClick={handleDelete}
onClick={handleDeleteClick}
disabled={isDeleting}
className={`${styles.actionButton} ${styles.deleteButton}`}
title="Prompt löschen"
className={`${styles.actionButton} ${styles.deleteButton} ${showDeleteConfirm ? styles.confirm : ''}`}
title={showDeleteConfirm ? "Klicken Sie erneut zum Bestätigen" : "Prompt löschen"}
onBlur={handleCancelDelete}
>
<AiOutlineDelete size={16} />
{isDeleting && <span className={styles.actionText}>Löschen...</span>}
{showDeleteConfirm && <span className={styles.actionText}>Zum Bestätigen klicken</span>}
</button>
</div>
</div>

View file

@ -0,0 +1,192 @@
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 20px;
}
.modal {
background: var(--color-bg);
border-radius: 12px;
border: 1px solid var(--color-gray);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 24px 0 24px;
border-bottom: 1px solid var(--color-gray);
margin-bottom: 20px;
}
.title {
font-size: 20px;
font-weight: 600;
color: var(--color-text);
margin: 0;
}
.closeButton {
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: var(--color-text);
border-radius: 6px;
transition: all 0.2s;
}
.closeButton:hover {
background-color: var(--color-gray-disabled);
color: var(--color-text);
}
.closeButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.form {
padding: 0 24px 24px 24px;
flex: 1;
overflow-y: auto;
}
.formGroup {
margin-bottom: 20px;
}
.label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--color-text);
margin-bottom: 6px;
}
.input {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--color-gray);
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s;
box-sizing: border-box;
background-color: var(--color-bg);
color: var(--color-text);
}
.input:focus {
outline: none;
border-color: var(--color-secondary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.input:disabled {
background-color: var(--color-gray-disabled);
opacity: 0.7;
}
.textarea {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--color-gray);
border-radius: 8px;
font-size: 14px;
font-family: inherit;
resize: vertical;
min-height: 120px;
transition: border-color 0.2s;
box-sizing: border-box;
background-color: var(--color-bg);
color: var(--color-text);
}
.textarea:focus {
outline: none;
border-color: var(--color-secondary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
background-color: var(--color-bg);
color: var(--color-text);
}
.textarea:disabled {
background-color: var(--color-gray-disabled);
opacity: 0.7;
}
.error {
background-color: var(--color-red-disabled);
border: 1px solid var(--color-red);
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
margin-bottom: 20px;
}
.buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--color-gray);
}
.cancelButton {
padding: 10px 20px;
border: 1px solid var(--color-red);
background: var(--color-red);
color: var(--color-text);
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.cancelButton:hover {
background-color: var(--color-red-hover);
border-color: var(--color-red-hover);
}
.cancelButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.submitButton {
padding: 10px 20px;
background: var(--color-secondary);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.submitButton:hover {
background: var(--color-secondary-hover);
}
.submitButton:disabled {
background: var(--color-secondary-disabled);
cursor: not-allowed;
}

View file

@ -0,0 +1,127 @@
import React, { useState } from 'react';
import { FaTimes } from 'react-icons/fa';
import styles from './DashboardPromptSetModal.module.css';
interface DashboardPromptSetModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (promptData: { name: string; content: string }) => Promise<void>;
isLoading?: boolean;
}
function DashboardPromptSetModal({ isOpen, onClose, onSubmit, isLoading = false }: DashboardPromptSetModalProps) {
const [name, setName] = useState('');
const [content, setContent] = useState('');
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) {
setError('Name ist erforderlich');
return;
}
if (!content.trim()) {
setError('Inhalt ist erforderlich');
return;
}
setError(null);
try {
await onSubmit({ name: name.trim(), content: content.trim() });
// Reset form on success
setName('');
setContent('');
onClose();
} catch (err: any) {
setError(err.message || 'Fehler beim Erstellen des Prompts');
}
};
const handleClose = () => {
setName('');
setContent('');
setError(null);
onClose();
};
if (!isOpen) return null;
return (
<div className={styles.overlay}>
<div className={styles.modal}>
<div className={styles.header}>
<h2 className={styles.title}>Neuen Prompt erstellen</h2>
<button
onClick={handleClose}
className={styles.closeButton}
disabled={isLoading}
>
<FaTimes />
</button>
</div>
<form onSubmit={handleSubmit} className={styles.form}>
<div className={styles.formGroup}>
<label htmlFor="promptName" className={styles.label}>
Name *
</label>
<input
id="promptName"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className={styles.input}
placeholder="Geben Sie einen Namen für den Prompt ein"
disabled={isLoading}
maxLength={100}
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="promptContent" className={styles.label}>
Inhalt *
</label>
<textarea
id="promptContent"
value={content}
onChange={(e) => setContent(e.target.value)}
className={styles.textarea}
placeholder="Geben Sie den Inhalt des Prompts ein"
disabled={isLoading}
rows={8}
/>
</div>
{error && (
<div className={styles.error}>
{error}
</div>
)}
<div className={styles.buttons}>
<button
type="button"
onClick={handleClose}
className={styles.cancelButton}
disabled={isLoading}
>
Abbrechen
</button>
<button
type="submit"
className={styles.submitButton}
disabled={isLoading}
>
{isLoading ? 'Erstellen...' : 'Prompt erstellen'}
</button>
</div>
</form>
</div>
</div>
);
}
export default DashboardPromptSetModal;

View file

@ -164,7 +164,7 @@ const DateienItem = ({ file, onDelete, onOptimisticDelete }: DateienItemProps) =
title="Download file"
>
<FaDownload className={styles.actionIcon} />
{isDownloading && <span className={styles.actionText}>Downloading...</span>}
{isDownloading && <span className={styles.actionText}>Laden...</span>}
</button>
<button
className={`${styles.deleteButton} ${isDeleting ? styles.deleting : ''} ${showDeleteConfirm ? styles.confirm : ''}`}
@ -174,8 +174,8 @@ const DateienItem = ({ file, onDelete, onOptimisticDelete }: DateienItemProps) =
onBlur={handleCancelDelete}
>
<FaTrash className={styles.actionIcon} />
{isDeleting && <span className={styles.actionText}>Deleting...</span>}
{showDeleteConfirm && <span className={styles.actionText}>Click to confirm</span>}
{isDeleting && <span className={styles.actionText}>Löschen...</span>}
{showDeleteConfirm && <span className={styles.actionText}>Zum Bestätigen klicken...</span>}
</button>
</div>
</div>

View file

@ -13,8 +13,8 @@ const useSidebarData = () => {
return useMemo(() => [
{
id: '1',
name: 'Organisation',
link: '/organisation',
name: 'Team-Bereich',
link: '/team-bereich',
icon: MdOutlineWorkOutline,
},
{
@ -29,36 +29,24 @@ const useSidebarData = () => {
link: '/dateien',
icon: FaRegFileAlt,
},
{
id: '4',
name: 'Mitglieder',
link: '/mitglieder',
icon: RiTeamLine,
},
{
id: '5',
name: 'Nachrichten',
link: '',
icon: BiInfoSquare,
},
{
/*{
id: '6',
name: 'Logs',
link: '',
icon: TbLogs ,
},
},*/
{
id: '7',
name: 'Einstellungen',
link: '/einstellungen',
icon: GoGear,
},
{
/*{
id: '8',
name: 'Help',
link: '',
icon: BiInfoSquare,
},
},*/
], []);
}

View file

@ -47,7 +47,7 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ user, isLoading, error }) =>
<FaUserCircle className={styles.user_icon} />
<div className={styles.text_content}>
<h1>{ user.name }</h1>
<p>role: {user.role}</p>
<p>Rolle: {user.role}</p>
</div>
</div>
<button

View file

@ -205,7 +205,6 @@ export function useWorkflowMessages(workflowId: string | null, messageId?: strin
// Error is already handled by useApiRequest
}
};
useEffect(() => {
fetchMessages();
}, [workflowId, messageId]);
@ -250,8 +249,6 @@ export function useFilePreview() {
const { request } = useApiRequest();
const fetchPreview = async (fileId: string | number) => {
console.log('fetchPreview called with fileId:', fileId, 'type:', typeof fileId);
if (!fileId) {
setError("File ID not available");
return;
@ -272,44 +269,71 @@ export function useFilePreview() {
numericFileId = parseInt(String(fileId), 10);
}
console.log('Parsed fileId:', numericFileId, 'isNaN:', isNaN(numericFileId));
if (isNaN(numericFileId)) {
throw new Error(`Invalid file ID format: "${fileId}" (type: ${typeof fileId}). Expected a numeric file ID, but got a document UUID. Make sure the document object has a 'fileId' property with the numeric file ID.`);
}
console.log('Making API request to:', `/api/workflows/files/${numericFileId}/preview`);
const response = await request({
url: `/api/workflows/files/${numericFileId}/preview`,
method: 'get'
});
console.log('API response:', response);
// Handle response as object with metadata and preview content
if (typeof response === 'object' && response !== null) {
setFileMetadata(response);
// Debug: log the full response
console.log('Full backend response:', response);
console.log('Response keys:', Object.keys(response));
// Try different possible property names for the content
const content = response.preview || response.content || response.data || response.previewContent;
// Debug for PDF issues only
if (response.mimeType === 'application/pdf') {
console.log('PDF Preview Debug:', {
hasPreview: !!response.preview,
previewLength: response.preview?.length,
hasBase64Flag: response.base64Encoded,
mimeType: response.mimeType
});
console.log('Extracted content:', content ? 'has content' : 'null/empty');
console.log('Content type:', typeof content);
console.log('Content length:', content?.length);
// If base64Encoded is true and we have content, try to decode it
let processedContent = content;
if (response.base64Encoded && content && typeof content === 'string') {
try {
processedContent = atob(content);
console.log('Decoded base64 content:', processedContent.substring(0, 200));
} catch (e) {
console.error('Failed to decode base64 content:', e);
}
}
setPreviewContent(content || null);
// If no preview content but file should be previewable, try to fetch raw content
if (!processedContent && response.name) {
const fileExtension = response.name.split('.').pop()?.toLowerCase();
const shouldBePreviewable = ['md', 'markdown', 'txt', 'py', 'js', 'ts', 'jsx', 'tsx', 'html', 'css', 'json', 'xml', 'yaml', 'yml'].includes(fileExtension || '');
if (shouldBePreviewable) {
console.log('File should be previewable, attempting to fetch raw content...');
try {
// Try to fetch the raw file content using download endpoint
const rawResponse = await request({
url: `/api/workflows/files/${numericFileId}/download`,
method: 'get',
additionalConfig: { responseType: 'text' }
});
console.log('Raw content fetched:', typeof rawResponse, rawResponse?.substring?.(0, 200));
setPreviewContent(rawResponse || null);
return;
} catch (rawError) {
console.error('Failed to fetch raw content:', rawError);
}
}
}
setPreviewContent(processedContent || null);
} else {
// Fallback if response is just the content
setPreviewContent(response);
}
} catch (err: any) {
console.error('File preview error:', err);
setError(err.message || "Failed to load preview");
} finally {
setIsLoading(false);
@ -331,4 +355,75 @@ export function useFilePreview() {
fetchPreview,
clearPreview
};
}
// File download hook
export function useFileDownload() {
const [isDownloading, setIsDownloading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { request } = useApiRequest();
const downloadFile = async (fileId: string | number, fileName?: string) => {
if (!fileId) {
setError("File ID not available");
return;
}
setIsDownloading(true);
setError(null);
try {
// Convert fileId to number since backend expects integer
let numericFileId: number;
if (typeof fileId === 'number') {
numericFileId = fileId;
} else {
numericFileId = parseInt(String(fileId), 10);
}
if (isNaN(numericFileId)) {
throw new Error(`Invalid file ID format: "${fileId}" (type: ${typeof fileId}). Expected a numeric file ID, but got a document UUID. Make sure the document object has a 'fileId' property with the numeric file ID.`);
}
// Use the same approach as useFiles.ts - use request with blob response type
const blob = await request({
url: `/api/workflows/files/${numericFileId}/download`,
method: 'get',
// Override axios config for blob response
additionalConfig: { responseType: 'blob' }
});
// Use provided fileName or fallback to 'download'
const downloadFileName = fileName || 'download';
// Create download link and trigger download (same as useFiles.ts)
const url = window.URL.createObjectURL(new Blob([blob]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', downloadFileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
return true;
} catch (err: any) {
setError(err.message || "Failed to download file");
return false;
} finally {
setIsDownloading(false);
}
};
const clearError = () => {
setError(null);
};
return {
isDownloading,
error,
downloadFile,
clearError
};
}

View file

@ -23,11 +23,11 @@
/* Height classes for different states */
.chatArea15vh {
height: 15vh;
height: 35vh;
}
.chatArea40vh {
height: 40vh;
height: 60vh;
}
.chatArea45vh {
@ -47,7 +47,7 @@
}
.logArea40vh {
height: 40vh;
height: 60vh;
}
.logArea60vh {

View file

@ -4,12 +4,11 @@ import styles from './Einstellungen.module.css';
function Einstellungen() {
const [isDarkMode, setIsDarkMode] = useState(false);
// Load saved theme preference on component mount
// Sync component state with current theme on mount
useEffect(() => {
const savedTheme = localStorage.getItem('theme');
const prefersDark = savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches);
setIsDarkMode(prefersDark);
applyTheme(prefersDark);
}, []);
const applyTheme = (isDark: boolean) => {

View file

@ -1,43 +0,0 @@
import styles from './Mitglieder.module.css'
import MitgliederItem from '../../components/Mitglieder/MitgliederItem';
import { IoPersonAddSharp } from "react-icons/io5";
import { useOrgUsers } from '../../hooks/useUsers';
function Mitglieder () {
const { users, loading, error, refetch } = useOrgUsers();
return (
<div className={styles.mitgliederContainer}>
<div className={styles.header}>
<button className={styles.mitglieder_hinzufügen_button}>
<IoPersonAddSharp className={styles.add_icon}/>
Mitglied hinzufügen
</button>
</div>
<div className={styles.horizontalLineLight}></div>
{loading ? (
<p>Loading...</p>
) : error ? (
<p>Error: {error}</p>
) : users.length === 0 ? (
<p>No users found.</p>
) : (
<ul className={styles.membersList}>
{users.map((user) => (
<MitgliederItem
key={user.id}
user={user}
refetchUsers={refetch}
totalUsers={users.length}
/>
))}
</ul>
)}
</div>
);
}
export default Mitglieder;

View file

@ -0,0 +1,15 @@
import styles from './TeamBereich.module.css'
import MitgliederItem from '../../components/Mitglieder/MitgliederItem';
import { IoPersonAddSharp } from "react-icons/io5";
import { useOrgUsers } from '../../hooks/useUsers';
function TeamBereich () {
return (
<h1>Team-Bereich</h1>
);
}
export default TeamBereich;