"use client" import type React from "react" import ReactMarkdown from "react-markdown" import { Loader2 } from "lucide-react" import { useState, useRef, useEffect, useCallback } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Card, CardContent } from "@/components/ui/card" import { Send, Upload, Download, Check, Info } from "lucide-react" import ChartRenderer from "./chart-renderer" import UserRegistrationModal from "./user-registration-modal" import UsageLimitIndicator from "./usage-limit-indicator" import CTAMessage from "./cta-message" import { createConversation, saveMessage, incrementUserMessageCount, getUserMessageCount } from "@/actions/chat-actions" import { uploadLoadProfile } from "@/actions/file-actions" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" // Add a style object for consistent font sizes const fontStyles = { base: { fontSize: "12px", }, logo: { fontSize: "14px", fontWeight: 500, }, heading: { fontSize: "18px", fontWeight: 600, }, } type Message = { id: string role: "user" | "assistant" | "cta" content: string hasChart?: boolean chartConfig?: any chartData?: any isLoading?: boolean userId?: string // For CTA messages isCTA?: boolean // Flag to identify CTA messages } // Stage-based content for guiding users const stageContent = [ { stage: 1, description: "Stellen Sie mir Ihre ersten Frage, und ich zeige Ihnen, wie Sie Energie sparen und effizienter werden können!", prompts: [ "Wie kann ich Energie effizienter nutzen?", "Welche vorteile habe ich durch die ISO 50001?", "Wie senke ich mit ecoplanet meine Energiekosten?", ], }, { stage: 2, description: "Laden Sie das ausgefüllte Lastgang-Template hoch, und ich analysiere Ihren Energieverbrauch! Ich berechne Grund- und Spitzenlasten und visualisiere Ihren Lastgang für einen Tag. Alternativ können sie den Demo-Lastgang verwenden.", prompts: [ "Was war mein Energieverbauch im Mai 2024", "Visualisiere meinen Lastgang am 20.04.2024", "Was war meine Grundlast in 3. Quartal 2024", ], }, { stage: 3, description: "Lassen Sie uns Ihre Energieziele und Maßnahmen für die ISO 50001 planen! Ich helfe Ihnen, Ihre Effizienz zu steigern und CO₂ zu reduzieren.", prompts: [ "Welche Energieziele sind sinnvoll?", "Welche Maßnahmen schlagen Sie vor?", "Wie plane ich CO₂-Reduktion?", ], }, ] const initialMessage: Message = { id: "initial", role: "assistant", content: "Willkommen bei Ember AI! Ich bin Ihr virtueller Assistent für alle Fragen rund um Energiemanagement, Energiebeschaffung und Energiemanagement-Zertifizierungen. Sie können mich zu diesen Themen befragen oder Ihren Lastgang analysieren lassen. Wie kann ich Ihnen heute helfen?", } const MAX_MESSAGES = 2 export default function EmberAIChat() { const [messages, setMessages] = useState([initialMessage]) const [input, setInput] = useState("") const [isLoading, setIsLoading] = useState(false) const [file, setFile] = useState(null) const [useDemo, setUseDemo] = useState(true) const messagesEndRef = useRef(null) const messagesContainerRef = useRef(null) const fileInputRef = useRef(null) const [errorMessage, setErrorMessage] = useState(null) const [isOffline, setIsOffline] = useState(!navigator.onLine) const chatContainerRef = useRef(null) const [fileUploadStatus, setFileUploadStatus] = useState<"idle" | "uploading" | "success" | "error">("idle") const [fileUploadError, setFileUploadError] = useState(null) // Add this with the other state variables const [currentStage, setCurrentStage] = useState(1) // User and conversation state const [userId, setUserId] = useState(null) const [conversationId, setConversationId] = useState(null) const [messageCount, setMessageCount] = useState(0) const [showRegistrationModal, setShowRegistrationModal] = useState(false) const [showCTA, setShowCTA] = useState(false) const [hasAttemptedToSend, setHasAttemptedToSend] = useState(false) const [pendingMessage, setPendingMessage] = useState(null) // Flag to prevent duplicate message processing const [isProcessingPendingMessage, setIsProcessingPendingMessage] = useState(false) // Flag to track if we're waiting for the last message response const [isWaitingForLastMessageResponse, setIsWaitingForLastMessageResponse] = useState(false) // Flag to prevent adding CTA message from useEffect const [shouldSkipCTACheck, setShouldSkipCTACheck] = useState(false) // Helper function to check if a CTA message already exists in the messages array const hasCTAMessage = useCallback(() => { return messages.some((msg) => msg.isCTA === true) }, [messages]) // Debug logging function const logDebug = (message: string, ...args: any[]) => { console.log(`[EmberAI Debug] ${message}`, ...args) } useEffect(() => { // Offline/Online-Status überwachen const handleOnline = () => setIsOffline(false) const handleOffline = () => setIsOffline(true) window.addEventListener("online", handleOnline) window.addEventListener("offline", handleOffline) return () => { window.removeEventListener("online", handleOffline) window.removeEventListener("offline", handleOffline) } }, []) // Fixed scrolling issue by storing the scroll position before updating useEffect(() => { if (!messagesContainerRef.current) return // Store current scroll information const container = messagesContainerRef.current const isScrolledToBottom = container.scrollHeight - container.clientHeight <= container.scrollTop + 50 // After the DOM updates, restore scroll position if needed if (isScrolledToBottom) { scrollToBottom() } }, [messages]) // Load user message count when userId changes useEffect(() => { if (!userId) return const loadMessageCount = async () => { try { const count = await getUserMessageCount(userId) logDebug(`Loaded message count for user ${userId}: ${count}`) setMessageCount(count) // Only check for CTA if we're not waiting for an API response, not explicitly skipping the check, // and the user is not registered if (count >= MAX_MESSAGES && !isLoading && !isWaitingForLastMessageResponse && !shouldSkipCTACheck && !userId) { logDebug(`User ${userId} has reached the message limit (${MAX_MESSAGES}), showing CTA`) // This is for Scenario 1 - user already at limit when logging in logDebug("Scenario 1: User already at message limit when logging in") // Only add CTA message if it doesn't already exist if (!hasCTAMessage()) { logDebug("No CTA message found, adding one") addCTAMessage() } else { logDebug("CTA message already exists, not adding another one") } } // Process any pending message after login if (pendingMessage && !isProcessingPendingMessage) { logDebug(`Processing pending message after login: "${pendingMessage}"`) // Set flag to prevent duplicate processing setIsProcessingPendingMessage(true) const messageToSend = pendingMessage // Clear pending message immediately to prevent duplicate processing setPendingMessage(null) // If the user has already reached their limit, don't send the message if (count >= MAX_MESSAGES) { logDebug("User already at message limit, not sending pending message") setInput("") // Clear the input field setIsProcessingPendingMessage(false) return } // Add a small delay before sending to ensure state updates have completed setTimeout(() => { logDebug("Now sending pending message after delay") processSendMessage(messageToSend).finally(() => { setIsProcessingPendingMessage(false) }) }, 300) } } catch (error) { console.error("Error loading message count:", error) setIsProcessingPendingMessage(false) } } loadMessageCount() }, [userId, pendingMessage, isLoading, isWaitingForLastMessageResponse, hasCTAMessage, shouldSkipCTACheck]) const scrollToBottom = () => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: "smooth", block: "end" }) } } // Add CTA message to the chat const addCTAMessage = () => { if (!userId) { logDebug("Cannot add CTA message: no userId") return } // Double-check that CTA message doesn't already exist if (hasCTAMessage()) { logDebug("CTA message already exists, not adding another one") return } logDebug("Adding CTA message to chat") const ctaMessage: Message = { id: `cta-${Date.now()}`, role: "assistant", content: "cta-special-content", // Special marker to identify this as a CTA message userId: userId, isCTA: true, // Flag to identify this as a CTA message } // Use a functional update to ensure we're working with the latest state setMessages((prevMessages) => { // Double check again inside the update function const alreadyHasCTA = prevMessages.some((msg) => msg.isCTA === true) if (alreadyHasCTA) { logDebug("CTA message already exists (checked inside setMessages), not adding another one") return prevMessages } logDebug("Adding CTA message to messages array") return [...prevMessages, ctaMessage] }) // Also save this as a system message in the database if we have a conversation if (conversationId) { saveMessage( conversationId, "assistant", "Vielen Dank für Ihr Interesse an unserem KI-gestützten Energie-Experten. Sie haben Ihr Nachrichtenlimit erreicht. Möchten Sie tiefer in Ihre Energiepotenziale eintauchen? Fordern Sie jetzt eine kostenlose Demo an oder lassen Sie sich einen personalisierten Energiebericht erstellen – maßgeschneidert für Ihr Unternehmen!", ) } } // Extract JSON from a string, handling both direct JSON and markdown code blocks const extractJsonFromString = (content: string): any | null => { try { // First try to parse the content directly as JSON return JSON.parse(content.trim()) } catch (e) { // If direct parsing fails, look for JSON in markdown code blocks const jsonCodeBlockRegex = /```(?:json)?\s*([\s\S]*?)```/g const matches = [...content.matchAll(jsonCodeBlockRegex)] if (matches.length > 0) { for (const match of matches) { try { return JSON.parse(match[1].trim()) } catch (parseError) { console.error("Error parsing JSON in code block:", parseError) } } } return null } } // Validate chart configuration const validateChartConfig = (config: any): boolean => { if (!config || typeof config !== "object") { console.error("Invalid chart config: not an object", config) return false } // If type is missing, we'll default to line chart in the renderer if (!config.type) { console.warn("Chart type not specified, will default to line chart") config.type = "line" } else { // Normalize chart type to lowercase const originalType = config.type config.type = config.type.toLowerCase() // Handle variations of chart types if (config.type === "linechart") config.type = "line" if (config.type === "barchart") config.type = "bar" if (config.type === "piechart") config.type = "pie" if (originalType !== config.type) { console.log(`Normalized chart type from "${originalType}" to "${config.type}"`) } } // Check if we have series data for line and bar charts if ( (config.type === "line" || config.type === "bar") && (!config.series || !Array.isArray(config.series) || config.series.length === 0) ) { console.warn("No series defined for line/bar chart, will attempt to auto-generate from data") } return true } // Validate chart data const validateChartData = (data: any): boolean => { if (!data || !Array.isArray(data) || data.length === 0) { console.error("Invalid chart data: not an array or empty", data) return false } // Check if data items are objects if (typeof data[0] !== "object") { console.error("Invalid chart data: items are not objects", data[0]) return false } return true } const uploadFileAfterRegistration = async () => { if (userId && file && !useDemo) { try { setFileUploadStatus("uploading") const result = await uploadLoadProfile(userId, file) if (result.success) { setFileUploadStatus("success") logDebug(`File uploaded successfully after registration: ${result.url}`) } else { setFileUploadStatus("error") setFileUploadError(result.error || "Unbekannter Fehler beim Hochladen") logDebug(`File upload failed after registration: ${result.error}`) } } catch (error) { setFileUploadStatus("error") setFileUploadError("Fehler beim Hochladen der Datei") console.error("Error uploading file after registration:", error) } } } const handleRegistrationSuccess = async (newUserId: string, messageCount: number) => { logDebug(`Registration success: User ${newUserId} with message count ${messageCount}`) setUserId(newUserId) setShowRegistrationModal(false) // Get the latest message count from the database to ensure accuracy try { const currentCount = await getUserMessageCount(newUserId) logDebug(`Verified message count for user ${newUserId}: ${currentCount}`) setMessageCount(currentCount) // Create a new conversation if we don't have one if (!conversationId) { try { const newConversationId = await createConversation(newUserId) setConversationId(newConversationId) // Save the initial assistant message await saveMessage(newConversationId, "assistant", initialMessage.content) } catch (error) { console.error("Error creating conversation:", error) } } // Upload file if one was selected before registration await uploadFileAfterRegistration() // If the user has already reached their limit, show the CTA instead of sending the message if (currentCount >= MAX_MESSAGES) { logDebug(`User ${newUserId} has already reached the message limit (${MAX_MESSAGES})`) // Clear any pending message attempts setHasAttemptedToSend(false) setPendingMessage(null) setInput("") // Clear the input field to prevent accidental resubmission // Only add CTA if it doesn't already exist if (!hasCTAMessage()) { addCTAMessage() } return } // Just reset the flag to indicate we've attempted to send if (hasAttemptedToSend) { setHasAttemptedToSend(false) } } catch (error) { console.error("Error getting message count after registration:", error) } } // Process and send a message const processSendMessage = async (messageText: string) => { if (messageText.trim() === "") return logDebug(`Processing message: "${messageText}"`) // Reset error messages setErrorMessage(null) try { // First, get the current message count from the database to ensure accuracy if (userId) { const currentCount = await getUserMessageCount(userId) logDebug(`Current message count for user ${userId}: ${currentCount}`) setMessageCount(currentCount) // Only check message limit for unregistered users if (!userId && currentCount >= MAX_MESSAGES) { logDebug(`User has reached the message limit (${MAX_MESSAGES})`) // Only add CTA if it doesn't already exist if (!hasCTAMessage()) { addCTAMessage() } setInput("") // Clear input to prevent resubmission attempts return } } const userMessage: Message = { id: Date.now().toString(), role: "user", content: messageText, } // Add a loading message const loadingMessage: Message = { id: Date.now().toString() + "-loading", role: "assistant", content: "Denke nach...", isLoading: true, } setMessages((prev) => [...prev, userMessage, loadingMessage]) // Add this after the line: setMessages((prev) => [...prev, userMessage, loadingMessage]) // Update the stage based on user message count (excluding the initial assistant message) const userMessageCount = messages.filter((msg) => msg.role === "user").length + 1 const newStage = Math.min(userMessageCount + 1, 3) setCurrentStage(newStage) setInput("") setIsLoading(true) // Create conversation if it doesn't exist if (!conversationId && userId) { const newConversationId = await createConversation(userId) setConversationId(newConversationId) } // Save user message to database if (conversationId) { await saveMessage(conversationId, "user", userMessage.content) } const formData = new FormData() formData.append("prompt", messageText) // Add load profile file if available, but don't add any file for demo option if (file && !useDemo) { formData.append("load_profile", file) } // Add anti-abuse measures // Add a timestamp to prevent replay attacks formData.append("timestamp", Date.now().toString()) // Use proxy endpoint to avoid CORS issues const response = await fetch("/api/ember-ai", { method: "POST", body: formData, }) if (response.status === 429) { throw new Error("Zu viele Anfragen. Bitte versuchen Sie es später erneut.") } if (!response.ok) { throw new Error(`Server-Fehler: ${response.status} ${response.statusText}`) } let data try { data = await response.json() } catch (jsonError) { console.error("JSON-Parsing-Fehler:", jsonError) throw new Error("Die Antwort konnte nicht als JSON verarbeitet werden.") } if (!data || typeof data.message !== "string") { throw new Error("Ungültiges Antwortformat vom Server") } // Check if the response contains chart configuration let hasChart = false let chartConfig = null let chartData = null const originalMessage = data.message // Process chart data (keeping the existing chart processing code) // First check for legacy RECHART format if ( data.message.includes("RECHARTCONFIG_START") && data.message.includes("RECHARTCONFIG_END") && data.message.includes("RECHARTDATA_START") && data.message.includes("RECHARTDATA_END") ) { try { // Extract the chart configuration const configStartTag = "RECHARTCONFIG_START" const configEndTag = "RECHARTCONFIG_END" const configStart = data.message.indexOf(configStartTag) + configStartTag.length const configEnd = data.message.indexOf(configEndTag) if (configStart > 0 && configEnd > configStart) { const configContent = data.message.substring(configStart, configEnd).trim() chartConfig = extractJsonFromString(configContent) if (chartConfig) { logDebug("Found chart config:", chartConfig) } } // Extract the chart data const dataStartTag = "RECHARTDATA_START" const dataEndTag = "RECHARTDATA_END" const dataStart = data.message.indexOf(dataStartTag) + dataStartTag.length const dataEnd = data.message.indexOf(dataEndTag) if (dataStart > 0 && dataEnd > dataStart) { const dataContent = data.message.substring(dataStart, dataEnd).trim() chartData = extractJsonFromString(dataContent) if (chartData) { logDebug("Found chart data:", chartData) } } // Validate chart config and data const isConfigValid = validateChartConfig(chartConfig) const isDataValid = validateChartData(chartData) // If we have both valid config and data, we can render a chart if (isConfigValid && isDataValid) { hasChart = true // Clean the message by removing the chart configuration and data data.message = data.message .replace(/RECHARTCONFIG_START[\s\S]*?RECHARTCONFIG_END/, "") .replace(/RECHARTDATA_START[\s_S]*?RECHARTDATA_END/, "") .trim() // Add a note about the chart if the message is empty after removing JSON if (data.message.trim() === "") { data.message = "*Hier ist die grafische Darstellung der Daten:*" } } else { console.error("Invalid chart config or data:", { chartConfig, chartData }) } } catch (chartError) { console.error("Fehler beim Parsen der Diagrammkonfiguration:", chartError) // If parsing fails, we don't show a chart hasChart = false } } else { // If not in legacy format, look for JSON code blocks const jsonCodeBlockRegex = /```(?:json)?\s*([\s\S]*?)```/g const jsonMatches = [...data.message.matchAll(jsonCodeBlockRegex)] if (jsonMatches.length > 0) { try { // Try to find chart configuration and data in JSON blocks for (const match of jsonMatches) { try { const jsonContent = match[1].trim() const parsedJson = JSON.parse(jsonContent) // Check if this is a chart configuration if (parsedJson && typeof parsedJson === "object" && !Array.isArray(parsedJson)) { // If it has a type property or series property, it's likely a chart config if (parsedJson.type || parsedJson.series) { chartConfig = parsedJson logDebug("Found chart config in code block:", chartConfig) } } // Check if this is chart data (array of objects) else if (Array.isArray(parsedJson) && parsedJson.length > 0 && typeof parsedJson[0] === "object") { chartData = parsedJson logDebug("Found chart data in code block:", chartData) } } catch (parseError) { console.error("Error parsing JSON in code block:", parseError) } } // Validate chart config and data const isConfigValid = validateChartConfig(chartConfig) const isDataValid = validateChartData(chartData) // If we have both valid config and data, we can render a chart if (isConfigValid && isDataValid) { hasChart = true // Remove JSON code blocks from the message data.message = data.message.replace(jsonCodeBlockRegex, "").trim() // Add a note about the chart if the message is empty after removing JSON if (data.message.trim() === "") { data.message = "*Hier ist die grafische Darstellung der Daten:*" } } else { console.error("Invalid chart config or data:", { chartConfig, chartData }) } } catch (chartError) { console.error("Fehler beim Parsen der JSON-Blöcke:", chartError) // If parsing fails, we don't show a chart hasChart = false } } } // If we couldn't parse the chart, restore the original message if (!hasChart || !chartConfig || !chartData) { data.message = originalMessage } // Ensure chart config has at least an empty object if (hasChart && (!chartConfig || typeof chartConfig !== "object")) { chartConfig = { type: "line" } } // Add this block after the API response is received, just before creating assistantMessage: // Increment user message count in the database AFTER receiving the response if (userId) { await incrementUserMessageCount(userId) // Get the updated count after incrementing const updatedCount = await getUserMessageCount(userId) logDebug(`Updated message count for user ${userId} after response: ${updatedCount}`) setMessageCount(updatedCount) // Direct check: if we've just reached the message limit, ensure we'll show the CTA if (updatedCount >= MAX_MESSAGES) { logDebug(`User has now reached the message limit (${updatedCount}/${MAX_MESSAGES})`) // Set a flag to add the CTA after the message is displayed setIsWaitingForLastMessageResponse(true) // This is a fallback in case the other mechanism fails setTimeout(() => { logDebug("Fallback CTA check executing") if (!hasCTAMessage()) { logDebug("No CTA message found in fallback check, adding one") addCTAMessage() } }, 2000) } } const assistantMessage: Message = { id: data.thread_id || Date.now().toString() + "-assistant", role: "assistant", content: data.message, hasChart, chartConfig, chartData, } // Save assistant message to database if (conversationId) { await saveMessage(conversationId, "assistant", assistantMessage.content, hasChart, chartConfig, chartData) } // Replace the loading message with the actual response setMessages((prev) => { // Filter out the loading message and add the assistant message return prev.filter((msg) => !msg.isLoading).concat(assistantMessage) }) logDebug("Added assistant message to chat") // Check if we were waiting for the last message response // Handle this outside the setMessages callback for more reliability if (isWaitingForLastMessageResponse) { logDebug("Last message response received, will add CTA message after delay") // Add a delay to ensure the message is fully rendered first setTimeout(() => { // Check flags again inside the timeout to ensure we have the latest state logDebug("Timeout executed, checking if we should add CTA message") logDebug(`Current message count: ${messageCount}, MAX_MESSAGES: ${MAX_MESSAGES}`) // Only add CTA if it doesn't already exist if (!hasCTAMessage()) { logDebug("No CTA message found, adding one now") addCTAMessage() } else { logDebug("CTA message already exists, not adding another one") } // Reset the flags after the timeout completes setIsWaitingForLastMessageResponse(false) setShouldSkipCTACheck(false) }, 1500) } } catch (error) { console.error("Fehler beim Senden der Nachricht:", error) const errorMsg = error instanceof Error ? error.message : "Unbekannter Fehler bei der Kommunikation mit dem Server" setErrorMessage(errorMsg) const errorMessage: Message = { id: Date.now().toString() + "-error", role: "assistant", content: "Es tut mir leid, aber ich konnte Ihre Anfrage nicht verarbeiten. Bitte versuchen Sie es später noch einmal.", } // Replace the loading message with the error message setMessages((prev) => prev.filter((msg) => !msg.isLoading).concat(errorMessage)) // Reset the waiting flag if there was an error setIsWaitingForLastMessageResponse(false) // Reset the skip CTA check flag setShouldSkipCTACheck(false) } finally { setIsLoading(false) } } // Add this function to the component to implement a simple client-side throttling const useThrottledCallback = (callback: (...args: any[]) => any, delay: number) => { const [lastCall, setLastCall] = useState(0) return useCallback( (...args: any[]) => { const now = Date.now() if (now - lastCall > delay) { setLastCall(now) return callback(...args) } }, [callback, lastCall, delay], ) } // Then update the handleSendMessage function to use throttling // Add this near the other hooks at the top of the component const throttledProcessSendMessage = useThrottledCallback(processSendMessage, 1000) // Now modify the handleSendMessage function to add a check to prevent reopening the modal // And update the handleSendMessage function to use the throttled version const handleSendMessage = async () => { if (input.trim() === "") return logDebug(`User attempting to send message: "${input}"`) // Get the current number of user messages const userMessageCount = messages.filter((msg) => msg.role === "user").length // Check if user is registered - only require registration after 2 messages if (!userId && userMessageCount >= MAX_MESSAGES) { logDebug("User has sent 2 messages, storing pending message and showing registration modal") // Store the message to be sent after registration setPendingMessage(input) // Only show registration modal if it's not already showing if (!showRegistrationModal) { setShowRegistrationModal(true) setHasAttemptedToSend(true) } return } // If we have a userId or haven't reached the message limit yet, send the message await throttledProcessSendMessage(input) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault() handleSendMessage() } } const handleFileChange = async (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { const selectedFile = e.target.files[0] // Check if the file is an Excel file if (!selectedFile.name.toLowerCase().endsWith(".xlsx")) { setErrorMessage("Bitte laden Sie nur Excel-Dateien (.xlsx) hoch.") // Reset the file input if (fileInputRef.current) { fileInputRef.current.value = "" } return } setFile(selectedFile) setUseDemo(false) setErrorMessage(null) // Clear any previous error messages // Only upload to Supabase if we have a userId if (userId) { try { setFileUploadStatus("uploading") const result = await uploadLoadProfile(userId, selectedFile) if (result.success) { setFileUploadStatus("success") logDebug(`File uploaded successfully: ${result.url}`) } else { setFileUploadStatus("error") setFileUploadError(result.error || "Unbekannter Fehler beim Hochladen") logDebug(`File upload failed: ${result.error}`) } } catch (error) { setFileUploadStatus("error") setFileUploadError("Fehler beim Hochladen der Datei") console.error("Error uploading file:", error) } } else { // If no userId, we'll need to show the registration modal // The file will be uploaded after registration setShowRegistrationModal(true) } } } const handleExampleClick = (prompt: string) => { setInput(prompt) } const downloadTemplate = () => { // Direct the user to the Google Drive download URL window.open("https://drive.google.com/uc?export=download&id=1RjFu-QVMy7eGHFLy4HANAAx2t8mQPp--", "_blank") // MARK: HTTPS URL - correct usage } return ( <>
{messages.map((message) => (
{message.role === "assistant" && !message.isCTA && ( Ember AI )}
{message.isLoading ? (
{message.content}
) : message.content === "cta-special-content" ? (
) : (
{message.content}
)} {message.hasChart && message.chartConfig && message.chartData && (
)}
))}
{isOffline && (

Sie sind offline. Bitte stellen Sie eine Internetverbindung her, um mit Ember AI zu chatten.

)} {errorMessage && (

Technischer Fehler: {errorMessage}

Bitte versuchen Sie es später erneut oder kontaktieren Sie den Support.

)}
{/* Usage indicator above everything */} {userId ? ( // -1 indicates unlimited ) : ( msg.role === "user").length} maxMessages={MAX_MESSAGES} /> )} {/* Condensed UI: Example prompts and load profile source in the same row */}
{/* Replace the existing example prompts section with this */}
{stageContent[currentStage - 1].prompts.map((prompt, index) => ( ))}
{/* Load profile source in a grey container - redesigned for better usability */}
Lastgang wählen: Ihre Daten sind bei uns sicher und werden nur im Rahmen dieser Demo genutzt. Nach Beendigung der Demo werden diese sofort gelöscht.
{/* Rest of the container content remains unchanged */}
{!useDemo && (
{file && (

{file.name.length > 15 ? file.name.substring(0, 12) + "..." : file.name}

{fileUploadStatus === "success" && ( )}
)} {fileUploadStatus === "error" && fileUploadError && (

{fileUploadError}

)}
)}
{/* Chat input field at the bottom */}
setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder="Frage stellen..." disabled={isLoading || isWaitingForLastMessageResponse || (userId && messageCount >= MAX_MESSAGES)} className="flex-1 border-[#e5e7eb] focus-visible:ring-[#4DC779]" style={fontStyles.base} />
{/* Modals */} setShowRegistrationModal(false)} onSuccess={handleRegistrationSuccess} containerRef={chatContainerRef} /> ) }

