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 { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
// Import global CSS reset first
|
// Import global CSS reset first
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
@ -10,13 +11,27 @@ import { AuthProvider } from './auth/authProvider';
|
||||||
import { ProtectedRoute } from './auth/ProtectedRoute';
|
import { ProtectedRoute } from './auth/ProtectedRoute';
|
||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import Dateien from './pages/Dateien/Dateien';
|
import Dateien from './pages/Dateien/Dateien';
|
||||||
import Mitglieder from './pages/Mitglieder/Mitglieder';
|
import TeamBereich from './pages/Mitglieder/TeamBereich';
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
import Einstellungen from './pages/Einstellungen/Einstellungen';
|
import Einstellungen from './pages/Einstellungen/Einstellungen';
|
||||||
// Import the global light theme CSS variables as default
|
// Import the global light theme CSS variables as default
|
||||||
import './assets/styles/light.css';
|
import './assets/styles/light.css';
|
||||||
|
|
||||||
function App() {
|
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 (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Router>
|
<Router>
|
||||||
|
|
@ -31,7 +46,7 @@ function App() {
|
||||||
}>
|
}>
|
||||||
<Route path="dashboard" element={<Dashboard />} />
|
<Route path="dashboard" element={<Dashboard />} />
|
||||||
<Route path="dateien" element={<Dateien />} />
|
<Route path="dateien" element={<Dateien />} />
|
||||||
<Route path="mitglieder" element={<Mitglieder />} />
|
<Route path="team-bereich" element={<TeamBereich />} />
|
||||||
<Route path="einstellungen" element={<Einstellungen />} />
|
<Route path="einstellungen" element={<Einstellungen />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,12 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||||
onWorkflowCompletedChange,
|
onWorkflowCompletedChange,
|
||||||
onWorkflowResume
|
onWorkflowResume
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState("Chat Area");
|
const [activeTab, setActiveTab] = useState("Chatbereich");
|
||||||
const [resumeWorkflowId, setResumeWorkflowId] = useState<string | null>(null);
|
const [resumeWorkflowId, setResumeWorkflowId] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleWorkflowResume = (workflowId: string) => {
|
const handleWorkflowResume = (workflowId: string) => {
|
||||||
// Switch to Chat Area tab first
|
// Switch to Chat Area tab first
|
||||||
setActiveTab("Chat Area");
|
setActiveTab("Chatbereich");
|
||||||
// Set the workflow ID to resume
|
// Set the workflow ID to resume
|
||||||
setResumeWorkflowId(workflowId);
|
setResumeWorkflowId(workflowId);
|
||||||
// Then call the parent's resume handler
|
// Then call the parent's resume handler
|
||||||
|
|
@ -57,7 +57,7 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
>
|
>
|
||||||
<div className={styles.chat_button_div}>
|
<div className={styles.chat_button_div}>
|
||||||
{["Chat Area", "Workflow History"].map((tab) => (
|
{["Chatbereich", "Workflow-Verlauf"].map((tab) => (
|
||||||
<div key={tab} className={styles.buttonWrapper}>
|
<div key={tab} className={styles.buttonWrapper}>
|
||||||
<motion.button
|
<motion.button
|
||||||
key={tab}
|
key={tab}
|
||||||
|
|
@ -116,7 +116,7 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||||
}}
|
}}
|
||||||
style={{ overflow: "hidden" }}
|
style={{ overflow: "hidden" }}
|
||||||
>
|
>
|
||||||
{activeTab === "Chat Area" ? (
|
{activeTab === "Chatbereich" ? (
|
||||||
<DashboardChatArea
|
<DashboardChatArea
|
||||||
selectedPrompt={selectedPrompt}
|
selectedPrompt={selectedPrompt}
|
||||||
onPromptUsed={onPromptUsed}
|
onPromptUsed={onPromptUsed}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { WorkflowStatusDisplayProps } from "./dashboardChatAreaTypes";
|
import { WorkflowStatusDisplayProps } from "./dashboardChatAreaTypes";
|
||||||
import styles from './DashboardChatArea.module.css';
|
import styles from './DashboardChatArea.module.css';
|
||||||
|
|
||||||
|
|
@ -8,30 +9,93 @@ const WorkflowStatusDisplay: React.FC<WorkflowStatusDisplayProps> = ({
|
||||||
workflowCompleted,
|
workflowCompleted,
|
||||||
onStartNewWorkflow
|
onStartNewWorkflow
|
||||||
}) => {
|
}) => {
|
||||||
if (!currentWorkflowId) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<AnimatePresence>
|
||||||
{!workflowCompleted && (
|
{currentWorkflowId && !workflowCompleted && (
|
||||||
<div className={styles.workflow_status}>
|
<motion.div
|
||||||
<p>
|
className={styles.workflow_status}
|
||||||
Workflow {currentWorkflowId.substring(0, 8)}... is {workflowStatus?.status || 'running'}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
{workflowStatus?.currentRound && ` (Round ${workflowStatus.currentRound})`}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
</p>
|
exit={{ opacity: 0, scale: 0.8 }}
|
||||||
</div>
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
)}
|
>
|
||||||
{workflowCompleted && (
|
<div
|
||||||
<div className={styles.completion_message}>
|
className={styles.dots_container}
|
||||||
<p>Workflow completed! You can continue the conversation or start a new workflow.</p>
|
style={{ display: 'flex', alignItems: 'center', gap: '4px', height: '20px' }}
|
||||||
<button
|
|
||||||
className={styles.new_workflow_button}
|
|
||||||
onClick={onStartNewWorkflow}
|
|
||||||
>
|
>
|
||||||
Start New Workflow
|
<motion.span
|
||||||
</button>
|
initial={{ opacity: 0, scale: 0 }}
|
||||||
</div>
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages_spacer {
|
.messages_spacer {
|
||||||
|
|
@ -54,12 +54,20 @@
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-direction: column;
|
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 {
|
.input_row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,7 +94,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment_button {
|
.attachment_button {
|
||||||
padding: 11px 11px;
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
background-color: var(--color-secondary-disabled);
|
background-color: var(--color-secondary-disabled);
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -134,7 +143,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.attached_file_icon {
|
.attached_file_icon {
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attached_file_name {
|
.attached_file_name {
|
||||||
|
|
@ -166,7 +175,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.send_button {
|
.send_button {
|
||||||
padding: 12px 12px;
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
color: var(--color-bg);
|
color: var(--color-bg);
|
||||||
border: 1px solid var(--color-secondary);
|
border: 1px solid var(--color-secondary);
|
||||||
|
|
@ -185,8 +195,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.send_button_icon {
|
.send_button_icon {
|
||||||
height: 100%;
|
height: 60%;
|
||||||
width: 100%;
|
width: 60%;
|
||||||
margin: none;
|
margin: none;
|
||||||
padding: none;
|
padding: none;
|
||||||
}
|
}
|
||||||
|
|
@ -200,6 +210,8 @@
|
||||||
|
|
||||||
.stop_button {
|
.stop_button {
|
||||||
padding: 12px 12px;
|
padding: 12px 12px;
|
||||||
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
background-color: var(--color-red);
|
background-color: var(--color-red);
|
||||||
color: var(--color-bg);
|
color: var(--color-bg);
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -315,11 +327,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow_status {
|
.workflow_status {
|
||||||
padding: 8px 12px;
|
display: flex;
|
||||||
background-color: var(--color-secondary-disabled);
|
align-items: center;
|
||||||
border-left: 4px solid var(--color-secondary);
|
justify-content: center;
|
||||||
border-radius: 4px;
|
width: 100%;
|
||||||
margin-bottom: 10px;
|
color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow_status p {
|
.workflow_status p {
|
||||||
|
|
@ -403,6 +415,8 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.document_info {
|
.document_info {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||||
}
|
}
|
||||||
}, [workflowCompleted, onWorkflowCompletedChange]);
|
}, [workflowCompleted, onWorkflowCompletedChange]);
|
||||||
|
|
||||||
const placeholder = workflowCompleted ? "Continue the conversation..." : "Type your message...";
|
const placeholder = workflowCompleted ? "Gespräch fortsetzen..." : "Nachricht eingeben...";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.chat_area}>
|
<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 { motion } from "framer-motion";
|
||||||
import { LuSendHorizontal } from "react-icons/lu";
|
import { LuSendHorizontal } from "react-icons/lu";
|
||||||
import { FaStop } from "react-icons/fa";
|
import { FaStop } from "react-icons/fa";
|
||||||
|
|
@ -45,6 +45,29 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
||||||
onFilesSelect
|
onFilesSelect
|
||||||
}) => {
|
}) => {
|
||||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
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 = () => {
|
const handleAttachmentClick = () => {
|
||||||
setIsUploadModalOpen(true);
|
setIsUploadModalOpen(true);
|
||||||
|
|
@ -59,12 +82,78 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
||||||
onFileRemove(fileId);
|
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 (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className={styles.chat_input}
|
className={`${styles.chat_input} ${isDragOver ? styles.drag_over : ''}`}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }}
|
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }}
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
{/* Show attached files if any */}
|
{/* Show attached files if any */}
|
||||||
{attachedFiles.length > 0 && (
|
{attachedFiles.length > 0 && (
|
||||||
|
|
@ -91,15 +180,20 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
||||||
|
|
||||||
{/* Input row with text input, attachment button, and send button */}
|
{/* Input row with text input, attachment button, and send button */}
|
||||||
<div className={styles.input_row}>
|
<div className={styles.input_row}>
|
||||||
<input
|
<textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyPress={onKeyPress}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={styles.message_input}
|
className={styles.message_input}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
rows={1}
|
||||||
|
style={{
|
||||||
|
resize: 'none',
|
||||||
|
minHeight: '24px',
|
||||||
|
lineHeight: '24px'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Attachment button */}
|
{/* Attachment button */}
|
||||||
|
|
@ -111,7 +205,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
||||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||||
title="Datei anhängen"
|
title="Datei anhängen"
|
||||||
>
|
>
|
||||||
<IoAttach size={18} />
|
<IoAttach size={26} />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
||||||
{/* Send/Stop button */}
|
{/* Send/Stop button */}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { FaDownload } from "react-icons/fa";
|
||||||
import { MdOutlineRemoveRedEye } from "react-icons/md";
|
import { MdOutlineRemoveRedEye } from "react-icons/md";
|
||||||
import { Message, Document } from "./dashboardChatAreaTypes";
|
import { Message, Document } from "./dashboardChatAreaTypes";
|
||||||
import FilePreviewPopup from "./FilePreviewPopup";
|
import FilePreviewPopup from "./FilePreviewPopup";
|
||||||
|
import { useFileDownload } from "../../../../hooks/useWorkflows";
|
||||||
import styles from './DashboardChatArea.module.css';
|
import styles from './DashboardChatArea.module.css';
|
||||||
|
|
||||||
interface MessageItemProps {
|
interface MessageItemProps {
|
||||||
|
|
@ -63,15 +64,9 @@ const getFileIcon = (type?: string, ext?: string): string => {
|
||||||
const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
|
const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
|
||||||
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
|
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
|
||||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
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) => {
|
const handleDocumentClick = (document: Document) => {
|
||||||
// If there's a downloadUrl, use it; otherwise try the url
|
// 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) => {
|
const handlePreview = (document: Document, e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
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
|
// Use fileId if available, otherwise try to use id as fallback
|
||||||
const fileId = document.fileId || document.id;
|
const fileId = document.fileId || document.id;
|
||||||
|
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
console.error('Neither fileId nor id is available on document:', document);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Using fileId for preview:', fileId, 'type:', typeof fileId);
|
|
||||||
|
|
||||||
setPreviewDocument(document);
|
setPreviewDocument(document);
|
||||||
setIsPreviewOpen(true);
|
setIsPreviewOpen(true);
|
||||||
};
|
};
|
||||||
|
|
@ -107,10 +97,20 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
|
||||||
setPreviewDocument(null);
|
setPreviewDocument(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = (document: Document, e: React.MouseEvent) => {
|
const handleDownload = async (document: Document, e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
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 (
|
return (
|
||||||
|
|
@ -119,8 +119,7 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
|
||||||
className={`${styles.message} ${styles[`message_${message.role}`]}`}
|
className={`${styles.message} ${styles[`message_${message.role}`]}`}
|
||||||
>
|
>
|
||||||
<div className={styles.message_role}>
|
<div className={styles.message_role}>
|
||||||
{message.role === 'user' ? 'You' :
|
{message.role === 'user' ? 'You' : message.agentName}
|
||||||
message.role === 'assistant' ? 'Assistant' : 'System'}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.message_content}>
|
<div className={styles.message_content}>
|
||||||
{message.content}
|
{message.content}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : !currentWorkflowId ? (
|
) : !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}
|
) : null}
|
||||||
|
|
||||||
{/* Spacer to push workflow status to bottom when there are fewer messages */}
|
{/* 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);
|
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;
|
max-width: 90vw;
|
||||||
height: 90vh;
|
height: 90vh;
|
||||||
width: 800px;
|
width: 80vw;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
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 mimeType = fileMetadata?.mimeType;
|
||||||
const isBase64Encoded = fileMetadata?.base64Encoded;
|
const isBase64Encoded = fileMetadata?.base64Encoded;
|
||||||
const fileExtension = document.ext?.toLowerCase();
|
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' ||
|
const isMarkdownByType = fileExtension === 'md' ||
|
||||||
fileExtension === 'markdown' ||
|
fileExtension === 'markdown' ||
|
||||||
mimeType === 'text/markdown' ||
|
mimeType === 'text/markdown' ||
|
||||||
|
|
@ -110,7 +110,20 @@ const FilePreviewPopup: React.FC<FilePreviewPopupProps> = ({ document, isOpen, o
|
||||||
previewContent.includes('[') && previewContent.includes('](') // Links
|
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/')) {
|
if (mimeType?.startsWith('image/')) {
|
||||||
// Image preview
|
// Image preview
|
||||||
|
|
@ -162,8 +175,8 @@ const FilePreviewPopup: React.FC<FilePreviewPopupProps> = ({ document, isOpen, o
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (mimeType?.startsWith('text/') || fileExtension === 'txt') {
|
} else if ((mimeType?.startsWith('text/') || fileExtension === 'txt') && !isMarkdown) {
|
||||||
// Enhanced text preview for all text files
|
// Enhanced text preview for text files that are not markdown
|
||||||
return (
|
return (
|
||||||
<div className={styles.enhanced_text_preview}>
|
<div className={styles.enhanced_text_preview}>
|
||||||
{previewContent?.split('\n').map((line, index) => {
|
{previewContent?.split('\n').map((line, index) => {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export interface Document {
|
||||||
export interface Message {
|
export interface Message {
|
||||||
id?: string;
|
id?: string;
|
||||||
role: 'user' | 'assistant' | 'system';
|
role: 'user' | 'assistant' | 'system';
|
||||||
|
agentName: string;
|
||||||
content: string;
|
content: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
documents?: Document[];
|
documents?: Document[];
|
||||||
|
|
@ -40,7 +41,7 @@ export interface ChatInputProps {
|
||||||
onKeyPress: (e: React.KeyboardEvent) => void;
|
onKeyPress: (e: React.KeyboardEvent) => void;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
inputRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||||
isWorkflowRunning: boolean;
|
isWorkflowRunning: boolean;
|
||||||
onStopWorkflow: () => void;
|
onStopWorkflow: () => void;
|
||||||
isStoppingWorkflow: boolean;
|
isStoppingWorkflow: boolean;
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ const DashboardChatHistory: React.FC<DashboardChatHistoryProps> = ({ onWorkflowR
|
||||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
||||||
>
|
>
|
||||||
<div className={styles.loadingContainer}>
|
<div className={styles.loadingContainer}>
|
||||||
<div className={styles.loadingText}>Loading workflows...</div>
|
<div className={styles.loadingText}>Workflows werden geladen...</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
@ -43,7 +43,7 @@ const DashboardChatHistory: React.FC<DashboardChatHistoryProps> = ({ onWorkflowR
|
||||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
||||||
>
|
>
|
||||||
<div className={styles.errorContainer}>
|
<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
|
<button
|
||||||
onClick={refetch}
|
onClick={refetch}
|
||||||
className={styles.retryButton}
|
className={styles.retryButton}
|
||||||
|
|
@ -64,7 +64,7 @@ const DashboardChatHistory: React.FC<DashboardChatHistoryProps> = ({ onWorkflowR
|
||||||
>
|
>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<h2 className={styles.history_title}>Workflow History</h2>
|
<h2 className={styles.history_title}>Workflow-Verlauf</h2>
|
||||||
<div className={styles.workflowCount}>
|
<div className={styles.workflowCount}>
|
||||||
{workflows.length} {workflows.length === 1 ? 'Workflow' : 'Workflows'}
|
{workflows.length} {workflows.length === 1 ? 'Workflow' : 'Workflows'}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -73,7 +73,7 @@ const DashboardChatHistory: React.FC<DashboardChatHistoryProps> = ({ onWorkflowR
|
||||||
<div className={styles.scrollableContent}>
|
<div className={styles.scrollableContent}>
|
||||||
{workflows.length === 0 ? (
|
{workflows.length === 0 ? (
|
||||||
<div className={styles.emptyState}>
|
<div className={styles.emptyState}>
|
||||||
No workflows available
|
Keine Workflows verfügbar
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.workflowsList}>
|
<div className={styles.workflowsList}>
|
||||||
|
|
|
||||||
|
|
@ -61,14 +61,13 @@
|
||||||
|
|
||||||
.horizontalLine {
|
.horizontalLine {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--color-gray);
|
|
||||||
height: 2px;
|
height: 2px;
|
||||||
margin-top: 19px;
|
margin-top: 19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontalLineLight {
|
.horizontalLineLight {
|
||||||
width: calc(100%);
|
width: calc(100%);
|
||||||
background-color: var(--color-gray);
|
background-color: var(--color-gray-disabled);
|
||||||
height: 2px;
|
height: 2px;
|
||||||
margin-top: 39px;
|
margin-top: 39px;
|
||||||
margin-left: -20px;
|
margin-left: -20px;
|
||||||
|
|
@ -101,7 +100,6 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
border-bottom: 1px solid rgba(0, 255, 0, 0.1);
|
|
||||||
animation: fadeIn 0.3s ease-in;
|
animation: fadeIn 0.3s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const DashboardPrompt: React.FC<DashboardPromptProps> = ({
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
onToggleCollapse
|
onToggleCollapse
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState("Prompt Set");
|
const [activeTab, setActiveTab] = useState("Prompt Vorlage");
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -27,7 +27,7 @@ const DashboardPrompt: React.FC<DashboardPromptProps> = ({
|
||||||
const promptId = searchParams.get('promptId');
|
const promptId = searchParams.get('promptId');
|
||||||
|
|
||||||
if (expandedPrompt) {
|
if (expandedPrompt) {
|
||||||
setActiveTab("Prompt Set");
|
setActiveTab("Prompt Vorlage");
|
||||||
} else if (promptId) {
|
} else if (promptId) {
|
||||||
setActiveTab("Einstellungen");
|
setActiveTab("Einstellungen");
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ const DashboardPrompt: React.FC<DashboardPromptProps> = ({
|
||||||
<div className={ styles.prompt_header }>
|
<div className={ styles.prompt_header }>
|
||||||
<div className={ styles.prompt_button_div }>
|
<div className={ styles.prompt_button_div }>
|
||||||
{[
|
{[
|
||||||
"Prompt Set",
|
"Prompt Vorlage",
|
||||||
"Einstellungen"
|
"Einstellungen"
|
||||||
].map((tab) => (
|
].map((tab) => (
|
||||||
<div key={tab} className={styles.buttonWrapper}>
|
<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' }}>
|
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||||
{activeTab === "Prompt Set" ? (
|
{activeTab === "Prompt Vorlage" ? (
|
||||||
<DashboardPromptSet onPromptRun={onPromptRun} />
|
<DashboardPromptSet onPromptRun={onPromptRun} />
|
||||||
) : (
|
) : (
|
||||||
<DashboardPromptSettings />
|
<DashboardPromptSettings />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { usePrompts, Prompt } from '../../../../hooks/usePrompts';
|
import { usePrompts, usePromptOperations, Prompt } from '../../../../hooks/usePrompts';
|
||||||
import DashboardPromptSetItem from './DashboardPromptSetItem';
|
import DashboardPromptSetItem from './DashboardPromptSetItem';
|
||||||
|
import DashboardPromptSetModal from './DashboardPromptSetModal';
|
||||||
import styles from './DashboardPromptSet.module.css';
|
import styles from './DashboardPromptSet.module.css';
|
||||||
import { FaPlus } from 'react-icons/fa';
|
import { FaPlus } from 'react-icons/fa';
|
||||||
|
|
||||||
|
|
@ -10,6 +11,18 @@ interface DashboardPromptSetProps {
|
||||||
|
|
||||||
function DashboardPromptSet({ onPromptRun }: DashboardPromptSetProps) {
|
function DashboardPromptSet({ onPromptRun }: DashboardPromptSetProps) {
|
||||||
const { prompts, loading, error, refetch } = usePrompts();
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -37,7 +50,7 @@ function DashboardPromptSet({ onPromptRun }: DashboardPromptSetProps) {
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className={styles.headerButtons}>
|
<div className={styles.headerButtons}>
|
||||||
<button className={styles.addButton} onClick={() => console.log('add prompt')}>
|
<button className={styles.addButton} onClick={() => setIsModalOpen(true)}>
|
||||||
<FaPlus />
|
<FaPlus />
|
||||||
Neuer Prompt
|
Neuer Prompt
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -66,6 +79,13 @@ function DashboardPromptSet({ onPromptRun }: DashboardPromptSetProps) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DashboardPromptSetModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
onSubmit={handleCreatePrompt}
|
||||||
|
isLoading={creatingPrompt}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,46 @@
|
||||||
color: var(--color-bg);
|
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 {
|
.promptContent {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -14,18 +14,26 @@ interface DashboardPromptSetItemProps {
|
||||||
function DashboardPromptSetItem({ prompt, onDelete, onRun }: DashboardPromptSetItemProps) {
|
function DashboardPromptSetItem({ prompt, onDelete, onRun }: DashboardPromptSetItemProps) {
|
||||||
const { handlePromptDelete, deletingPrompts, deleteError } = usePromptOperations();
|
const { handlePromptDelete, deletingPrompts, deleteError } = usePromptOperations();
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
|
||||||
const isDeleting = deletingPrompts.has(prompt.id);
|
const isDeleting = deletingPrompts.has(prompt.id);
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDeleteClick = async () => {
|
||||||
if (window.confirm(`Möchten Sie den Prompt "${prompt.name}" wirklich löschen?`)) {
|
if (showDeleteConfirm) {
|
||||||
const success = await handlePromptDelete(prompt.id);
|
const success = await handlePromptDelete(prompt.id);
|
||||||
if (success && onDelete) {
|
if (success && onDelete) {
|
||||||
onDelete();
|
onDelete();
|
||||||
}
|
}
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
} else {
|
||||||
|
setShowDeleteConfirm(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCancelDelete = () => {
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleRun = () => {
|
const handleRun = () => {
|
||||||
onRun(prompt);
|
onRun(prompt);
|
||||||
};
|
};
|
||||||
|
|
@ -76,12 +84,15 @@ function DashboardPromptSetItem({ prompt, onDelete, onRun }: DashboardPromptSetI
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDeleteClick}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
className={`${styles.actionButton} ${styles.deleteButton}`}
|
className={`${styles.actionButton} ${styles.deleteButton} ${showDeleteConfirm ? styles.confirm : ''}`}
|
||||||
title="Prompt löschen"
|
title={showDeleteConfirm ? "Klicken Sie erneut zum Bestätigen" : "Prompt löschen"}
|
||||||
|
onBlur={handleCancelDelete}
|
||||||
>
|
>
|
||||||
<AiOutlineDelete size={16} />
|
<AiOutlineDelete size={16} />
|
||||||
|
{isDeleting && <span className={styles.actionText}>Löschen...</span>}
|
||||||
|
{showDeleteConfirm && <span className={styles.actionText}>Zum Bestätigen klicken</span>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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"
|
title="Download file"
|
||||||
>
|
>
|
||||||
<FaDownload className={styles.actionIcon} />
|
<FaDownload className={styles.actionIcon} />
|
||||||
{isDownloading && <span className={styles.actionText}>Downloading...</span>}
|
{isDownloading && <span className={styles.actionText}>Laden...</span>}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`${styles.deleteButton} ${isDeleting ? styles.deleting : ''} ${showDeleteConfirm ? styles.confirm : ''}`}
|
className={`${styles.deleteButton} ${isDeleting ? styles.deleting : ''} ${showDeleteConfirm ? styles.confirm : ''}`}
|
||||||
|
|
@ -174,8 +174,8 @@ const DateienItem = ({ file, onDelete, onOptimisticDelete }: DateienItemProps) =
|
||||||
onBlur={handleCancelDelete}
|
onBlur={handleCancelDelete}
|
||||||
>
|
>
|
||||||
<FaTrash className={styles.actionIcon} />
|
<FaTrash className={styles.actionIcon} />
|
||||||
{isDeleting && <span className={styles.actionText}>Deleting...</span>}
|
{isDeleting && <span className={styles.actionText}>Löschen...</span>}
|
||||||
{showDeleteConfirm && <span className={styles.actionText}>Click to confirm</span>}
|
{showDeleteConfirm && <span className={styles.actionText}>Zum Bestätigen klicken...</span>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ const useSidebarData = () => {
|
||||||
return useMemo(() => [
|
return useMemo(() => [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'Organisation',
|
name: 'Team-Bereich',
|
||||||
link: '/organisation',
|
link: '/team-bereich',
|
||||||
icon: MdOutlineWorkOutline,
|
icon: MdOutlineWorkOutline,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -29,36 +29,24 @@ const useSidebarData = () => {
|
||||||
link: '/dateien',
|
link: '/dateien',
|
||||||
icon: FaRegFileAlt,
|
icon: FaRegFileAlt,
|
||||||
},
|
},
|
||||||
{
|
/*{
|
||||||
id: '4',
|
|
||||||
name: 'Mitglieder',
|
|
||||||
link: '/mitglieder',
|
|
||||||
icon: RiTeamLine,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
name: 'Nachrichten',
|
|
||||||
link: '',
|
|
||||||
icon: BiInfoSquare,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
id: '6',
|
||||||
name: 'Logs',
|
name: 'Logs',
|
||||||
link: '',
|
link: '',
|
||||||
icon: TbLogs ,
|
icon: TbLogs ,
|
||||||
},
|
},*/
|
||||||
{
|
{
|
||||||
id: '7',
|
id: '7',
|
||||||
name: 'Einstellungen',
|
name: 'Einstellungen',
|
||||||
link: '/einstellungen',
|
link: '/einstellungen',
|
||||||
icon: GoGear,
|
icon: GoGear,
|
||||||
},
|
},
|
||||||
{
|
/*{
|
||||||
id: '8',
|
id: '8',
|
||||||
name: 'Help',
|
name: 'Help',
|
||||||
link: '',
|
link: '',
|
||||||
icon: BiInfoSquare,
|
icon: BiInfoSquare,
|
||||||
},
|
},*/
|
||||||
], []);
|
], []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ user, isLoading, error }) =>
|
||||||
<FaUserCircle className={styles.user_icon} />
|
<FaUserCircle className={styles.user_icon} />
|
||||||
<div className={styles.text_content}>
|
<div className={styles.text_content}>
|
||||||
<h1>{ user.name }</h1>
|
<h1>{ user.name }</h1>
|
||||||
<p>role: {user.role}</p>
|
<p>Rolle: {user.role}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,6 @@ export function useWorkflowMessages(workflowId: string | null, messageId?: strin
|
||||||
// Error is already handled by useApiRequest
|
// Error is already handled by useApiRequest
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMessages();
|
fetchMessages();
|
||||||
}, [workflowId, messageId]);
|
}, [workflowId, messageId]);
|
||||||
|
|
@ -250,8 +249,6 @@ export function useFilePreview() {
|
||||||
const { request } = useApiRequest();
|
const { request } = useApiRequest();
|
||||||
|
|
||||||
const fetchPreview = async (fileId: string | number) => {
|
const fetchPreview = async (fileId: string | number) => {
|
||||||
console.log('fetchPreview called with fileId:', fileId, 'type:', typeof fileId);
|
|
||||||
|
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
setError("File ID not available");
|
setError("File ID not available");
|
||||||
return;
|
return;
|
||||||
|
|
@ -272,44 +269,71 @@ export function useFilePreview() {
|
||||||
numericFileId = parseInt(String(fileId), 10);
|
numericFileId = parseInt(String(fileId), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Parsed fileId:', numericFileId, 'isNaN:', isNaN(numericFileId));
|
|
||||||
|
|
||||||
if (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.`);
|
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({
|
const response = await request({
|
||||||
url: `/api/workflows/files/${numericFileId}/preview`,
|
url: `/api/workflows/files/${numericFileId}/preview`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('API response:', response);
|
|
||||||
|
|
||||||
// Handle response as object with metadata and preview content
|
// Handle response as object with metadata and preview content
|
||||||
if (typeof response === 'object' && response !== null) {
|
if (typeof response === 'object' && response !== null) {
|
||||||
setFileMetadata(response);
|
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
|
// Try different possible property names for the content
|
||||||
const content = response.preview || response.content || response.data || response.previewContent;
|
const content = response.preview || response.content || response.data || response.previewContent;
|
||||||
|
|
||||||
// Debug for PDF issues only
|
console.log('Extracted content:', content ? 'has content' : 'null/empty');
|
||||||
if (response.mimeType === 'application/pdf') {
|
console.log('Content type:', typeof content);
|
||||||
console.log('PDF Preview Debug:', {
|
console.log('Content length:', content?.length);
|
||||||
hasPreview: !!response.preview,
|
|
||||||
previewLength: response.preview?.length,
|
// If base64Encoded is true and we have content, try to decode it
|
||||||
hasBase64Flag: response.base64Encoded,
|
let processedContent = content;
|
||||||
mimeType: response.mimeType
|
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 {
|
} else {
|
||||||
// Fallback if response is just the content
|
// Fallback if response is just the content
|
||||||
setPreviewContent(response);
|
setPreviewContent(response);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('File preview error:', err);
|
|
||||||
setError(err.message || "Failed to load preview");
|
setError(err.message || "Failed to load preview");
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
@ -332,3 +356,74 @@ export function useFilePreview() {
|
||||||
clearPreview
|
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 */
|
/* Height classes for different states */
|
||||||
.chatArea15vh {
|
.chatArea15vh {
|
||||||
height: 15vh;
|
height: 35vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatArea40vh {
|
.chatArea40vh {
|
||||||
height: 40vh;
|
height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatArea45vh {
|
.chatArea45vh {
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.logArea40vh {
|
.logArea40vh {
|
||||||
height: 40vh;
|
height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logArea60vh {
|
.logArea60vh {
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,11 @@ import styles from './Einstellungen.module.css';
|
||||||
function Einstellungen() {
|
function Einstellungen() {
|
||||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||||
|
|
||||||
// Load saved theme preference on component mount
|
// Sync component state with current theme on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem('theme');
|
||||||
const prefersDark = savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
const prefersDark = savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
setIsDarkMode(prefersDark);
|
setIsDarkMode(prefersDark);
|
||||||
applyTheme(prefersDark);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const applyTheme = (isDark: boolean) => {
|
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