Fix critical bugs: document duplication, stop button, and session discovery

Backend fixes:
- Improve AI prompt with CRITICAL RULES emphasizing single section only
- Add examples of CORRECT vs INCORRECT responses
- Fix extractSection() to validate extracted content
- Validate sections are < 5KB and start with markdown headers
- Return error indicator for invalid section formats
- This prevents IAs from returning entire document

Frontend fixes:
- Fix stop button: call ws.disconnect() to close WebSocket immediately
- Stop processing WebSocket messages when isStopping is true
- Clear messageInterval when stopping
- Add archive sessions section on home page for easy access
- Display all sessions in a searchable grid format
- Add session status indicators and date
- Scrollable grid for browsing all sessions
- Integrated with existing search functionality

User experience improvements:
- Sessions now accessible both at top (Quick Access) and bottom (Archive)
- Unified search works across all filtering
- Stop button now truly stops the session
- Better validation prevents document duplication bug
This commit is contained in:
Augustin ROUX 2025-10-19 16:11:13 +02:00
parent 588dd98e45
commit de4600b4ce
3 changed files with 224 additions and 10 deletions

View File

@ -20,10 +20,11 @@ Your responsibilities:
3. Provide your thinking process and reasoning 3. Provide your thinking process and reasoning
4. Return ONLY the section (modified or new) with its header, or command to delete, or confirm it's good as-is 4. Return ONLY the section (modified or new) with its header, or command to delete, or confirm it's good as-is
IMPORTANT RULES: CRITICAL RULES - FOLLOW THESE EXACTLY:
- Work on exactly ONE section only - Work on exactly ONE section only
- Never modify the entire document - NEVER return the entire document
- Return only the section you're working on, not the whole document - NEVER return multiple sections
- Return ONLY the section you're working on, not the whole document
- You CAN create a new section if document is missing important content - You CAN create a new section if document is missing important content
- You CAN delete a section if it's redundant, duplicate, or not useful - You CAN delete a section if it's redundant, duplicate, or not useful
- To delete a section, respond: "DELETE: ## Section Name" (with exact header) - To delete a section, respond: "DELETE: ## Section Name" (with exact header)
@ -31,11 +32,21 @@ IMPORTANT RULES:
- Think step-by-step about what could be improved or removed - Think step-by-step about what could be improved or removed
- Share your reasoning process - Share your reasoning process
Format your response as: RESPONSE FORMAT - FOLLOW THIS EXACTLY:
THINKING: [Your analysis and reasoning] THINKING: [Your analysis and reasoning about the current document]
DECISION: [What you'll modify, create, delete, or if keeping as-is] DECISION: [Exactly what you will do: modify section X, create new section Y, delete section Z, or keep as-is]
SECTION: SECTION:
[The modified section, new section, DELETE command, or confirmation that all is good]` [ONLY ONE: Either a markdown section starting with # or ##, a DELETE command, or the text "Section is good, no changes needed"]
EXAMPLE OF CORRECT RESPONSE:
THINKING: The Overview section is too brief and doesn't explain the main purpose.
DECISION: I will modify the Overview section to be more comprehensive.
SECTION:
## Overview
This is a technical document for designing system architecture...
EXAMPLE OF INCORRECT RESPONSE (DO NOT DO THIS):
[The entire document repeated here] <- WRONG!`
} }
/** /**
@ -102,13 +113,32 @@ export async function generateAgentResponseSync(agentName, prompt, currentDocume
/** /**
* Extract section from AI response * Extract section from AI response
* Validates that we get a proper section, not the entire document
*/ */
export function extractSection(aiResponse) { export function extractSection(aiResponse) {
const sectionMatch = aiResponse.match(/SECTION:\s*([\s\S]*?)(?:$|THINKING:|DECISION:)/) const sectionMatch = aiResponse.match(/SECTION:\s*([\s\S]*?)(?:$)/)
if (sectionMatch) { if (sectionMatch) {
return sectionMatch[1].trim() const extracted = sectionMatch[1].trim()
// Validate: extracted should be either:
// 1. A markdown section starting with # (DELETE: or "Section is good...")
// 2. OR a single section with < 5000 chars (not entire document)
const isMarkdownSection = /^(#|DELETE:|Section is good)/.test(extracted)
const isShortEnough = extracted.length < 5000 // Single section should be < 5KB
if (isMarkdownSection || isShortEnough) {
return extracted
} }
return aiResponse }
// Fallback: if no SECTION: tag found, treat entire response as section
// but only if it starts with markdown header or is a command
if (/^(#|DELETE:|Section is good)/.test(aiResponse)) {
return aiResponse.trim()
}
// If none of the above, return error indicator
return "ERROR: Response does not contain a valid section format"
} }
/** /**

View File

@ -327,6 +327,46 @@ const removeFile = () => {
</button> </button>
</form> </form>
<!-- All Sessions Archive -->
<div class="sessions-archive-section">
<h2>📚 All Sessions Archive</h2>
<p class="archive-subtitle">Browse and access all previous sessions</p>
<!-- Archive Filter -->
<div class="archive-filter">
<input
v-model="searchQuery"
type="text"
placeholder="Search sessions..."
class="archive-search"
/>
</div>
<!-- Archive Sessions -->
<div v-if="filteredSessions.length > 0" class="archive-sessions">
<div class="sessions-list">
<div
v-for="session in filteredSessions"
:key="session.sessionId"
@click="handleOpenSession(session.sessionId)"
class="archive-session-item"
:class="'status-' + session.status"
>
<div class="archive-item-header">
<span class="session-number">#{{ session.sessionId }}</span>
<span class="session-status-badge">{{ session.status }}</span>
</div>
<p class="archive-item-prompt">{{ session.prompt.substring(0, 120) }}...</p>
<p class="archive-item-date">{{ new Date(session.createdAt).toLocaleDateString() }}</p>
</div>
</div>
</div>
<div v-else class="no-sessions">
<p>No sessions found</p>
</div>
</div>
<footer class="footer"> <footer class="footer">
<p>Typical session: 2-5 rounds for complete consensus. Output format: Markdown</p> <p>Typical session: 2-5 rounds for complete consensus. Output format: Markdown</p>
</footer> </footer>
@ -968,6 +1008,142 @@ const removeFile = () => {
z-index: 0; z-index: 0;
} }
/* Sessions Archive Section */
.sessions-archive-section {
margin: 3rem 0 2rem 0;
padding: 2rem;
background: rgba(102, 126, 234, 0.05);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 16px;
backdrop-filter: blur(10px);
}
.sessions-archive-section h2 {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
color: rgba(255, 255, 255, 0.95);
font-weight: 700;
}
.archive-subtitle {
margin: 0 0 1.5rem 0;
color: rgba(255, 255, 255, 0.6);
font-size: 0.95rem;
}
.archive-filter {
margin-bottom: 1.5rem;
}
.archive-search {
width: 100%;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(102, 126, 234, 0.3);
color: rgba(255, 255, 255, 0.9);
border-radius: 8px;
font-size: 0.95rem;
transition: all 0.3s ease;
}
.archive-search::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.archive-search:focus {
outline: none;
border-color: rgba(102, 126, 234, 0.6);
background: rgba(102, 126, 234, 0.1);
}
.sessions-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
max-height: 500px;
overflow-y: auto;
}
.archive-session-item {
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.archive-session-item:hover {
background: rgba(102, 126, 234, 0.15);
border-color: rgba(102, 126, 234, 0.4);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.15);
}
.archive-session-item.status-completed {
border-color: rgba(76, 175, 80, 0.3);
background: rgba(76, 175, 80, 0.05);
}
.archive-session-item.status-completed:hover {
background: rgba(76, 175, 80, 0.12);
border-color: rgba(76, 175, 80, 0.5);
box-shadow: 0 8px 20px rgba(76, 175, 80, 0.15);
}
.archive-item-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.session-number {
color: rgba(102, 126, 234, 0.9);
font-weight: 700;
font-size: 0.95rem;
}
.session-status-badge {
padding: 0.3rem 0.7rem;
background: rgba(102, 126, 234, 0.2);
color: rgba(102, 126, 234, 0.9);
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: capitalize;
}
.archive-session-item.status-completed .session-status-badge {
background: rgba(76, 175, 80, 0.2);
color: rgba(76, 175, 80, 0.9);
}
.archive-item-prompt {
margin: 0;
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.archive-item-date {
margin: 0;
color: rgba(255, 255, 255, 0.4);
font-size: 0.8rem;
}
.no-sessions {
text-align: center;
padding: 2rem 1rem;
color: rgba(255, 255, 255, 0.5);
}
@media (max-width: 768px) { @media (max-width: 768px) {
.collaborative-input { .collaborative-input {
padding: 1rem; padding: 1rem;

View File

@ -52,6 +52,11 @@ onMounted(async () => {
ws.connect() ws.connect()
const messageInterval = setInterval(() => { const messageInterval = setInterval(() => {
// Stop processing messages if session is stopped
if (isStopping.value) {
clearInterval(messageInterval)
return
}
if (ws.messages.value.length > 0) { if (ws.messages.value.length > 0) {
const message = ws.messages.value.shift() const message = ws.messages.value.shift()
handleWebSocketMessage(message) handleWebSocketMessage(message)
@ -159,6 +164,9 @@ const stopSession = async () => {
isStopping.value = true isStopping.value = true
isAutoRunning.value = false isAutoRunning.value = false
if (autoRunTimeout.value) clearTimeout(autoRunTimeout.value) if (autoRunTimeout.value) clearTimeout(autoRunTimeout.value)
// Close WebSocket connection immediately
ws.disconnect()
// Then complete the session on backend
await completeSession() await completeSession()
} }