Die Energiemanagement-Software für Unternehmen

Energiemanagement, das Sie voranbringt

Mit dem ecoplanet Cockpit und KI-gestützten Analysen erreichen Sie maximale Ersparnisse bei minimalem Aufwand.

Mit Experten sprechen
Kostenfrei · Unverbindlich · Maßgeschneidert
ecoplanet Software Dashboard mit Budget und Kostenstatus Strom

Über 100 Unternehmen sparen bereits mit der ecoplanet Software

Ihr gesamtes Energiemanagement – Alles an einem Ort

Das ecoplanet Cockpit vereint ganzheitliches Energiemanagement durch das Monitoring und die Steuerung von Energieverbrauchern, die Integration intelligenter Beschaffungsstrategien sowie die Anpassung an regulatorische Anforderungen.

Energieverbrauch verstehen

Maximieren Sie die Energieeffizienz direkt über das benutzerfreundliche Interface des ecoplanet Cockpits. Entdecken Sie unüblichen Verbrauch mit der ecoplanet-Verbrauchstransparenz und erhalten Sie sofort Warnungen bei Veränderungen. Nutzen Sie förderfähige Messtechnik, um Verbrauchsanomalien noch detaillierter zu identifizieren. Ember, die künstliche Intelligenz von ecoplanet, unterstützt Sie dabei, Energieeffizienz zu steigern und Kosten zu senken.

