minor bug fixes and new features
This commit is contained in:
parent
09bfff5409
commit
b827c3e00b
28 changed files with 822 additions and 180 deletions
19
src/App.tsx
19
src/App.tsx
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},*/
|
||||
], []);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
15
src/pages/Mitglieder/TeamBereich.tsx
Normal file
15
src/pages/Mitglieder/TeamBereich.tsx
Normal 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;
|
||||
|
||||
Loading…
Reference in a new issue