feat(aia): Protocol Agent streaming + editable state panel + protocol generation plan

Day 2 Development (2026-01-24):

Backend Enhancements:
- Implement SSE streaming in ProtocolAgentController using createStreamingService
- Add data condensation via LLM in ProtocolOrchestrator.handleProtocolSync
- Support stage editing without resetting progress
- Add explicit JSON output format for each stage in system prompt
- Create independent seed script for Protocol Agent (seed-protocol-agent.ts)

Frontend Improvements:
- Integrate useAIStream hook for typewriter effect in ChatArea
- Add MarkdownContent component for basic Markdown rendering
- Implement StageEditModal for editing stage data (scientific question, PICO, etc.)
- Add edit button to StageCard (visible on hover)
- Fix routing paths from /aia to /ai-qa
- Enhance CSS with full-screen layout and Markdown styles

New Documentation:
- One-click protocol generation development plan (v1.1)
- Editor selection evaluation (Novel vs BlockNote vs Tiptap)
- Novel fork strategy for AI-native editing

Technical Decisions:
- Choose Novel (Fork) as protocol editor for AI-first design
- Two-stage progressive generation: summary in chat, full protocol in editor
- 10-day development plan for protocol generation feature

Code Stats:
- Backend: 3 files modified, 1 new file
- Frontend: 9 files modified, 2 new files
- Docs: 3 new files

Status: Streaming and editable features working, protocol generation pending
This commit is contained in:
2026-01-24 23:06:33 +08:00
parent 596f2dfc02
commit 4d7d97ca19
18 changed files with 2708 additions and 192 deletions

View File