Übersicht der Messstellen in der ecoplanet-Software. Das Dashboard zeigt Echtzeit-Daten zu Strom-, Gas- und Druckluftverbrauch mit Standortangaben und Leistungsdiagrammen.
Dashboard der ecoplanet-Software zur Energieverwaltung. Die Benutzeroberfläche zeigt Marktperformance, Kostenprognosen und eine Übersicht über Energieeinkäufe nach Jahr und Energieart.

Energieeinkauf optimieren

Optimieren Sie Ihre Energiebeschaffung: Im ecoplanet Cockpit können Sie Ihre individuelle Beschaffungsstrategie direkt festlegen, Angebote vergleichen und die automatisierte Marktfolge nutzen. Erzielen Sie mühelos direkte und transparente Einsparungen. Mit Zielpreisalarmen und Marktupdates bleiben Sie stets informiert und können vorausschauend agieren. Je nach Planungshorizont haben Sie die Möglichkeit bereits jetzt Teilmengen für die kommenden Jahre zu fixieren.

Regulatorische Anforderung einfach umsetzen

Navigieren Sie mühelos durch regulatorische Anforderungen mit dem ecoplanet Energiemanager Pro. Behalten Sie Berichtspflichten im Blick, automatisieren Sie Energieaudits und CO2-Reporting, und bleiben Sie immer informiert. Im Prozess der ISO 50001 unterstützt der Energiemanager Pro Sie umfassend, indem er alle notwendigen Formerfordernisse, wie Energieleistungs-kennzahlen (EnPIs) nach ISO 50006, abbildet.

