docs: add Search & Recall + Evidence implementation plan
10 tasks: S3 storage service, upload endpoints, RichTextInput with clipboard paste, FlowPilot integration, export evidence, structured filters, FTS, Command Palette search, semantic similar matching, similar sessions UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
589
docs/plans/2026-03-20-search-recall-evidence-impl.md
Normal file
589
docs/plans/2026-03-20-search-recall-evidence-impl.md
Normal file
@@ -0,0 +1,589 @@
|
||||
# Search & Recall + Evidence-Rich Sessions — Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Add file upload with clipboard paste for evidence capture during FlowPilot sessions, and build three layers of session search (structured filters, full-text, semantic similarity).
|
||||
|
||||
**Architecture:** Evidence uses Railway Object Storage (S3-compatible) via boto3, with a `file_uploads` table tracking metadata. Search uses PostgreSQL generated tsvector columns with GIN indexes for full-text, and Voyage AI embeddings with pgvector for semantic similarity (reusing existing RAG infrastructure). A `RichTextInput` component handles clipboard paste-to-upload UX.
|
||||
|
||||
**Tech Stack:** FastAPI, SQLAlchemy 2.0 (async), boto3 (S3), pgvector, Voyage AI embeddings, React 19, TypeScript, Tailwind CSS v4
|
||||
|
||||
**Design doc:** `docs/plans/2026-03-20-search-recall-evidence-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Part A: Evidence-Rich Sessions
|
||||
|
||||
### Task 1: S3 storage service + file_uploads model
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/services/storage_service.py`
|
||||
- Create: `backend/app/models/file_upload.py`
|
||||
- Modify: `backend/app/models/__init__.py`
|
||||
- Modify: `backend/app/core/config.py`
|
||||
- Create: migration
|
||||
|
||||
**Step 1: Add storage config**
|
||||
|
||||
In `backend/app/core/config.py`, add to the Settings class:
|
||||
|
||||
```python
|
||||
# Object Storage (Railway S3-compatible)
|
||||
STORAGE_ENDPOINT: str | None = None
|
||||
STORAGE_ACCESS_KEY: str | None = None
|
||||
STORAGE_SECRET_KEY: str | None = None
|
||||
STORAGE_BUCKET_NAME: str = "resolutionflow-uploads"
|
||||
STORAGE_REGION: str = "us-east-1"
|
||||
```
|
||||
|
||||
**Step 2: Create file_uploads model**
|
||||
|
||||
`backend/app/models/file_upload.py`:
|
||||
|
||||
```python
|
||||
"""File upload metadata — tracks files stored in S3-compatible object storage."""
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import String, Integer, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class FileUpload(Base):
|
||||
__tablename__ = "file_uploads"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
account_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
uploaded_by: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=False
|
||||
)
|
||||
session_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("ai_sessions.id", ondelete="SET NULL"), nullable=True, index=True
|
||||
)
|
||||
filename: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
content_type: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
size_bytes: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
storage_key: Mapped[str] = mapped_column(String(500), nullable=False, unique=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
```
|
||||
|
||||
Register in `backend/app/models/__init__.py`.
|
||||
|
||||
**Step 3: Create storage service**
|
||||
|
||||
`backend/app/services/storage_service.py`:
|
||||
|
||||
```python
|
||||
"""S3-compatible object storage service for file uploads."""
|
||||
import logging
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
|
||||
import boto3
|
||||
from botocore.config import Config as BotoConfig
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ALLOWED_IMAGE_TYPES = {"image/png", "image/jpeg", "image/gif", "image/webp"}
|
||||
ALLOWED_TEXT_TYPES = {"text/plain", "text/csv", "application/octet-stream"}
|
||||
ALLOWED_TYPES = ALLOWED_IMAGE_TYPES | ALLOWED_TEXT_TYPES
|
||||
|
||||
MAX_IMAGE_SIZE = 5 * 1024 * 1024 # 5MB
|
||||
MAX_TEXT_SIZE = 1 * 1024 * 1024 # 1MB
|
||||
MAX_FILES_PER_SESSION = 20
|
||||
MAX_BYTES_PER_SESSION = 50 * 1024 * 1024 # 50MB
|
||||
|
||||
PRESIGNED_URL_EXPIRY = 3600 # 1 hour
|
||||
|
||||
|
||||
def _get_client():
|
||||
"""Get S3 client configured for Railway Object Storage."""
|
||||
if not settings.STORAGE_ENDPOINT:
|
||||
raise RuntimeError("Object storage not configured (STORAGE_ENDPOINT missing)")
|
||||
return boto3.client(
|
||||
"s3",
|
||||
endpoint_url=settings.STORAGE_ENDPOINT,
|
||||
aws_access_key_id=settings.STORAGE_ACCESS_KEY,
|
||||
aws_secret_access_key=settings.STORAGE_SECRET_KEY,
|
||||
region_name=settings.STORAGE_REGION,
|
||||
config=BotoConfig(signature_version="s3v4"),
|
||||
)
|
||||
|
||||
|
||||
def validate_upload(content_type: str, size_bytes: int) -> str | None:
|
||||
"""Validate file type and size. Returns error message or None."""
|
||||
if content_type not in ALLOWED_TYPES:
|
||||
return f"File type {content_type} not allowed"
|
||||
max_size = MAX_IMAGE_SIZE if content_type in ALLOWED_IMAGE_TYPES else MAX_TEXT_SIZE
|
||||
if size_bytes > max_size:
|
||||
return f"File too large ({size_bytes} bytes, max {max_size})"
|
||||
return None
|
||||
|
||||
|
||||
async def upload_file(
|
||||
file_data: bytes,
|
||||
filename: str,
|
||||
content_type: str,
|
||||
account_id: str,
|
||||
) -> str:
|
||||
"""Upload file to S3, returns the storage key."""
|
||||
ext = filename.rsplit(".", 1)[-1] if "." in filename else "bin"
|
||||
storage_key = f"uploads/{account_id}/{uuid.uuid4()}.{ext}"
|
||||
|
||||
client = _get_client()
|
||||
client.upload_fileobj(
|
||||
BytesIO(file_data),
|
||||
settings.STORAGE_BUCKET_NAME,
|
||||
storage_key,
|
||||
ExtraArgs={"ContentType": content_type},
|
||||
)
|
||||
return storage_key
|
||||
|
||||
|
||||
def get_presigned_url(storage_key: str) -> str:
|
||||
"""Generate a time-limited presigned URL for downloading a file."""
|
||||
client = _get_client()
|
||||
return client.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": settings.STORAGE_BUCKET_NAME, "Key": storage_key},
|
||||
ExpiresIn=PRESIGNED_URL_EXPIRY,
|
||||
)
|
||||
|
||||
|
||||
async def delete_file(storage_key: str) -> None:
|
||||
"""Delete a file from S3."""
|
||||
try:
|
||||
client = _get_client()
|
||||
client.delete_object(Bucket=settings.STORAGE_BUCKET_NAME, Key=storage_key)
|
||||
except ClientError:
|
||||
logger.warning(f"Failed to delete S3 object: {storage_key}")
|
||||
```
|
||||
|
||||
**Step 4: Generate migration, add boto3 to requirements**
|
||||
|
||||
```bash
|
||||
pip install boto3
|
||||
echo "boto3>=1.34.0" >> backend/requirements.txt
|
||||
cd backend && alembic revision --autogenerate -m "add file_uploads table"
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "feat(evidence): add S3 storage service and file_uploads model"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Upload API endpoints
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/api/endpoints/uploads.py`
|
||||
- Create: `backend/app/schemas/upload.py`
|
||||
- Modify: `backend/app/api/router.py`
|
||||
- Create: `backend/tests/test_uploads.py`
|
||||
|
||||
**Schemas** (`backend/app/schemas/upload.py`):
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel
|
||||
|
||||
class FileUploadResponse(BaseModel):
|
||||
id: UUID
|
||||
filename: str
|
||||
content_type: str
|
||||
size_bytes: int
|
||||
url: str # presigned URL
|
||||
created_at: datetime
|
||||
model_config = {"from_attributes": True}
|
||||
```
|
||||
|
||||
**Endpoints** (`backend/app/api/endpoints/uploads.py`):
|
||||
|
||||
```
|
||||
POST /uploads — Multipart upload, returns FileUploadResponse
|
||||
GET /uploads/{id}/url — Presigned download URL
|
||||
GET /uploads?session_id={id} — List uploads for a session
|
||||
DELETE /uploads/{id} — Delete upload + S3 object
|
||||
```
|
||||
|
||||
Key details:
|
||||
- `POST /uploads` accepts `UploadFile` from FastAPI + `session_id` form field
|
||||
- Validates content_type and size via `storage_service.validate_upload()`
|
||||
- Checks per-session limits (count + total bytes) before uploading
|
||||
- Rate limit: `@limiter.limit("10/minute")`
|
||||
- All endpoints require auth via `get_current_active_user`
|
||||
- Delete: verify ownership (uploaded_by == current_user.id OR user is admin)
|
||||
|
||||
**Tests:** Upload happy path, type rejection, size rejection, per-session limit, presigned URL, delete.
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git commit -m "feat(evidence): add file upload/download API endpoints"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Frontend — RichTextInput component
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/components/common/RichTextInput.tsx`
|
||||
- Create: `frontend/src/api/uploads.ts`
|
||||
- Create: `frontend/src/types/upload.ts`
|
||||
|
||||
**Types** (`frontend/src/types/upload.ts`):
|
||||
|
||||
```typescript
|
||||
export interface FileUploadResponse {
|
||||
id: string
|
||||
filename: string
|
||||
content_type: string
|
||||
size_bytes: number
|
||||
url: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface PendingUpload {
|
||||
id: string // temp client ID
|
||||
file: File
|
||||
preview: string // object URL for thumbnail
|
||||
status: 'uploading' | 'done' | 'error'
|
||||
result?: FileUploadResponse
|
||||
error?: string
|
||||
}
|
||||
```
|
||||
|
||||
**API** (`frontend/src/api/uploads.ts`):
|
||||
|
||||
```typescript
|
||||
import apiClient from './client'
|
||||
import type { FileUploadResponse } from '@/types/upload'
|
||||
|
||||
export const uploadsApi = {
|
||||
async upload(file: File, sessionId?: string): Promise<FileUploadResponse> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
if (sessionId) formData.append('session_id', sessionId)
|
||||
const response = await apiClient.post<FileUploadResponse>('/uploads', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
async getUrl(id: string): Promise<string> {
|
||||
const response = await apiClient.get<{ url: string }>(`/uploads/${id}/url`)
|
||||
return response.data.url
|
||||
},
|
||||
async list(sessionId: string): Promise<FileUploadResponse[]> {
|
||||
const response = await apiClient.get<FileUploadResponse[]>('/uploads', { params: { session_id: sessionId } })
|
||||
return response.data
|
||||
},
|
||||
async remove(id: string): Promise<void> {
|
||||
await apiClient.delete(`/uploads/${id}`)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**RichTextInput component** (`frontend/src/components/common/RichTextInput.tsx`):
|
||||
|
||||
Props:
|
||||
```typescript
|
||||
interface RichTextInputProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
onFilesChange?: (uploads: FileUploadResponse[]) => void
|
||||
sessionId?: string
|
||||
placeholder?: string
|
||||
rows?: number
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- Renders a `<textarea>` with standard styling
|
||||
- Listens for `paste` event — if `clipboardData.items` contains `image/*` blob, extracts it
|
||||
- On image paste: creates a `PendingUpload`, shows thumbnail strip below textarea, calls `uploadsApi.upload()` in background
|
||||
- Thumbnail strip: horizontal row of image previews (64x64 rounded), each with:
|
||||
- Uploading: pulse animation overlay
|
||||
- Done: image thumbnail, X button to remove
|
||||
- Error: red border, retry button
|
||||
- Completed uploads reported to parent via `onFilesChange`
|
||||
- Also supports drag-and-drop (same handler)
|
||||
- Design: thumbnails in a `flex gap-2 flex-wrap` row below the textarea, `.glass-card` styling on each thumbnail
|
||||
|
||||
**Build and commit:**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build
|
||||
git commit -m "feat(evidence): add RichTextInput with clipboard paste upload"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Wire RichTextInput into FlowPilot
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/components/flowpilot/FlowPilotIntake.tsx`
|
||||
- Modify: `frontend/src/components/flowpilot/FlowPilotOptions.tsx` (free-text input)
|
||||
- Modify: `frontend/src/components/flowpilot/EscalateModal.tsx`
|
||||
|
||||
Replace plain `<textarea>` elements in these components with `<RichTextInput>`. Pass the current session ID so uploads are linked.
|
||||
|
||||
For intake: the session doesn't exist yet at intake time. Upload without session_id, then link uploads to the session after creation (via a PATCH or by passing upload IDs to the session creation endpoint).
|
||||
|
||||
Alternative simpler approach: upload to a temporary session_id (null), then after session creation, call `PATCH /uploads/{id}` to set session_id. Or include upload_ids in the session creation payload.
|
||||
|
||||
**Build and commit:**
|
||||
|
||||
```bash
|
||||
git commit -m "feat(evidence): wire clipboard paste into FlowPilot intake, free-text, and escalation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Evidence in exports
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/services/export_service.py`
|
||||
|
||||
Extend each format generator to include file upload references:
|
||||
|
||||
- Query `file_uploads` for the session
|
||||
- **Markdown:** `` for images, `[{filename}]({presigned_url})` for text files
|
||||
- **HTML/PDF:** `<img src="{presigned_url}" alt="{filename}" style="max-width: 100%">` for images
|
||||
- **PSA:** List as `Evidence: {filename} — {presigned_url}` (CW notes don't support inline images)
|
||||
- **Text:** `[Attachment: {filename}]` with URL on next line
|
||||
|
||||
Generate presigned URLs at export time via `storage_service.get_presigned_url()`.
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git commit -m "feat(evidence): include file uploads in session exports"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part B: Search & Recall
|
||||
|
||||
### Task 6: Structured filters on AI sessions
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/api/endpoints/ai_sessions.py`
|
||||
- Modify: `frontend/src/pages/SessionHistoryPage.tsx`
|
||||
|
||||
**Backend:** Extend `list_sessions` endpoint with new query params:
|
||||
|
||||
```python
|
||||
async def list_sessions(
|
||||
...,
|
||||
problem_domain: Optional[str] = Query(None),
|
||||
matched_flow_id: Optional[UUID] = Query(None),
|
||||
confidence_tier: Optional[str] = Query(None, pattern="^(guided|exploring|discovery)$"),
|
||||
ticket_id: Optional[str] = Query(None),
|
||||
date_from: Optional[datetime] = Query(None),
|
||||
date_to: Optional[datetime] = Query(None),
|
||||
q: Optional[str] = Query(None, min_length=2, max_length=200),
|
||||
):
|
||||
```
|
||||
|
||||
Add `.where()` clauses for each non-None filter.
|
||||
|
||||
**Frontend:** Add a filter bar to the AI Sessions tab on `SessionHistoryPage`:
|
||||
- Search input (for `q` param)
|
||||
- Problem domain dropdown (populated from distinct domains in sessions)
|
||||
- Confidence tier pills (All / Guided / Exploring / Discovery)
|
||||
- Date range inputs
|
||||
- Match existing Flow Sessions filter bar styling
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git commit -m "feat(search): add structured filters to AI session list"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Full-text search (PostgreSQL FTS)
|
||||
|
||||
**Files:**
|
||||
- Create: migration for tsvector column + GIN index
|
||||
- Modify: `backend/app/api/endpoints/ai_sessions.py`
|
||||
|
||||
**Migration:**
|
||||
|
||||
```python
|
||||
# Generated tsvector column on ai_sessions
|
||||
op.execute("""
|
||||
ALTER TABLE ai_sessions ADD COLUMN IF NOT EXISTS search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
to_tsvector('english',
|
||||
coalesce(intake_summary, '') || ' ' ||
|
||||
coalesce(resolution_summary, '') || ' ' ||
|
||||
coalesce(escalation_reason, '') || ' ' ||
|
||||
coalesce(problem_domain, ''))
|
||||
) STORED
|
||||
""")
|
||||
op.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_ai_sessions_search
|
||||
ON ai_sessions USING gin(search_vector)
|
||||
""")
|
||||
```
|
||||
|
||||
**Wire into list endpoint:** When `q` is provided, add:
|
||||
|
||||
```python
|
||||
from sqlalchemy import text as sa_text
|
||||
query = query.where(
|
||||
sa_text("ai_sessions.search_vector @@ plainto_tsquery('english', :q)")
|
||||
).params(q=q)
|
||||
```
|
||||
|
||||
**Extend Command Palette:** In `frontend/src/components/layout/CommandPalette.tsx`, add AI session search alongside flows. Call the AI sessions list endpoint with `q` param. Show results in a new "AI Sessions" group.
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git commit -m "feat(search): add PostgreSQL FTS on AI sessions with Command Palette integration"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Similar session matching (semantic)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/models/ai_session_embedding.py`
|
||||
- Create: migration
|
||||
- Modify: `backend/app/api/endpoints/ai_sessions.py`
|
||||
- Modify: `backend/app/services/flowpilot_engine.py`
|
||||
|
||||
**Model** (`backend/app/models/ai_session_embedding.py`):
|
||||
|
||||
Same pattern as `tree_embedding.py`:
|
||||
|
||||
```python
|
||||
class AISessionEmbedding(Base):
|
||||
__tablename__ = "ai_session_embeddings"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
session_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("ai_sessions.id", ondelete="CASCADE"),
|
||||
nullable=False, unique=True, index=True
|
||||
)
|
||||
account_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("accounts.id", ondelete="CASCADE"),
|
||||
nullable=False, index=True
|
||||
)
|
||||
chunk_text: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
embedding_model: Mapped[str] = mapped_column(String(50), nullable=False, default="voyage-3.5")
|
||||
# embedding column created in migration as vector(1024)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
```
|
||||
|
||||
Migration adds the table + `embedding vector(1024)` column via raw SQL (same as tree_embeddings migration).
|
||||
|
||||
**Generate embedding:** In `flowpilot_engine.py`, after session intake is processed, generate an embedding of the intake_summary using the existing `rag_service` / Voyage AI client. Store in `ai_session_embeddings`. Update embedding on resolution (add resolution_summary to the text).
|
||||
|
||||
**Endpoint:** `GET /ai-sessions/{id}/similar?limit=5`
|
||||
|
||||
```python
|
||||
@router.get("/{session_id}/similar")
|
||||
async def get_similar_sessions(session_id, current_user, db, limit=5):
|
||||
# Get this session's embedding
|
||||
# Cosine similarity query against all session embeddings for the account
|
||||
# Return top N with similarity score + session summary
|
||||
```
|
||||
|
||||
Use the same cosine similarity query pattern as `rag_service.search()`.
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git commit -m "feat(search): add semantic similar session matching via Voyage AI embeddings"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Similar sessions UI
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/components/flowpilot/SimilarSessions.tsx`
|
||||
- Modify: `frontend/src/components/flowpilot/FlowPilotSession.tsx`
|
||||
- Modify: `frontend/src/api/flowpilotAnalytics.ts` (or ai-sessions API)
|
||||
|
||||
**Component** — small card list showing 3-5 similar sessions:
|
||||
|
||||
```typescript
|
||||
interface SimilarSession {
|
||||
id: string
|
||||
intake_summary: string
|
||||
status: string
|
||||
resolution_summary: string | null
|
||||
problem_domain: string | null
|
||||
similarity: number
|
||||
created_at: string
|
||||
}
|
||||
```
|
||||
|
||||
Each card: `.glass-card` with:
|
||||
- Problem summary (1-2 lines, truncated)
|
||||
- Status badge (resolved/escalated)
|
||||
- Resolution summary if resolved (1 line)
|
||||
- Similarity percentage
|
||||
- Click → navigate to session detail
|
||||
|
||||
**Where it renders:**
|
||||
- FlowPilot session sidebar (desktop) or expandable section (mobile): "Similar Past Sessions" below the session info
|
||||
- Only fetched if the session has been through intake (has intake_summary)
|
||||
|
||||
**Build and commit:**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build
|
||||
git commit -m "feat(search): add similar sessions UI in FlowPilot sidebar"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Final verification and docs
|
||||
|
||||
**Step 1: Run backend tests**
|
||||
|
||||
```bash
|
||||
cd backend && python -m pytest --override-ini="addopts="
|
||||
```
|
||||
|
||||
**Step 2: Run frontend build**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
**Step 3: Update CURRENT-STATE.md**
|
||||
|
||||
Add evidence-rich sessions and search & recall to completed items.
|
||||
|
||||
**Step 4: Update stack priorities doc** — mark items 1 and 2 as complete.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "docs: update CURRENT-STATE.md — Search & Recall and Evidence-Rich Sessions complete"
|
||||
```
|
||||
Reference in New Issue
Block a user