@@ -6,11 +6,16 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
/* ============================================ */
/* 全局样式 */
/* 全局样式 - 全屏覆盖与ChatWorkspace一致 */
/* ============================================ */
.protocol-agent-page {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
display: flex;
height: 100vh;
overflow: hidden;
font-family: 'Inter', 'Noto Sans SC', sans-serif;
background: #FFFFFF;
@@ -445,8 +450,252 @@
padding: 16px;
border-radius: 12px;
font-size: 14px;
line-height: 1.6;
line-height: 1.8;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
white-space: pre-wrap;
word-break: break-word;
}
/* Markdown 格式支持 */
.chat-bubble h1,
.chat-bubble h2,
.chat-bubble h3 {
margin: 16px 0 8px 0;
font-weight: 700;
color: #111827;
}
.chat-bubble h1 { font-size: 1.25em; }
.chat-bubble h2 { font-size: 1.15em; }
.chat-bubble h3 { font-size: 1.05em; }
.chat-bubble p {
margin: 8px 0;
}
.chat-bubble ul,
.chat-bubble ol {
margin: 8px 0;
padding-left: 24px;
}
.chat-bubble li {
margin: 4px 0;
}
.chat-bubble strong,
.chat-bubble b {
font-weight: 700;
color: #1F2937;
}
.chat-bubble em,
.chat-bubble i {
font-style: italic;
}
.chat-bubble code {
background: #F3F4F6;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Consolas', monospace;
font-size: 0.9em;
color: #6366F1;
}
.chat-bubble pre {
background: #1F2937;
color: #F9FAFB;
padding: 12px 16px;
border-radius: 8px;
overflow-x: auto;
margin: 12px 0;
}
.chat-bubble pre code {
background: transparent;
padding: 0;
color: inherit;
}
.chat-bubble blockquote {
border-left: 4px solid #6366F1;
margin: 12px 0;
padding: 8px 16px;
background: #F9FAFB;
color: #4B5563;
}
/* MarkdownContent 组件样式 */
.markdown-content {
white-space: normal;
}
.markdown-content p {
margin: 0 0 12px 0;
}
.markdown-content p:last-child {
margin-bottom: 0;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3 {
margin: 16px 0 8px 0;
font-weight: 700;
line-height: 1.4;
}
.markdown-content h1:first-child,
.markdown-content h2:first-child,
.markdown-content h3:first-child {
margin-top: 0;
}
.markdown-content ul,
.markdown-content ol {
margin: 8px 0 12px 0;
padding-left: 20px;
}
.markdown-content li {
margin: 6px 0;
line-height: 1.6;
}
.markdown-content li::marker {
color: #6366F1;
}
.markdown-content strong {
font-weight: 700;
color: #1F2937;
}
.markdown-content em {
font-style: italic;
color: #4B5563;
}
.markdown-content code {
background: rgba(99, 102, 241, 0.1);
color: #6366F1;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 0.9em;
}
/* ============================================ */
/* 阶段标签 */
/* ============================================ */
.message-meta .stage-tag {
display: inline-block;
background: linear-gradient(135deg, #6366F1, #8B5CF6);
color: white;
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
margin-left: 8px;
font-weight: 500;
}
.message-meta .timestamp {
color: #9CA3AF;
margin-left: 4px;
}
/* ============================================ */
/* 动作卡片 */
/* ============================================ */
.action-cards {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 12px;
}
.action-card {
display: flex;
flex-direction: column;
padding: 14px 18px;
background: linear-gradient(135deg, #F8FAFC, #F1F5F9);
border: 1px solid #E2E8F0;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
min-width: 200px;
max-width: 300px;
}
.action-card:hover {
background: linear-gradient(135deg, #EEF2FF, #E0E7FF);
border-color: #6366F1;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15);
}
.action-card-title {
font-size: 14px;
font-weight: 600;
color: #1F2937;
margin-bottom: 4px;
}
.action-card-desc {
font-size: 12px;
color: #6B7280;
line-height: 1.4;
}
.action-card-icon {
position: absolute;
top: 14px;
right: 14px;
color: #9CA3AF;
transition: color 0.2s;
}
.action-card:hover .action-card-icon {
color: #6366F1;
}
/* ============================================ */
/* 加载状态 */
/* ============================================ */
.chat-bubble.loading {
display: flex;
align-items: center;
gap: 10px;
color: #6B7280;
}
.spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 流式输出光标 */
.streaming-cursor {
display: inline-block;
animation: blink 0.8s step-end infinite;
color: #6366F1;
margin-left: 2px;
font-weight: 400;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.assistant-bubble {
@@ -1190,6 +1439,201 @@
background: #94A3B8;
}
/* ============================================ */
/* 阶段编辑弹窗 */
/* ============================================ */
.stage-edit-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
}
.stage-edit-modal {
background: white;
border-radius: 12px;
width: 480px;
max-width: 90%;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.stage-edit-modal .modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #E5E7EB;
background: #F9FAFB;
}
.stage-edit-modal .modal-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #111827;
}
.stage-edit-modal .close-btn {
padding: 4px;
background: transparent;
border: none;
border-radius: 6px;
cursor: pointer;
color: #6B7280;
transition: all 0.2s;
}
.stage-edit-modal .close-btn:hover {
background: #E5E7EB;
color: #111827;
}
.stage-edit-modal .modal-body {
padding: 20px;
overflow-y: auto;
max-height: 50vh;
}
.stage-edit-modal .form-group {
margin-bottom: 16px;
}
.stage-edit-modal .form-group label {
display: block;
font-size: 13px;
font-weight: 500;
color: #374151;
margin-bottom: 6px;
}
.stage-edit-modal .form-group input,
.stage-edit-modal .form-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #D1D5DB;
border-radius: 8px;
font-size: 14px;
color: #111827;
transition: border-color 0.2s, box-shadow 0.2s;
}
.stage-edit-modal .form-group input:focus,
.stage-edit-modal .form-group textarea:focus {
outline: none;
border-color: #3B82F6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.stage-edit-modal .form-group textarea {
resize: vertical;
min-height: 80px;
}
.stage-edit-modal .form-row {
display: flex;
gap: 12px;
}
.stage-edit-modal .form-group.half {
flex: 1;
}
.stage-edit-modal .modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid #E5E7EB;
background: #F9FAFB;
}
.stage-edit-modal .cancel-btn {
padding: 8px 16px;
background: white;
border: 1px solid #D1D5DB;
border-radius: 8px;
font-size: 14px;
color: #374151;
cursor: pointer;
transition: all 0.2s;
}
.stage-edit-modal .cancel-btn:hover {
background: #F3F4F6;
}
.stage-edit-modal .save-btn {
padding: 8px 16px;
background: #3B82F6;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
color: white;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.stage-edit-modal .save-btn:hover:not(:disabled) {
background: #2563EB;
}
.stage-edit-modal .save-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 编辑按钮样式 */
.stage-header .stage-actions {
display: flex;
align-items: center;
gap: 8px;
}
.stage-header .edit-btn {
padding: 4px;
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
color: #9CA3AF;
transition: all 0.2s;
opacity: 0;
}
.stage-card:hover .edit-btn {
opacity: 1;
}
.stage-header .edit-btn:hover {
background: #E5E7EB;
color: #3B82F6;
}
/* 流式输出光标 */
.streaming-cursor {
display: inline-block;
animation: blink 1s step-end infinite;
color: #3B82F6;
font-weight: bold;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
/* ============================================ */
/* 响应式 */
/* ============================================ */