Benutzeroberfläche der ecoplanet-Software zur Energieoptimierung. Die Maßnahmenübersicht zeigt den PDCA-Zyklus mit geplanten, umgesetzten und überprüften Maßnahmen zur Effizienzsteigerung.

Weitere Kundenstimmen

Zahlreiche Kunden profitieren bereits von ecoplanet
14%
Energiekosten eingespart bis heute

"ecoplanet hat mir nicht nur geholfen, Echtzeit-Transparenz über den Energieverbrauch meines Unternehmens zu erlangen, sondern definiert mir auch einen spezifischen Energieeffizienz-Fahrplan, mit dem ich Nachhaltigkeitsziele erreichen kann."

Jonathan Schmidt
Büchel GmbH
5%
Energiekosten eingespart bis heute

"Unser Ziel ist es, für jedes Fertigungswerkstück einen realistischen Kostensatz zu bestimmen, basierend auf tatsächlichem Stromverbrauch. Dafür sind präzise Einzelmessungen entscheidend. Wir sind gespannt auf die neue Ember AI, um zukünftig intelligente Handlungsempfehlungen erhalten zu können."

Björn Katthage
R&R Formentechnik
7%
Energiekosten eingespart bis heute

"Die Verwaltung des Energieverbrauchs in all unseren Seniorenresidenzen ist keine leichte Aufgabe. Die Unterstützung von ecoplanet bei der Energietransparenz und -beschaffung ist von unschätzbarem Wert für unsere Bemühungen, Kosten zu senken und zu optimieren."

Christoph Mosler
Alloheim Senioren-Residenzen GmbH

Unser nächstes Webinar

Jetzt anmelden!
Aktuell haben wir keine anstehenden Events.

Ihre Einsparungen sind
unser Erfolg!

ecoplanet verbindet das fortschrittliche ecoplanet Cockpit, die umfassende Energiemanagement-KI Ember und das Fachwissen eines Teams aus Energieexperten. Unser Ziel ist es, gemeinsam mit Ihnen Pionierarbeit in der Energiewende zu leisten.

Jetzt Kontakt aufnehmen
30-minütiges Gespräch • Einsparungen aufzeigen • Strategie entwickeln