- Layout improvements for the Chat client

This commit is contained in:
Josako
2025-07-23 18:06:47 +02:00
parent ccc1a2afb8
commit 32df3d0589
14 changed files with 339 additions and 93 deletions

View File

@@ -791,11 +791,6 @@
display: inline-block; display: inline-block;
} }
.working-animation {
width: 20px;
height: 20px;
}
/* Mobile responsiveness for progress icons */ /* Mobile responsiveness for progress icons */
@media (max-width: 768px) { @media (max-width: 768px) {
.progress-icon { .progress-icon {
@@ -803,9 +798,4 @@
height: 16px; height: 16px;
margin-right: 6px; margin-right: 6px;
} }
.working-animation {
width: 16px;
height: 16px;
}
} }

View File

@@ -28,23 +28,6 @@
overflow-y: auto; overflow-y: auto;
} }
.sidebar-logo {
text-align: center;
margin-bottom: 20px;
}
.sidebar-logo img {
max-width: 100%;
max-height: 100px;
}
.sidebar-make-name {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.sidebar-explanation { .sidebar-explanation {
margin-top: 20px; margin-top: 20px;
overflow-y: auto; overflow-y: auto;

View File

@@ -23,14 +23,6 @@
margin-bottom: 5px; margin-bottom: 5px;
} }
.form-field label {
display: block;
margin-bottom: 6px;
font-weight: 500;
font-size: 0.9rem;
color: #555;
}
.form-field input, .form-field input,
.form-field select, .form-field select,
.form-field textarea { .form-field textarea {
@@ -71,11 +63,6 @@
margin-right: 8px; margin-right: 8px;
} }
.checkbox-text {
font-size: 0.9rem;
color: #555;
}
.field-description { .field-description {
display: block; display: block;
margin-top: 5px; margin-top: 5px;

View File

@@ -21,11 +21,15 @@ active_text_color<template>
:allow-file-upload="true" :allow-file-upload="true"
:allow-voice-message="false" :allow-voice-message="false"
:form-data="currentInputFormData" :form-data="currentInputFormData"
:active-ai-message="activeAiMessage"
:api-prefix="apiPrefix"
@send-message="sendMessage" @send-message="sendMessage"
@update-message="updateCurrentMessage" @update-message="updateCurrentMessage"
@upload-file="handleFileUpload" @upload-file="handleFileUpload"
@record-voice="handleVoiceRecord" @record-voice="handleVoiceRecord"
@submit-form="submitFormFromInput" @submit-form="submitFormFromInput"
@specialist-error="handleSpecialistError"
@specialist-complete="handleSpecialistComplete"
ref="chatInput" ref="chatInput"
class="chat-input-area" class="chat-input-area"
></chat-input> ></chat-input>
@@ -150,6 +154,11 @@ export default {
return this.isSearching ? this.filteredMessages : this.allMessages; return this.isSearching ? this.filteredMessages : this.allMessages;
}, },
// Active AI message that should be shown in ChatInput
activeAiMessage() {
return this.allMessages.find(msg => msg.isTemporarilyAtBottom);
},
hasMessages() { hasMessages() {
return this.allMessages.length > 0; return this.allMessages.length > 0;
}, },

View File

@@ -1,6 +1,21 @@
<template> <template>
<div class="chat-input-container"> <div class="chat-input-container">
<!-- Material Icons worden nu globaal geladen in scripts.html --> <!-- Material Icons worden nu globaal geladen in scripts.html -->
<!-- Active AI Message Area -->
<div v-if="activeAiMessage" class="active-ai-message-area">
<chat-message
:message="activeAiMessage"
:is-submitting-form="false"
:api-prefix="apiPrefix"
:is-latest-ai-message="true"
:is-in-input-area="true"
@image-loaded="handleImageLoaded"
@specialist-complete="$emit('specialist-complete', $event)"
@specialist-error="$emit('specialist-error', $event)"
></chat-message>
</div>
<!-- Dynamisch formulier container --> <!-- Dynamisch formulier container -->
<div v-if="formData" class="dynamic-form-container"> <div v-if="formData" class="dynamic-form-container">
<!-- De titel wordt in DynamicForm weergegeven en niet hier om dubbele titels te voorkomen --> <!-- De titel wordt in DynamicForm weergegeven en niet hier om dubbele titels te voorkomen -->
@@ -62,13 +77,15 @@
<script> <script>
// Importeer de benodigde componenten // Importeer de benodigde componenten
import DynamicForm from './DynamicForm.vue'; import DynamicForm from './DynamicForm.vue';
import ChatMessage from './ChatMessage.vue';
import { useIconManager } from '../js/composables/useIconManager.js'; import { useIconManager } from '../js/composables/useIconManager.js';
import { useTranslationClient } from '../js/composables/useTranslation.js'; import { useTranslationClient } from '../js/composables/useTranslation.js';
export default { export default {
name: 'ChatInput', name: 'ChatInput',
components: { components: {
'dynamic-form': DynamicForm 'dynamic-form': DynamicForm,
'chat-message': ChatMessage
}, },
setup(props) { setup(props) {
const { watchIcon } = useIconManager(); const { watchIcon } = useIconManager();
@@ -103,8 +120,16 @@ export default {
type: Object, type: Object,
default: null default: null
}, },
activeAiMessage: {
type: Object,
default: null
},
apiPrefix: {
type: String,
default: ''
}
}, },
emits: ['send-message', 'update-message', 'submit-form'], emits: ['send-message', 'update-message', 'submit-form', 'specialist-complete', 'specialist-error'],
data() { data() {
return { return {
localMessage: this.currentMessage, localMessage: this.currentMessage,
@@ -377,6 +402,12 @@ export default {
if (JSON.stringify(newValues) !== JSON.stringify(this.formValues)) { if (JSON.stringify(newValues) !== JSON.stringify(this.formValues)) {
this.formValues = JSON.parse(JSON.stringify(newValues)); this.formValues = JSON.parse(JSON.stringify(newValues));
} }
},
handleImageLoaded() {
// Handle image loaded events from ChatMessage component
// This method can be used for layout adjustments if needed
console.log('Image loaded in active AI message');
} }
} }
}; };
@@ -500,4 +531,28 @@ export default {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
/* Active AI Message Area - positioned at top of ChatInput */
.active-ai-message-area {
margin-bottom: 15px;
padding: 12px;
background-color: var(--active-background-color);
color: var(--active-text-color);
border-radius: 8px;
font-family: Arial, sans-serif;
font-size: 14px;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
}
/* Ensure the active AI message integrates well with ChatInput styling */
.active-ai-message-area .message {
margin-bottom: 0;
max-width: 100%;
}
.active-ai-message-area .message-content {
background-color: transparent;
border: none;
padding: 0;
}
</style> </style>

View File

@@ -9,6 +9,7 @@
:task-id="message.taskId" :task-id="message.taskId"
:api-prefix="apiPrefix" :api-prefix="apiPrefix"
:is-latest-ai-message="isLatestAiMessage" :is-latest-ai-message="isLatestAiMessage"
:is-in-input-area="isInInputArea"
class="message-progress" class="message-progress"
@specialist-complete="handleSpecialistComplete" @specialist-complete="handleSpecialistComplete"
@specialist-error="handleSpecialistError" @specialist-error="handleSpecialistError"
@@ -169,11 +170,12 @@ export default {
isLatestAiMessage: { isLatestAiMessage: {
type: Boolean, type: Boolean,
default: false default: false
}
}, },
isInStickyArea: { isInInputArea: {
type: Boolean, type: Boolean,
default: false }, default: false
}
},
emits: ['image-loaded', 'retry-message', 'specialist-complete', 'specialist-error'], emits: ['image-loaded', 'retry-message', 'specialist-complete', 'specialist-error'],
data() { data() {
return { return {
@@ -309,7 +311,13 @@ export default {
// Add class for messages in sticky area // Add class for messages in sticky area
if (this.isInStickyArea) { if (this.isInStickyArea) {
classes += " sticky-area"; classes += " sticky-area";
} }
// Add class for messages in input area
if (this.isInInputArea) {
classes += " input-area";
}
return classes; return classes;
} }
} }
@@ -338,12 +346,10 @@ export default {
.message.ai.temporarily-at-bottom { .message.ai.temporarily-at-bottom {
background-color: var(--active-background-color); background-color: var(--active-background-color);
color: var(--active-text-color); color: var(--active-text-color);
border-left: 3px solid var(--active-text-color);
opacity: 0.9; opacity: 0.9;
border-radius: 8px; border-radius: 8px;
padding: 8px; padding: 8px;
margin: 8px 0; margin: 8px 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
} }
/* Styling for messages in sticky area - override history colors with active colors */ /* Styling for messages in sticky area - override history colors with active colors */
@@ -361,6 +367,14 @@ export default {
border-radius: 8px; border-radius: 8px;
padding: 12px; padding: 12px;
} }
/* Active styling for messages in input area */
.message.input-area .message-content {
background-color: var(--active-background-color);
color: var(--active-text-color);
border-radius: 8px;
padding: 12px;
}
.message-content { .message-content {
width: 100%; width: 100%;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;

View File

@@ -297,6 +297,7 @@ export default {
.dynamic-form { .dynamic-form {
padding: 15px; padding: 15px;
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
} }
.form-header { .form-header {
@@ -347,11 +348,10 @@ export default {
} }
.form-field label { .form-field label {
display: block; display: block;
margin-bottom: 6px; margin-bottom: 6px;
font-weight: 500; font-weight: 500;
font-size: 0.9rem; font-size: 0.9rem;
color: #555;
} }
.form-field input, .form-field input,
@@ -396,7 +396,6 @@ export default {
.checkbox-text { .checkbox-text {
font-size: 0.9rem; font-size: 0.9rem;
color: #555;
} }
.field-description { .field-description {

View File

@@ -373,7 +373,6 @@ export default {
.checkbox-text { .checkbox-text {
font-size: 0.9rem; font-size: 0.9rem;
color: #555;
} }
/* Context field styling */ /* Context field styling */

View File

@@ -174,10 +174,6 @@ export default {
.language-selector { .language-selector {
padding: 8px 12px; padding: 8px 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.2);
color: var(--sidebar-color);
font-size: 0.9rem; font-size: 0.9rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;

View File

@@ -8,7 +8,7 @@
</div> </div>
<!-- Empty state --> <!-- Empty state -->
<div v-if="normalMessages.length === 0 && !stickyAiMessage" class="empty-state"> <div v-if="normalMessages.length === 0" class="empty-state">
<div class="empty-icon">💬</div> <div class="empty-icon">💬</div>
<div class="empty-text">Nog geen berichten</div> <div class="empty-text">Nog geen berichten</div>
<div class="empty-subtext">Start een gesprek door een bericht te typen!</div> <div class="empty-subtext">Start een gesprek door een bericht te typen!</div>
@@ -35,18 +35,6 @@
<typing-indicator v-if="isTyping"></typing-indicator> <typing-indicator v-if="isTyping"></typing-indicator>
</div> </div>
<!-- Sticky bottom area for temporarily positioned AI messages -->
<div v-if="stickyAiMessage" class="sticky-ai-area">
<chat-message
:message="stickyAiMessage"
:is-submitting-form="isSubmittingForm"
:api-prefix="apiPrefix"
:is-latest-ai-message="isLatestAiMessage(stickyAiMessage)"
:is-in-sticky-area="true" @image-loaded="handleImageLoaded"
@specialist-complete="$emit('specialist-complete', $event)"
@specialist-error="$emit('specialist-error', $event)"
></chat-message>
</div>
</div> </div>
</template> </template>
@@ -104,10 +92,6 @@ export default {
normalMessages() { normalMessages() {
return this.messages.filter(msg => !msg.isTemporarilyAtBottom); return this.messages.filter(msg => !msg.isTemporarilyAtBottom);
}, },
// AI message that should be shown in sticky bottom area
stickyAiMessage() {
return this.messages.find(msg => msg.isTemporarilyAtBottom);
}
}, },
watch: { watch: {
messages: { messages: {
@@ -291,17 +275,6 @@ export default {
scroll-behavior: smooth; scroll-behavior: smooth;
} }
/* Sticky bottom area for temporarily positioned AI messages */
.sticky-ai-area {
flex-shrink: 0;
max-height: 30%; /* Takes max 30% of available space */
border-top: 1px solid #e0e0e0;
background-color: var(--active-background-color);
color: var(--active-text-color);
padding: 10px;
overflow-y: auto;
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
}
.load-more-indicator { .load-more-indicator {
text-align: center; text-align: center;

View File

@@ -3,7 +3,8 @@
<div v-if="shouldShowFullTracker" class="progress-tracker" :class="{ <div v-if="shouldShowFullTracker" class="progress-tracker" :class="{
'expanded': isExpanded, 'expanded': isExpanded,
'completed': isCompleted && !hasError, 'completed': isCompleted && !hasError,
'error': error || hasError 'error': error || hasError,
'input-area': isInInputArea
}"> }">
<div <div
class="progress-header" class="progress-header"
@@ -106,6 +107,10 @@ export default {
isLatestAiMessage: { isLatestAiMessage: {
type: Boolean, type: Boolean,
default: false default: false
},
isInInputArea: {
type: Boolean,
default: false
} }
}, },
emits: ['specialist-complete', 'specialist-error'], emits: ['specialist-complete', 'specialist-error'],
@@ -590,8 +595,8 @@ export default {
} }
.working-animation-only { .working-animation-only {
width: 24px; width: 50px;
height: 24px; height: 50px;
/* Evie working animatie styling */ /* Evie working animatie styling */
} }
@@ -604,4 +609,11 @@ export default {
font-size: 13px; font-size: 13px;
border: 1px solid #f44336; border: 1px solid #f44336;
} }
/* Input area specific styling */
.progress-tracker.input-area .progress-title {
justify-content: flex-start;
}
</style> </style>

View File

@@ -49,12 +49,11 @@ const handleImageError = () => {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 20px 15px; padding: 20px 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
} }
.logo-image { .logo-image {
max-width: 100%; max-width: 100%;
max-height: 60px; max-height: 100%;
object-fit: contain; object-fit: contain;
} }

View File

@@ -23,7 +23,6 @@ const props = defineProps({
.sidebar-make-name { .sidebar-make-name {
padding: 15px; padding: 15px;
text-align: center; text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
} }
.make-name-text { .make-name-text {

231
test_sse_stream_fix.py Normal file
View File

@@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""
Test script to verify the SSE stream fix for ProgressTracker.
This script tests that:
1. apiPrefix prop flows correctly from ChatApp → ChatInput → ChatMessage → ProgressTracker
2. The SSE URL is constructed correctly with the apiPrefix
3. Both MessageHistory sticky area and ChatInput active area work properly
4. No 404 errors occur when accessing task progress endpoints
"""
import os
import sys
import time
from pathlib import Path
def test_api_prefix_prop_flow():
"""Test that apiPrefix prop flows correctly through component hierarchy"""
print("🔍 Testing apiPrefix prop flow...")
# Test ChatApp.vue passes apiPrefix to ChatInput
chatapp_path = Path("/Volumes/OWC4M2_1/Development/Josako/EveAI/TBD/eveai_chat_client/static/assets/vue-components/ChatApp.vue")
with open(chatapp_path, 'r', encoding='utf-8') as f:
chatapp_content = f.read()
if ':api-prefix="apiPrefix"' in chatapp_content:
print("✅ ChatApp passes apiPrefix to ChatInput")
else:
print("❌ ChatApp does not pass apiPrefix to ChatInput")
return False
# Test ChatInput.vue receives and passes apiPrefix to ChatMessage
chatinput_path = Path("/Volumes/OWC4M2_1/Development/Josako/EveAI/TBD/eveai_chat_client/static/assets/vue-components/ChatInput.vue")
with open(chatinput_path, 'r', encoding='utf-8') as f:
chatinput_content = f.read()
# Check ChatInput has apiPrefix prop
if 'apiPrefix: {' in chatinput_content:
print("✅ ChatInput defines apiPrefix prop")
else:
print("❌ ChatInput missing apiPrefix prop")
return False
# Check ChatInput passes apiPrefix to ChatMessage
if ':api-prefix="apiPrefix"' in chatinput_content:
print("✅ ChatInput passes apiPrefix to ChatMessage")
else:
print("❌ ChatInput does not pass apiPrefix to ChatMessage")
return False
return True
def test_progress_tracker_sse_url_construction():
"""Test that ProgressTracker constructs SSE URLs correctly"""
print("\n🔍 Testing ProgressTracker SSE URL construction...")
progresstracker_path = Path("/Volumes/OWC4M2_1/Development/Josako/EveAI/TBD/eveai_chat_client/static/assets/vue-components/ProgressTracker.vue")
with open(progresstracker_path, 'r', encoding='utf-8') as f:
content = f.read()
# Check that ProgressTracker uses apiPrefix in URL construction
if 'const sseUrl = `${baseUrl}${this.apiPrefix}/api/task_progress/${this.taskId}`;' in content:
print("✅ ProgressTracker constructs SSE URL with apiPrefix")
else:
print("❌ ProgressTracker does not use apiPrefix in URL construction")
return False
# Check that ProgressTracker has apiPrefix prop
if 'apiPrefix: {' in content:
print("✅ ProgressTracker defines apiPrefix prop")
else:
print("❌ ProgressTracker missing apiPrefix prop")
return False
return True
def test_chatmessage_prop_passing():
"""Test that ChatMessage passes apiPrefix to ProgressTracker"""
print("\n🔍 Testing ChatMessage prop passing...")
chatmessage_path = Path("/Volumes/OWC4M2_1/Development/Josako/EveAI/TBD/eveai_chat_client/static/assets/vue-components/ChatMessage.vue")
with open(chatmessage_path, 'r', encoding='utf-8') as f:
content = f.read()
# Check that ChatMessage has apiPrefix prop
if 'apiPrefix: {' in content:
print("✅ ChatMessage defines apiPrefix prop")
else:
print("❌ ChatMessage missing apiPrefix prop")
return False
# Check that ChatMessage passes apiPrefix to ProgressTracker
if ':api-prefix="apiPrefix"' in content:
print("✅ ChatMessage passes apiPrefix to ProgressTracker")
else:
print("❌ ChatMessage does not pass apiPrefix to ProgressTracker")
return False
return True
def test_backwards_compatibility():
"""Test that MessageHistory sticky area still works"""
print("\n🔍 Testing backwards compatibility...")
messagehistory_path = Path("/Volumes/OWC4M2_1/Development/Josako/EveAI/TBD/eveai_chat_client/static/assets/vue-components/MessageHistory.vue")
with open(messagehistory_path, 'r', encoding='utf-8') as f:
content = f.read()
# Check that MessageHistory still has sticky area
if 'sticky-ai-area' in content:
print("✅ MessageHistory sticky area preserved")
else:
print("❌ MessageHistory sticky area removed")
return False
# Check that MessageHistory passes apiPrefix to ChatMessage
if ':api-prefix="apiPrefix"' in content:
print("✅ MessageHistory passes apiPrefix to ChatMessage")
else:
print("❌ MessageHistory does not pass apiPrefix to ChatMessage")
return False
return True
def test_expected_url_format():
"""Test that the expected URL format will be generated"""
print("\n🔍 Testing expected URL format...")
# Based on the error message, we expect URLs like:
# http://macstudio.ask-eve-ai-local.com/chat-client/chat/api/task_progress/616b3e7c-da47-4255-a900-68f9eb68e94f
# The apiPrefix should be '/chat-client/chat' based on chat.html template
chat_template_path = Path("/Volumes/OWC4M2_1/Development/Josako/EveAI/TBD/eveai_chat_client/templates/chat.html")
with open(chat_template_path, 'r', encoding='utf-8') as f:
content = f.read()
# Check that apiPrefix is set correctly in chat.html
if 'apiPrefix: \'{{ request.headers.get("X-Forwarded-Prefix", "") }}/chat\'' in content:
print("✅ apiPrefix correctly configured in chat.html template")
print(" Expected format: /chat-client/chat/api/task_progress/{task_id}")
else:
print("❌ apiPrefix not correctly configured in chat.html template")
return False
return True
def test_component_integration():
"""Test that all components are properly integrated"""
print("\n🔍 Testing component integration...")
# Test that ChatInput imports ChatMessage
chatinput_path = Path("/Volumes/OWC4M2_1/Development/Josako/EveAI/TBD/eveai_chat_client/static/assets/vue-components/ChatInput.vue")
with open(chatinput_path, 'r', encoding='utf-8') as f:
content = f.read()
if "import ChatMessage from './ChatMessage.vue';" in content:
print("✅ ChatInput imports ChatMessage")
else:
print("❌ ChatInput does not import ChatMessage")
return False
if "'chat-message': ChatMessage" in content:
print("✅ ChatInput registers ChatMessage component")
else:
print("❌ ChatInput does not register ChatMessage component")
return False
return True
def main():
"""Run all tests"""
print("🚀 Starting SSE Stream Fix Tests")
print("=" * 60)
tests = [
("API Prefix Prop Flow", test_api_prefix_prop_flow),
("ProgressTracker SSE URL Construction", test_progress_tracker_sse_url_construction),
("ChatMessage Prop Passing", test_chatmessage_prop_passing),
("Backwards Compatibility", test_backwards_compatibility),
("Expected URL Format", test_expected_url_format),
("Component Integration", test_component_integration)
]
passed = 0
total = len(tests)
for test_name, test_func in tests:
try:
if test_func():
passed += 1
print(f"{test_name} - PASSED")
else:
print(f"{test_name} - FAILED")
except Exception as e:
print(f"{test_name} - ERROR: {e}")
print("\n" + "=" * 60)
print(f"📊 Test Results: {passed}/{total} tests passed")
if passed == total:
print("🎉 All tests passed! SSE stream fix is working correctly.")
print("\n📋 Fix Summary:")
print("• apiPrefix now flows correctly: ChatApp → ChatInput → ChatMessage → ProgressTracker")
print("• ProgressTracker will construct correct SSE URLs with /chat-client/chat prefix")
print("• Both MessageHistory sticky area and ChatInput active area work properly")
print("• No more 404 errors expected for task progress endpoints")
print("\n🔧 Changes Made:")
print("• Added :api-prefix=\"apiPrefix\" prop to ChatInput in ChatApp.vue")
print("• ChatInput already had correct apiPrefix prop definition and passing")
print("• All components in the chain now receive the apiPrefix correctly")
return True
else:
print("⚠️ Some tests failed. Please review the implementation.")
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)