Spaces:
Running
Running
v0.12
Browse files- REFACTORING_COMPLETE.md +184 -0
- RESEARCH_REFACTOR_PLAN.md +59 -0
- backend/__pycache__/code.cpython-312.pyc +0 -0
- backend/__pycache__/research.cpython-312.pyc +0 -0
- backend/code.py +5 -0
- backend/main.py +111 -4
- backend/research.py +196 -75
- index.html +24 -0
- research-ui.js +185 -82
- script.js +235 -25
- style.css +325 -120
REFACTORING_COMPLETE.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Research Refactoring Complete!
|
| 2 |
+
|
| 3 |
+
## Summary of Changes
|
| 4 |
+
|
| 5 |
+
All files have been completely refactored to implement the new research logic with parallel processing, interleaved results, query grouping, and statistics tracking.
|
| 6 |
+
|
| 7 |
+
## Backend Changes
|
| 8 |
+
|
| 9 |
+
### 1. main.py
|
| 10 |
+
- Added `research_parallel_workers` to `ChatRequest` model
|
| 11 |
+
- Updated `stream_research_notebook()` to accept and pass parallel_workers parameter (default: 8)
|
| 12 |
+
- Passes parallel_workers through to `stream_research()`
|
| 13 |
+
|
| 14 |
+
### 2. research.py (Complete Rewrite)
|
| 15 |
+
**New Flow:**
|
| 16 |
+
1. Generate queries (sequential)
|
| 17 |
+
2. Search ALL queries in parallel (3 workers)
|
| 18 |
+
3. **Interleave** URLs from all queries (round-robin)
|
| 19 |
+
4. **Process URLs in parallel** (configurable workers, default 8)
|
| 20 |
+
- Each worker: extract_content() + analyze_content()
|
| 21 |
+
5. Track stats per query (relevant/irrelevant/error)
|
| 22 |
+
6. Stream results grouped by query
|
| 23 |
+
7. Assess completeness
|
| 24 |
+
8. Generate final report
|
| 25 |
+
|
| 26 |
+
**New Event Types:**
|
| 27 |
+
- `source` event now includes:
|
| 28 |
+
- `query_index`: which query this belongs to
|
| 29 |
+
- `query_text`: the actual query
|
| 30 |
+
- `is_error`: boolean for failed extractions
|
| 31 |
+
- `error_message`: error details
|
| 32 |
+
|
| 33 |
+
- `query_stats` event (NEW):
|
| 34 |
+
- `query_index`: which query
|
| 35 |
+
- `relevant_count`: # of relevant sources
|
| 36 |
+
- `irrelevant_count`: # of irrelevant sources
|
| 37 |
+
- `error_count`: # of failed requests
|
| 38 |
+
|
| 39 |
+
## Frontend Changes
|
| 40 |
+
|
| 41 |
+
### 3. index.html
|
| 42 |
+
- Added "RESEARCH PARALLEL WORKERS" setting field
|
| 43 |
+
- Type: number input (1-20)
|
| 44 |
+
- Default placeholder: 8
|
| 45 |
+
|
| 46 |
+
### 4. script.js
|
| 47 |
+
- Added `researchParallelWorkers` to settings object
|
| 48 |
+
- Sends `research_parallel_workers` in API requests
|
| 49 |
+
- Updated source event handler to pass full data object
|
| 50 |
+
- Added `query_stats` event handler
|
| 51 |
+
|
| 52 |
+
### 5. research-ui.js (Complete Rewrite)
|
| 53 |
+
**New Structure:**
|
| 54 |
+
- Maintains `queryData` object: `query_index -> {query, sources[], stats{}}`
|
| 55 |
+
- Creates query groups with headers showing query text and stats
|
| 56 |
+
- Sources are grouped under their respective queries
|
| 57 |
+
- **Toggle button** to show/hide irrelevant sources
|
| 58 |
+
- Sources display status icons: ✓ (relevant), ○ (irrelevant), ✗ (error)
|
| 59 |
+
|
| 60 |
+
**Key Functions:**
|
| 61 |
+
- `createQueriesMessage()`: Creates query group structure
|
| 62 |
+
- `createSourceMessage()`: Adds source to appropriate query group
|
| 63 |
+
- `updateQueryStats()`: Updates stats display per query
|
| 64 |
+
- `renderQuerySources()`: Re-renders sources based on toggle state
|
| 65 |
+
- `toggleIrrelevantSources()`: Global toggle for showing/hiding irrelevant
|
| 66 |
+
|
| 67 |
+
### 6. style.css
|
| 68 |
+
**New Styles:**
|
| 69 |
+
- `.toggle-irrelevant-btn`: Button in research header
|
| 70 |
+
- `.query-group`: Container for each query and its sources
|
| 71 |
+
- `.query-header`: Query text + stats display
|
| 72 |
+
- `.query-stats`: Live stats (X relevant / Y not relevant / Z failed)
|
| 73 |
+
- `.query-sources`: Container for sources under query
|
| 74 |
+
- `.source-status-icon`: Icons with color coding
|
| 75 |
+
- `.research-source.error`: Red background for failed requests
|
| 76 |
+
- `.no-sources`: Message when no sources to display
|
| 77 |
+
|
| 78 |
+
## New Features
|
| 79 |
+
|
| 80 |
+
### 1. Parallel Processing
|
| 81 |
+
- Configurable parallel workers (default: 8)
|
| 82 |
+
- Interleaved URL processing ensures progress across all queries
|
| 83 |
+
- Much faster than sequential processing
|
| 84 |
+
|
| 85 |
+
### 2. Query Grouping
|
| 86 |
+
- Sources are visually grouped under their originating query
|
| 87 |
+
- Makes it clear which query found which information
|
| 88 |
+
- Helps understand coverage
|
| 89 |
+
|
| 90 |
+
### 3. Statistics Tracking
|
| 91 |
+
- Per-query stats: relevant / not relevant / failed
|
| 92 |
+
- Live updates as sources are processed
|
| 93 |
+
- Green color for queries with relevant sources
|
| 94 |
+
|
| 95 |
+
### 4. Error Tracking
|
| 96 |
+
- Failed API calls are tracked and displayed
|
| 97 |
+
- Separate count in stats
|
| 98 |
+
- Red error indicator (✗) on failed sources
|
| 99 |
+
- Error messages preserved
|
| 100 |
+
|
| 101 |
+
### 5. Toggle for Irrelevant Sources
|
| 102 |
+
- Button in research header
|
| 103 |
+
- Default: hide irrelevant sources
|
| 104 |
+
- Click to show all sources including irrelevant
|
| 105 |
+
- Re-renders all query groups on toggle
|
| 106 |
+
|
| 107 |
+
## Diagram: New Parallel Flow
|
| 108 |
+
|
| 109 |
+
```
|
| 110 |
+
Research Flow - NEW (Parallel & Interleaved)
|
| 111 |
+
============================================
|
| 112 |
+
|
| 113 |
+
ITERATION 1:
|
| 114 |
+
│
|
| 115 |
+
├─ Generate Queries (Sequential - Main Model)
|
| 116 |
+
│ └─ Returns: ["query1", "query2", "query3"]
|
| 117 |
+
│
|
| 118 |
+
├─ Web Search (PARALLEL - 3 workers) ⚡
|
| 119 |
+
│ ├─ search_web(query1) ──┐
|
| 120 |
+
│ ├─ search_web(query2) ──┼─→ All results collected
|
| 121 |
+
│ └─ search_web(query3) ──┘
|
| 122 |
+
│ Results grouped by query_index
|
| 123 |
+
│
|
| 124 |
+
├─ Interleave URLs (Round-robin from all queries)
|
| 125 |
+
│ [query1_url1, query2_url1, query3_url1,
|
| 126 |
+
│ query1_url2, query2_url2, query3_url2, ...]
|
| 127 |
+
│
|
| 128 |
+
├─ Process URLs (PARALLEL - 8 workers) ⚡⚡⚡
|
| 129 |
+
│ Worker 1: extract + analyze (query1_url1)
|
| 130 |
+
│ Worker 2: extract + analyze (query2_url1)
|
| 131 |
+
│ Worker 3: extract + analyze (query3_url1)
|
| 132 |
+
│ Worker 4: extract + analyze (query1_url2)
|
| 133 |
+
│ Worker 5: extract + analyze (query2_url2)
|
| 134 |
+
│ ...
|
| 135 |
+
│ │
|
| 136 |
+
│ └─ As each completes:
|
| 137 |
+
│ ├─ Emit "source" event with query_index
|
| 138 |
+
│ ├─ Update query stats
|
| 139 |
+
│ └─ Emit "query_stats" event
|
| 140 |
+
│
|
| 141 |
+
├─ Assess Completeness (Sequential - Main Model)
|
| 142 |
+
│
|
| 143 |
+
└─ If not sufficient → ITERATION 2
|
| 144 |
+
|
| 145 |
+
FINAL REPORT (Sequential - Main Model)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
PERFORMANCE:
|
| 149 |
+
- Search phase: 3x faster (parallel searches)
|
| 150 |
+
- Analysis phase: 8x faster (8 parallel workers)
|
| 151 |
+
- Interleaving ensures balanced progress across queries
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
## Testing Checklist
|
| 155 |
+
|
| 156 |
+
- [ ] Settings page loads with new parallel workers field
|
| 157 |
+
- [ ] Settings save/load correctly
|
| 158 |
+
- [ ] Research creates single container with toggle button
|
| 159 |
+
- [ ] Queries are grouped with headers
|
| 160 |
+
- [ ] Sources appear under correct query
|
| 161 |
+
- [ ] Stats update live (X relevant / Y not relevant / Z failed)
|
| 162 |
+
- [ ] Toggle button works (show/hide irrelevant)
|
| 163 |
+
- [ ] Relevant sources show ✓ icon
|
| 164 |
+
- [ ] Irrelevant sources show ○ icon and are hidden by default
|
| 165 |
+
- [ ] Failed sources show ✗ icon and count in stats
|
| 166 |
+
- [ ] Final report appears below research container
|
| 167 |
+
- [ ] Report markdown renders correctly (tables, headings, etc.)
|
| 168 |
+
- [ ] Result appears in command center action widget
|
| 169 |
+
|
| 170 |
+
## Performance Expectations
|
| 171 |
+
|
| 172 |
+
With 3 queries × 5 URLs per query = 15 URLs:
|
| 173 |
+
|
| 174 |
+
**Old (Sequential):**
|
| 175 |
+
- Search: ~3 seconds per query × 3 = 9s
|
| 176 |
+
- Analysis: ~2 seconds per URL × 15 = 30s
|
| 177 |
+
- **Total: ~39 seconds**
|
| 178 |
+
|
| 179 |
+
**New (Parallel with 8 workers):**
|
| 180 |
+
- Search: ~3 seconds (all parallel) = 3s
|
| 181 |
+
- Analysis: ~2 seconds × 2 batches (15/8) = 4s
|
| 182 |
+
- **Total: ~7 seconds**
|
| 183 |
+
|
| 184 |
+
**Speedup: ~5.5x faster!**
|
RESEARCH_REFACTOR_PLAN.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Research Refactoring Plan
|
| 2 |
+
|
| 3 |
+
## Current Flow
|
| 4 |
+
1. Generate queries (sequential)
|
| 5 |
+
2. For each query: search (parallel), then for each result: extract+analyze (sequential)
|
| 6 |
+
3. Assess completeness
|
| 7 |
+
4. Generate report
|
| 8 |
+
|
| 9 |
+
## New Flow
|
| 10 |
+
1. Generate queries (sequential)
|
| 11 |
+
2. Search ALL queries (parallel) → get all URLs
|
| 12 |
+
3. Interleave URLs from all queries
|
| 13 |
+
4. Extract+analyze URLs in parallel (8 workers, interleaved)
|
| 14 |
+
5. Group results by query
|
| 15 |
+
6. Assess completeness
|
| 16 |
+
7. Generate report
|
| 17 |
+
|
| 18 |
+
## Data Structure Changes
|
| 19 |
+
|
| 20 |
+
### Source Event
|
| 21 |
+
```python
|
| 22 |
+
{
|
| 23 |
+
"type": "source",
|
| 24 |
+
"query_index": 0, # NEW: which query this belongs to
|
| 25 |
+
"query_text": "query string", # NEW: the actual query
|
| 26 |
+
"title": "...",
|
| 27 |
+
"url": "...",
|
| 28 |
+
"analysis": "...",
|
| 29 |
+
"finding_count": 1, # overall finding count
|
| 30 |
+
"is_relevant": True,
|
| 31 |
+
"is_error": False, # NEW: track failures
|
| 32 |
+
"error_message": "" # NEW: error details
|
| 33 |
+
}
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### Query Stats Event (NEW)
|
| 37 |
+
```python
|
| 38 |
+
{
|
| 39 |
+
"type": "query_stats",
|
| 40 |
+
"query_index": 0,
|
| 41 |
+
"relevant_count": 5,
|
| 42 |
+
"irrelevant_count": 3,
|
| 43 |
+
"error_count": 1
|
| 44 |
+
}
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
## UI Changes
|
| 48 |
+
- Group sources under each query
|
| 49 |
+
- Show stats per query: "5 relevant / 3 not relevant / 1 failed"
|
| 50 |
+
- Toggle button to show/hide irrelevant sources
|
| 51 |
+
- Track and display API failures
|
| 52 |
+
|
| 53 |
+
## Implementation Steps
|
| 54 |
+
1. ✅ Add research_parallel_workers to settings
|
| 55 |
+
2. ⏳ Update backend ChatRequest model
|
| 56 |
+
3. ⏳ Refactor stream_research() in research.py
|
| 57 |
+
4. ⏳ Update research-ui.js to group by query
|
| 58 |
+
5. ⏳ Add query stats display
|
| 59 |
+
6. ⏳ Add toggle for irrelevant sources
|
backend/__pycache__/code.cpython-312.pyc
CHANGED
|
Binary files a/backend/__pycache__/code.cpython-312.pyc and b/backend/__pycache__/code.cpython-312.pyc differ
|
|
|
backend/__pycache__/research.cpython-312.pyc
CHANGED
|
Binary files a/backend/__pycache__/research.cpython-312.pyc and b/backend/__pycache__/research.cpython-312.pyc differ
|
|
|
backend/code.py
CHANGED
|
@@ -181,6 +181,11 @@ def stream_code_execution(client, model: str, messages: List[Dict], sbx: Sandbox
|
|
| 181 |
yield format_code_cell(code, output, has_error, images)
|
| 182 |
|
| 183 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
yield format_code_cell(code, f"Execution error: {str(e)}", True)
|
| 185 |
output = f"Execution failed: {str(e)}"
|
| 186 |
has_error = True
|
|
|
|
| 181 |
yield format_code_cell(code, output, has_error, images)
|
| 182 |
|
| 183 |
except Exception as e:
|
| 184 |
+
error_str = str(e)
|
| 185 |
+
# Check if this is a sandbox timeout error - if so, re-raise to trigger cleanup
|
| 186 |
+
if "502" in error_str or "sandbox was not found" in error_str.lower() or "timeout" in error_str.lower():
|
| 187 |
+
raise # Re-raise to be caught by main.py handler
|
| 188 |
+
|
| 189 |
yield format_code_cell(code, f"Execution error: {str(e)}", True)
|
| 190 |
output = f"Execution failed: {str(e)}"
|
| 191 |
has_error = True
|
backend/main.py
CHANGED
|
@@ -128,6 +128,26 @@ Your role is to:
|
|
| 128 |
- Provide well-structured, evidence-based answers
|
| 129 |
- Identify key insights and trends
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
Focus on being comprehensive, analytical, and well-sourced in your research.
|
| 132 |
""",
|
| 133 |
"chat": """You are a conversational AI assistant.
|
|
@@ -157,9 +177,19 @@ class ChatRequest(BaseModel):
|
|
| 157 |
model: Optional[str] = "gpt-4" # Model name
|
| 158 |
e2b_key: Optional[str] = None # E2B API key for code execution
|
| 159 |
serper_key: Optional[str] = None # Serper API key for research
|
|
|
|
|
|
|
|
|
|
| 160 |
notebook_id: Optional[str] = None # Unique notebook/tab ID for session management
|
| 161 |
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
async def stream_code_notebook(
|
| 164 |
messages: List[dict],
|
| 165 |
endpoint: str,
|
|
@@ -204,7 +234,21 @@ async def stream_code_notebook(
|
|
| 204 |
import traceback
|
| 205 |
error_message = f"Code execution error: {str(e)}\n{traceback.format_exc()}"
|
| 206 |
print(error_message)
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
|
| 210 |
async def stream_research_notebook(
|
|
@@ -212,7 +256,10 @@ async def stream_research_notebook(
|
|
| 212 |
endpoint: str,
|
| 213 |
token: Optional[str],
|
| 214 |
model: str,
|
| 215 |
-
serper_key: str
|
|
|
|
|
|
|
|
|
|
| 216 |
):
|
| 217 |
"""Handle research notebook with web search"""
|
| 218 |
|
|
@@ -235,8 +282,20 @@ async def stream_research_notebook(
|
|
| 235 |
# Create OpenAI client
|
| 236 |
client = OpenAI(base_url=endpoint, api_key=token)
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
# Stream research
|
| 239 |
-
for update in stream_research(client, model, question, serper_key):
|
| 240 |
yield f"data: {json.dumps(update)}\n\n"
|
| 241 |
|
| 242 |
except Exception as e:
|
|
@@ -358,6 +417,52 @@ async def root():
|
|
| 358 |
}
|
| 359 |
|
| 360 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
@app.post("/api/chat/stream")
|
| 362 |
async def chat_stream(request: ChatRequest):
|
| 363 |
"""Proxy streaming chat to user's configured LLM endpoint"""
|
|
@@ -403,7 +508,9 @@ async def chat_stream(request: ChatRequest):
|
|
| 403 |
request.endpoint,
|
| 404 |
request.token,
|
| 405 |
request.model or "gpt-4",
|
| 406 |
-
request.serper_key or ""
|
|
|
|
|
|
|
| 407 |
),
|
| 408 |
media_type="text/event-stream",
|
| 409 |
headers={
|
|
|
|
| 128 |
- Provide well-structured, evidence-based answers
|
| 129 |
- Identify key insights and trends
|
| 130 |
|
| 131 |
+
When presenting your final research report:
|
| 132 |
+
1. Be CONCISE - focus on key findings, not lengthy explanations
|
| 133 |
+
2. Use TABLES wherever possible to structure information clearly
|
| 134 |
+
3. Use markdown table syntax for comparisons, lists of facts, statistics, etc.
|
| 135 |
+
4. Example table format:
|
| 136 |
+
| Category | Details |
|
| 137 |
+
|----------|---------|
|
| 138 |
+
| Item 1 | Data |
|
| 139 |
+
| Item 2 | Data |
|
| 140 |
+
|
| 141 |
+
5. Only use prose for context and synthesis that can't be tabulated
|
| 142 |
+
|
| 143 |
+
When you have completed your research, wrap your final report in <result> tags:
|
| 144 |
+
|
| 145 |
+
<result>
|
| 146 |
+
Your concise, table-based report here
|
| 147 |
+
</result>
|
| 148 |
+
|
| 149 |
+
The report will be sent back to the main interface.
|
| 150 |
+
|
| 151 |
Focus on being comprehensive, analytical, and well-sourced in your research.
|
| 152 |
""",
|
| 153 |
"chat": """You are a conversational AI assistant.
|
|
|
|
| 177 |
model: Optional[str] = "gpt-4" # Model name
|
| 178 |
e2b_key: Optional[str] = None # E2B API key for code execution
|
| 179 |
serper_key: Optional[str] = None # Serper API key for research
|
| 180 |
+
research_sub_agent_model: Optional[str] = None # Smaller model for research sub-tasks
|
| 181 |
+
research_parallel_workers: Optional[int] = None # Number of parallel workers for research
|
| 182 |
+
research_max_websites: Optional[int] = None # Max websites to analyze per research session
|
| 183 |
notebook_id: Optional[str] = None # Unique notebook/tab ID for session management
|
| 184 |
|
| 185 |
|
| 186 |
+
class TitleRequest(BaseModel):
|
| 187 |
+
query: str
|
| 188 |
+
endpoint: str # User's configured LLM endpoint
|
| 189 |
+
token: Optional[str] = None # Optional auth token
|
| 190 |
+
model: Optional[str] = "gpt-4" # Model name
|
| 191 |
+
|
| 192 |
+
|
| 193 |
async def stream_code_notebook(
|
| 194 |
messages: List[dict],
|
| 195 |
endpoint: str,
|
|
|
|
| 234 |
import traceback
|
| 235 |
error_message = f"Code execution error: {str(e)}\n{traceback.format_exc()}"
|
| 236 |
print(error_message)
|
| 237 |
+
|
| 238 |
+
# Check if this is a sandbox timeout error (502)
|
| 239 |
+
error_str = str(e)
|
| 240 |
+
if "502" in error_str or "sandbox was not found" in error_str.lower() or "timeout" in error_str.lower():
|
| 241 |
+
# Remove the timed-out sandbox from cache
|
| 242 |
+
if session_id in SANDBOXES:
|
| 243 |
+
try:
|
| 244 |
+
SANDBOXES[session_id].kill()
|
| 245 |
+
except:
|
| 246 |
+
pass
|
| 247 |
+
del SANDBOXES[session_id]
|
| 248 |
+
|
| 249 |
+
yield f"data: {json.dumps({'type': 'error', 'content': 'Sandbox timed out and has been deleted. Please run your code again to create a new sandbox.'})}\n\n"
|
| 250 |
+
else:
|
| 251 |
+
yield f"data: {json.dumps({'type': 'error', 'content': error_message})}\n\n"
|
| 252 |
|
| 253 |
|
| 254 |
async def stream_research_notebook(
|
|
|
|
| 256 |
endpoint: str,
|
| 257 |
token: Optional[str],
|
| 258 |
model: str,
|
| 259 |
+
serper_key: str,
|
| 260 |
+
sub_agent_model: Optional[str] = None,
|
| 261 |
+
parallel_workers: Optional[int] = None,
|
| 262 |
+
max_websites: Optional[int] = None
|
| 263 |
):
|
| 264 |
"""Handle research notebook with web search"""
|
| 265 |
|
|
|
|
| 282 |
# Create OpenAI client
|
| 283 |
client = OpenAI(base_url=endpoint, api_key=token)
|
| 284 |
|
| 285 |
+
# Get system prompt for research
|
| 286 |
+
system_prompt = SYSTEM_PROMPTS.get("research", "")
|
| 287 |
+
|
| 288 |
+
# Use sub-agent model if provided, otherwise fall back to main model
|
| 289 |
+
analysis_model = sub_agent_model if sub_agent_model else model
|
| 290 |
+
|
| 291 |
+
# Use parallel workers if provided, otherwise default to 8
|
| 292 |
+
workers = parallel_workers if parallel_workers else 8
|
| 293 |
+
|
| 294 |
+
# Use max websites if provided, otherwise default to 50
|
| 295 |
+
max_sites = max_websites if max_websites else 50
|
| 296 |
+
|
| 297 |
# Stream research
|
| 298 |
+
for update in stream_research(client, model, question, serper_key, max_websites=max_sites, system_prompt=system_prompt, sub_agent_model=analysis_model, parallel_workers=workers):
|
| 299 |
yield f"data: {json.dumps(update)}\n\n"
|
| 300 |
|
| 301 |
except Exception as e:
|
|
|
|
| 417 |
}
|
| 418 |
|
| 419 |
|
| 420 |
+
@app.post("/api/generate-title")
|
| 421 |
+
async def generate_title(request: TitleRequest):
|
| 422 |
+
"""Generate a short 2-3 word title for a user query"""
|
| 423 |
+
try:
|
| 424 |
+
# Create headers
|
| 425 |
+
headers = {"Content-Type": "application/json"}
|
| 426 |
+
if request.token:
|
| 427 |
+
headers["Authorization"] = f"Bearer {request.token}"
|
| 428 |
+
|
| 429 |
+
# Call the LLM to generate a title
|
| 430 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 431 |
+
llm_response = await client.post(
|
| 432 |
+
f"{request.endpoint}/chat/completions",
|
| 433 |
+
headers=headers,
|
| 434 |
+
json={
|
| 435 |
+
"model": request.model,
|
| 436 |
+
"messages": [
|
| 437 |
+
{
|
| 438 |
+
"role": "system",
|
| 439 |
+
"content": "You are a helpful assistant that generates concise 2-3 word titles for user queries. Respond with ONLY the title, no additional text, punctuation, or quotes."
|
| 440 |
+
},
|
| 441 |
+
{
|
| 442 |
+
"role": "user",
|
| 443 |
+
"content": f"Generate a 2-3 word title for this query: {request.query}"
|
| 444 |
+
}
|
| 445 |
+
],
|
| 446 |
+
"temperature": 0.3,
|
| 447 |
+
"max_tokens": 20
|
| 448 |
+
}
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
if llm_response.status_code != 200:
|
| 452 |
+
raise HTTPException(status_code=llm_response.status_code, detail="LLM API error")
|
| 453 |
+
|
| 454 |
+
result = llm_response.json()
|
| 455 |
+
title = result["choices"][0]["message"]["content"].strip()
|
| 456 |
+
|
| 457 |
+
# Remove any quotes that might be in the response
|
| 458 |
+
title = title.replace('"', '').replace("'", '')
|
| 459 |
+
|
| 460 |
+
return {"title": title}
|
| 461 |
+
|
| 462 |
+
except Exception as e:
|
| 463 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 464 |
+
|
| 465 |
+
|
| 466 |
@app.post("/api/chat/stream")
|
| 467 |
async def chat_stream(request: ChatRequest):
|
| 468 |
"""Proxy streaming chat to user's configured LLM endpoint"""
|
|
|
|
| 508 |
request.endpoint,
|
| 509 |
request.token,
|
| 510 |
request.model or "gpt-4",
|
| 511 |
+
request.serper_key or "",
|
| 512 |
+
request.research_sub_agent_model,
|
| 513 |
+
request.research_parallel_workers
|
| 514 |
),
|
| 515 |
media_type="text/event-stream",
|
| 516 |
headers={
|
backend/research.py
CHANGED
|
@@ -162,7 +162,7 @@ Return ONLY a JSON object with this structure:
|
|
| 162 |
return {"sufficient": len(findings) >= 5, "missing_aspects": []}
|
| 163 |
|
| 164 |
|
| 165 |
-
def generate_final_report(client, model: str, user_question: str, findings: List[Dict]) -> str:
|
| 166 |
"""Generate final research report"""
|
| 167 |
findings_text = "\n\n".join([
|
| 168 |
f"Source {i+1}: {f['source']}\n{f['analysis']}"
|
|
@@ -175,16 +175,11 @@ All information gathered from {len(findings)} sources:
|
|
| 175 |
|
| 176 |
{findings_text}
|
| 177 |
|
| 178 |
-
Write a
|
| 179 |
-
1. Directly answers the research question
|
| 180 |
-
2. Synthesizes information from multiple sources
|
| 181 |
-
3. Includes specific facts, data, and insights
|
| 182 |
-
4. Cites sources where appropriate (e.g., "According to [source]...")
|
| 183 |
-
5. Is organized with clear sections/paragraphs
|
| 184 |
-
|
| 185 |
-
Your report:"""
|
| 186 |
|
| 187 |
messages = [{"role": "user", "content": prompt}]
|
|
|
|
|
|
|
| 188 |
|
| 189 |
try:
|
| 190 |
response = client.chat.completions.create(
|
|
@@ -205,23 +200,35 @@ def stream_research(
|
|
| 205 |
question: str,
|
| 206 |
serper_key: str,
|
| 207 |
max_iterations: int = 5,
|
| 208 |
-
max_websites: int = 50
|
|
|
|
|
|
|
|
|
|
| 209 |
):
|
| 210 |
"""
|
| 211 |
Stream deep research results with progress updates
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
Yields:
|
| 214 |
-
dict: Updates with type 'progress', 'source', '
|
| 215 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
findings = []
|
| 217 |
websites_visited = 0
|
| 218 |
iteration = 0
|
| 219 |
|
|
|
|
|
|
|
|
|
|
| 220 |
yield {
|
| 221 |
"type": "status",
|
| 222 |
-
"message": f"Starting research
|
| 223 |
-
"iteration": 0,
|
| 224 |
-
"total_iterations": max_iterations
|
| 225 |
}
|
| 226 |
|
| 227 |
while iteration < max_iterations and websites_visited < max_websites:
|
|
@@ -230,9 +237,7 @@ def stream_research(
|
|
| 230 |
# Generate queries
|
| 231 |
yield {
|
| 232 |
"type": "status",
|
| 233 |
-
"message":
|
| 234 |
-
"iteration": iteration,
|
| 235 |
-
"total_iterations": max_iterations
|
| 236 |
}
|
| 237 |
|
| 238 |
existing_knowledge = "\n".join([f['analysis'] for f in findings[-3:]])
|
|
@@ -244,90 +249,206 @@ def stream_research(
|
|
| 244 |
"iteration": iteration
|
| 245 |
}
|
| 246 |
|
| 247 |
-
#
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
"type": "progress",
|
| 254 |
-
"message": f"Searching: {query}",
|
| 255 |
-
"query_index": query_idx + 1,
|
| 256 |
-
"total_queries": len(queries),
|
| 257 |
-
"iteration": iteration
|
| 258 |
}
|
| 259 |
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
break
|
| 265 |
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
}
|
| 274 |
|
|
|
|
|
|
|
| 275 |
content = extract_content(url)
|
| 276 |
-
websites_visited += 1
|
| 277 |
|
| 278 |
if not content or len(content) < 100:
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
-
|
|
|
|
| 282 |
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
yield {
|
| 291 |
"type": "source",
|
|
|
|
|
|
|
| 292 |
"title": result['title'],
|
| 293 |
-
"url": url,
|
| 294 |
-
"analysis": analysis,
|
| 295 |
-
"finding_count": len(findings)
|
|
|
|
|
|
|
|
|
|
| 296 |
}
|
| 297 |
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
"sufficient": assessment.get('sufficient', False),
|
| 311 |
-
"missing_aspects": assessment.get('missing_aspects', []),
|
| 312 |
-
"findings_count": len(findings)
|
| 313 |
-
}
|
| 314 |
|
| 315 |
-
|
|
|
|
| 316 |
yield {
|
| 317 |
"type": "status",
|
| 318 |
-
"message": "
|
| 319 |
}
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
# Generate final report
|
| 323 |
if findings:
|
| 324 |
-
report = generate_final_report(client, model, question, findings)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
|
|
|
|
| 326 |
yield {
|
| 327 |
-
"type": "
|
| 328 |
-
"content":
|
| 329 |
-
"
|
| 330 |
-
"websites_visited": websites_visited
|
| 331 |
}
|
| 332 |
else:
|
| 333 |
yield {
|
|
|
|
| 162 |
return {"sufficient": len(findings) >= 5, "missing_aspects": []}
|
| 163 |
|
| 164 |
|
| 165 |
+
def generate_final_report(client, model: str, user_question: str, findings: List[Dict], system_prompt: str = "") -> str:
|
| 166 |
"""Generate final research report"""
|
| 167 |
findings_text = "\n\n".join([
|
| 168 |
f"Source {i+1}: {f['source']}\n{f['analysis']}"
|
|
|
|
| 175 |
|
| 176 |
{findings_text}
|
| 177 |
|
| 178 |
+
Write a concise, well-structured report following the guidelines in your system instructions. Remember to use tables where appropriate and wrap your final report in <result> tags."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
messages = [{"role": "user", "content": prompt}]
|
| 181 |
+
if system_prompt:
|
| 182 |
+
messages.insert(0, {"role": "system", "content": system_prompt})
|
| 183 |
|
| 184 |
try:
|
| 185 |
response = client.chat.completions.create(
|
|
|
|
| 200 |
question: str,
|
| 201 |
serper_key: str,
|
| 202 |
max_iterations: int = 5,
|
| 203 |
+
max_websites: int = 50,
|
| 204 |
+
system_prompt: str = "",
|
| 205 |
+
sub_agent_model: Optional[str] = None,
|
| 206 |
+
parallel_workers: int = 8
|
| 207 |
):
|
| 208 |
"""
|
| 209 |
Stream deep research results with progress updates
|
| 210 |
|
| 211 |
+
Args:
|
| 212 |
+
sub_agent_model: Smaller/faster model for analyzing individual web pages. If None, uses main model.
|
| 213 |
+
parallel_workers: Number of parallel workers for extract+analyze operations
|
| 214 |
+
|
| 215 |
Yields:
|
| 216 |
+
dict: Updates with type 'progress', 'source', 'query_stats', 'report', 'result', 'result_preview', 'done', or 'error'
|
| 217 |
"""
|
| 218 |
+
import concurrent.futures
|
| 219 |
+
import re
|
| 220 |
+
from collections import defaultdict
|
| 221 |
+
|
| 222 |
findings = []
|
| 223 |
websites_visited = 0
|
| 224 |
iteration = 0
|
| 225 |
|
| 226 |
+
# Use sub-agent model for analysis if provided, otherwise use main model
|
| 227 |
+
analysis_model = sub_agent_model if sub_agent_model else model
|
| 228 |
+
|
| 229 |
yield {
|
| 230 |
"type": "status",
|
| 231 |
+
"message": f"Starting research: {question}"
|
|
|
|
|
|
|
| 232 |
}
|
| 233 |
|
| 234 |
while iteration < max_iterations and websites_visited < max_websites:
|
|
|
|
| 237 |
# Generate queries
|
| 238 |
yield {
|
| 239 |
"type": "status",
|
| 240 |
+
"message": "Generating search queries..."
|
|
|
|
|
|
|
| 241 |
}
|
| 242 |
|
| 243 |
existing_knowledge = "\n".join([f['analysis'] for f in findings[-3:]])
|
|
|
|
| 249 |
"iteration": iteration
|
| 250 |
}
|
| 251 |
|
| 252 |
+
# Collect all search results for all queries in parallel
|
| 253 |
+
query_results = {} # query_index -> list of results
|
| 254 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
|
| 255 |
+
future_to_query_idx = {
|
| 256 |
+
executor.submit(search_web, query, serper_key, num_results=5): idx
|
| 257 |
+
for idx, query in enumerate(queries)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
}
|
| 259 |
|
| 260 |
+
for future in concurrent.futures.as_completed(future_to_query_idx):
|
| 261 |
+
query_idx = future_to_query_idx[future]
|
| 262 |
+
try:
|
| 263 |
+
results = future.result()
|
| 264 |
+
query_results[query_idx] = results
|
| 265 |
+
except Exception as e:
|
| 266 |
+
print(f"Search failed for query {query_idx}: {e}")
|
| 267 |
+
query_results[query_idx] = []
|
| 268 |
+
|
| 269 |
+
# Interleave results from all queries
|
| 270 |
+
interleaved_urls = []
|
| 271 |
+
max_results = max((len(results) for results in query_results.values()), default=0)
|
| 272 |
+
|
| 273 |
+
for result_idx in range(max_results):
|
| 274 |
+
for query_idx in range(len(queries)):
|
| 275 |
+
if query_idx in query_results and result_idx < len(query_results[query_idx]):
|
| 276 |
+
result = query_results[query_idx][result_idx]
|
| 277 |
+
interleaved_urls.append({
|
| 278 |
+
'query_index': query_idx,
|
| 279 |
+
'query_text': queries[query_idx],
|
| 280 |
+
'url': result['url'],
|
| 281 |
+
'title': result['title']
|
| 282 |
+
})
|
| 283 |
|
| 284 |
+
# Track stats per query
|
| 285 |
+
query_stats = defaultdict(lambda: {'relevant': 0, 'irrelevant': 0, 'error': 0})
|
|
|
|
| 286 |
|
| 287 |
+
# Process URLs in parallel with interleaved order
|
| 288 |
+
def process_url(url_data):
|
| 289 |
+
"""Extract content and analyze for a single URL"""
|
| 290 |
+
query_idx = url_data['query_index']
|
| 291 |
+
query_text = url_data['query_text']
|
| 292 |
+
url = url_data['url']
|
| 293 |
+
title = url_data['title']
|
|
|
|
| 294 |
|
| 295 |
+
try:
|
| 296 |
+
# Extract content
|
| 297 |
content = extract_content(url)
|
|
|
|
| 298 |
|
| 299 |
if not content or len(content) < 100:
|
| 300 |
+
return {
|
| 301 |
+
'query_index': query_idx,
|
| 302 |
+
'query_text': query_text,
|
| 303 |
+
'title': title,
|
| 304 |
+
'url': url,
|
| 305 |
+
'analysis': "Could not extract content from this page.",
|
| 306 |
+
'is_relevant': False,
|
| 307 |
+
'is_error': True,
|
| 308 |
+
'error_message': "Content extraction failed"
|
| 309 |
+
}
|
| 310 |
|
| 311 |
+
# Analyze content
|
| 312 |
+
analysis = analyze_content(client, analysis_model, question, content, url)
|
| 313 |
|
| 314 |
+
is_relevant = "no relevant information" not in analysis.lower()
|
| 315 |
+
|
| 316 |
+
return {
|
| 317 |
+
'query_index': query_idx,
|
| 318 |
+
'query_text': query_text,
|
| 319 |
+
'title': title,
|
| 320 |
+
'url': url,
|
| 321 |
+
'analysis': analysis,
|
| 322 |
+
'is_relevant': is_relevant,
|
| 323 |
+
'is_error': False,
|
| 324 |
+
'error_message': ""
|
| 325 |
+
}
|
| 326 |
+
except Exception as e:
|
| 327 |
+
return {
|
| 328 |
+
'query_index': query_idx,
|
| 329 |
+
'query_text': query_text,
|
| 330 |
+
'title': title,
|
| 331 |
+
'url': url,
|
| 332 |
+
'analysis': f"Error: {str(e)}",
|
| 333 |
+
'is_relevant': False,
|
| 334 |
+
'is_error': True,
|
| 335 |
+
'error_message': str(e)
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
# Process all URLs in parallel with progress updates
|
| 339 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=parallel_workers) as executor:
|
| 340 |
+
futures = {executor.submit(process_url, url_data): url_data for url_data in interleaved_urls[:max_websites - websites_visited]}
|
| 341 |
+
|
| 342 |
+
for future in concurrent.futures.as_completed(futures):
|
| 343 |
+
if websites_visited >= max_websites:
|
| 344 |
+
break
|
| 345 |
|
| 346 |
+
try:
|
| 347 |
+
result = future.result()
|
| 348 |
+
websites_visited += 1
|
| 349 |
+
|
| 350 |
+
# Update stats
|
| 351 |
+
if result['is_error']:
|
| 352 |
+
query_stats[result['query_index']]['error'] += 1
|
| 353 |
+
elif result['is_relevant']:
|
| 354 |
+
query_stats[result['query_index']]['relevant'] += 1
|
| 355 |
+
# Add to findings
|
| 356 |
+
findings.append({
|
| 357 |
+
'source': result['url'],
|
| 358 |
+
'title': result['title'],
|
| 359 |
+
'analysis': result['analysis']
|
| 360 |
+
})
|
| 361 |
+
else:
|
| 362 |
+
query_stats[result['query_index']]['irrelevant'] += 1
|
| 363 |
+
|
| 364 |
+
# Send source event
|
| 365 |
yield {
|
| 366 |
"type": "source",
|
| 367 |
+
"query_index": result['query_index'],
|
| 368 |
+
"query_text": result['query_text'],
|
| 369 |
"title": result['title'],
|
| 370 |
+
"url": result['url'],
|
| 371 |
+
"analysis": result['analysis'],
|
| 372 |
+
"finding_count": len(findings),
|
| 373 |
+
"is_relevant": result['is_relevant'],
|
| 374 |
+
"is_error": result['is_error'],
|
| 375 |
+
"error_message": result['error_message']
|
| 376 |
}
|
| 377 |
|
| 378 |
+
# Send updated stats for this query
|
| 379 |
+
stats = query_stats[result['query_index']]
|
| 380 |
+
yield {
|
| 381 |
+
"type": "query_stats",
|
| 382 |
+
"query_index": result['query_index'],
|
| 383 |
+
"relevant_count": stats['relevant'],
|
| 384 |
+
"irrelevant_count": stats['irrelevant'],
|
| 385 |
+
"error_count": stats['error']
|
| 386 |
+
}
|
| 387 |
|
| 388 |
+
except Exception as e:
|
| 389 |
+
print(f"Error processing URL: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
|
| 391 |
+
# Assess completeness
|
| 392 |
+
if len(findings) >= 3: # Only assess if we have some findings
|
| 393 |
yield {
|
| 394 |
"type": "status",
|
| 395 |
+
"message": "Evaluating information gathered..."
|
| 396 |
}
|
| 397 |
+
|
| 398 |
+
assessment = assess_completeness(client, model, question, findings)
|
| 399 |
+
|
| 400 |
+
yield {
|
| 401 |
+
"type": "assessment",
|
| 402 |
+
"sufficient": assessment.get('sufficient', False),
|
| 403 |
+
"missing_aspects": assessment.get('missing_aspects', []),
|
| 404 |
+
"findings_count": len(findings)
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
if assessment.get('sufficient', False):
|
| 408 |
+
yield {
|
| 409 |
+
"type": "status",
|
| 410 |
+
"message": "Research complete! Generating final report..."
|
| 411 |
+
}
|
| 412 |
+
break
|
| 413 |
|
| 414 |
# Generate final report
|
| 415 |
if findings:
|
| 416 |
+
report = generate_final_report(client, model, question, findings, system_prompt)
|
| 417 |
+
|
| 418 |
+
print("\n" + "="*80)
|
| 419 |
+
print("RAW REPORT TEXT:")
|
| 420 |
+
print("="*80)
|
| 421 |
+
print(report)
|
| 422 |
+
print("="*80 + "\n")
|
| 423 |
+
|
| 424 |
+
# Parse result tags from report
|
| 425 |
+
result_match = re.search(r'<result>(.*?)</result>', report, re.DOTALL)
|
| 426 |
+
|
| 427 |
+
result_content = None
|
| 428 |
+
if result_match:
|
| 429 |
+
result_content = result_match.group(1).strip()
|
| 430 |
+
print("\n" + "="*80)
|
| 431 |
+
print("EXTRACTED RESULT CONTENT:")
|
| 432 |
+
print("="*80)
|
| 433 |
+
print(result_content)
|
| 434 |
+
print("="*80 + "\n")
|
| 435 |
+
else:
|
| 436 |
+
# If no result tags, use the full report
|
| 437 |
+
result_content = report
|
| 438 |
+
print(f"Warning: No <result> tags found in report, using full content")
|
| 439 |
+
|
| 440 |
+
# Send result preview to research notebook
|
| 441 |
+
yield {
|
| 442 |
+
"type": "result_preview",
|
| 443 |
+
"content": result_content,
|
| 444 |
+
"figures": {} # Research doesn't generate figures
|
| 445 |
+
}
|
| 446 |
|
| 447 |
+
# Send result to command center
|
| 448 |
yield {
|
| 449 |
+
"type": "result",
|
| 450 |
+
"content": result_content,
|
| 451 |
+
"figures": {}
|
|
|
|
| 452 |
}
|
| 453 |
else:
|
| 454 |
yield {
|
index.html
CHANGED
|
@@ -139,6 +139,30 @@
|
|
| 139 |
</div>
|
| 140 |
</div>
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
<div class="settings-actions">
|
| 143 |
<button class="settings-save-btn" id="saveSettingsBtn">SAVE</button>
|
| 144 |
<button class="settings-cancel-btn" id="cancelSettingsBtn">CANCEL</button>
|
|
|
|
| 139 |
</div>
|
| 140 |
</div>
|
| 141 |
|
| 142 |
+
<div class="settings-section">
|
| 143 |
+
<label class="settings-label">
|
| 144 |
+
<span class="label-text">RESEARCH SUB-AGENT MODEL (OPTIONAL)</span>
|
| 145 |
+
<span class="label-description">Smaller/faster model for analyzing individual web pages during research</span>
|
| 146 |
+
</label>
|
| 147 |
+
<input type="text" id="setting-research-sub-agent-model" class="settings-input" placeholder="Use research model or default">
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
<div class="settings-section">
|
| 151 |
+
<label class="settings-label">
|
| 152 |
+
<span class="label-text">RESEARCH PARALLEL WORKERS (OPTIONAL)</span>
|
| 153 |
+
<span class="label-description">Number of web pages to analyze in parallel (default: 8)</span>
|
| 154 |
+
</label>
|
| 155 |
+
<input type="number" id="setting-research-parallel-workers" class="settings-input" placeholder="8" min="1" max="20">
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<div class="settings-section">
|
| 159 |
+
<label class="settings-label">
|
| 160 |
+
<span class="label-text">RESEARCH MAX WEBSITES (OPTIONAL)</span>
|
| 161 |
+
<span class="label-description">Maximum number of websites to analyze per research session (default: 50)</span>
|
| 162 |
+
</label>
|
| 163 |
+
<input type="number" id="setting-research-max-websites" class="settings-input" placeholder="50" min="10" max="200">
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
<div class="settings-actions">
|
| 167 |
<button class="settings-save-btn" id="saveSettingsBtn">SAVE</button>
|
| 168 |
<button class="settings-cancel-btn" id="cancelSettingsBtn">CANCEL</button>
|
research-ui.js
CHANGED
|
@@ -1,108 +1,211 @@
|
|
| 1 |
-
// Research UI
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
if (
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
}
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
`;
|
| 17 |
-
chatContainer.appendChild(statusDiv);
|
| 18 |
}
|
| 19 |
|
| 20 |
function createQueriesMessage(chatContainer, queries, iteration) {
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
queriesDiv.innerHTML = `
|
| 24 |
-
<div class="queries-label">Search Queries (Iteration ${iteration})</div>
|
| 25 |
-
<ul class="queries-list">
|
| 26 |
-
${queries.map(q => `<li>${escapeHtml(q)}</li>`).join('')}
|
| 27 |
-
</ul>
|
| 28 |
-
`;
|
| 29 |
-
chatContainer.appendChild(queriesDiv);
|
| 30 |
-
}
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
progressDiv.className = 'research-progress';
|
| 39 |
-
chatContainer.appendChild(progressDiv);
|
| 40 |
-
}
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
<div class="
|
| 47 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
</div>
|
| 49 |
-
<div class="progress-count">${websitesVisited}/${maxWebsites} websites</div>
|
| 50 |
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
-
function
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
-
function
|
| 73 |
-
const
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
const
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
</div>
|
| 86 |
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
|
|
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
| 95 |
}
|
| 96 |
|
| 97 |
function createReportMessage(chatContainer, content, sourcesCount, websitesVisited) {
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
}
|
|
|
|
| 1 |
+
// Research UI - Grouped by queries with stats and toggle
|
| 2 |
|
| 3 |
+
let researchContainer = null;
|
| 4 |
+
let currentQueries = [];
|
| 5 |
+
let queryData = {}; // query_index -> {sources: [], stats: {}}
|
| 6 |
+
let showIrrelevant = false; // Toggle state
|
| 7 |
|
| 8 |
+
function getOrCreateResearchContainer(chatContainer) {
|
| 9 |
+
if (!researchContainer || !chatContainer.contains(researchContainer)) {
|
| 10 |
+
researchContainer = document.createElement('div');
|
| 11 |
+
researchContainer.className = 'research-container';
|
| 12 |
+
researchContainer.innerHTML = `
|
| 13 |
+
<div class="research-header">
|
| 14 |
+
<div class="research-title">Research in progress...</div>
|
| 15 |
+
<button class="toggle-irrelevant-btn" onclick="toggleIrrelevantSources()">
|
| 16 |
+
Show irrelevant sources
|
| 17 |
+
</button>
|
| 18 |
+
</div>
|
| 19 |
+
<div class="research-body">
|
| 20 |
+
<div class="research-queries-section"></div>
|
| 21 |
+
</div>
|
| 22 |
+
`;
|
| 23 |
+
chatContainer.appendChild(researchContainer);
|
| 24 |
}
|
| 25 |
+
return researchContainer;
|
| 26 |
+
}
|
| 27 |
|
| 28 |
+
function createStatusMessage(chatContainer, message) {
|
| 29 |
+
const container = getOrCreateResearchContainer(chatContainer);
|
| 30 |
+
const header = container.querySelector('.research-title');
|
| 31 |
+
header.textContent = message;
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
function createQueriesMessage(chatContainer, queries, iteration) {
|
| 35 |
+
currentQueries = queries;
|
| 36 |
+
queryData = {}; // Reset
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
// Initialize query data
|
| 39 |
+
queries.forEach((query, idx) => {
|
| 40 |
+
queryData[idx] = {
|
| 41 |
+
query: query,
|
| 42 |
+
sources: [],
|
| 43 |
+
stats: { relevant: 0, irrelevant: 0, error: 0 }
|
| 44 |
+
};
|
| 45 |
+
});
|
| 46 |
|
| 47 |
+
const container = getOrCreateResearchContainer(chatContainer);
|
| 48 |
+
const queriesSection = container.querySelector('.research-queries-section');
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
// Create query sections
|
| 51 |
+
let html = '<div class="queries-title">Search queries:</div>';
|
| 52 |
+
queries.forEach((query, idx) => {
|
| 53 |
+
html += `
|
| 54 |
+
<div class="query-group" id="query-group-${idx}">
|
| 55 |
+
<div class="query-header" style="cursor: pointer;" onclick="document.getElementById('query-sources-${idx}').scrollIntoView({behavior: 'smooth', block: 'nearest'})">
|
| 56 |
+
<span class="query-text">${escapeHtml(query)}</span>
|
| 57 |
+
<span class="query-stats" id="query-stats-${idx}">0 relevant / 0 not relevant / 0 failed</span>
|
| 58 |
+
</div>
|
| 59 |
+
<div class="query-sources" id="query-sources-${idx}"></div>
|
| 60 |
</div>
|
|
|
|
| 61 |
`;
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
queriesSection.innerHTML = html;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function createSourceMessage(chatContainer, data) {
|
| 68 |
+
// data includes: query_index, query_text, title, url, analysis, is_relevant, is_error, error_message
|
| 69 |
+
|
| 70 |
+
const queryIdx = data.query_index;
|
| 71 |
+
|
| 72 |
+
// Store source data
|
| 73 |
+
if (!queryData[queryIdx]) {
|
| 74 |
+
queryData[queryIdx] = {
|
| 75 |
+
query: data.query_text,
|
| 76 |
+
sources: [],
|
| 77 |
+
stats: { relevant: 0, irrelevant: 0, error: 0 }
|
| 78 |
+
};
|
| 79 |
}
|
| 80 |
|
| 81 |
+
queryData[queryIdx].sources.push(data);
|
| 82 |
+
|
| 83 |
+
// Update display if relevant or if showing irrelevant
|
| 84 |
+
if (data.is_relevant || showIrrelevant) {
|
| 85 |
+
renderQuerySources(queryIdx);
|
| 86 |
+
}
|
| 87 |
}
|
| 88 |
|
| 89 |
+
function updateQueryStats(queryIdx, stats) {
|
| 90 |
+
// Update stored stats
|
| 91 |
+
if (queryData[queryIdx]) {
|
| 92 |
+
queryData[queryIdx].stats = stats;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Update stats display
|
| 96 |
+
const statsEl = document.getElementById(`query-stats-${queryIdx}`);
|
| 97 |
+
if (statsEl) {
|
| 98 |
+
const parts = [];
|
| 99 |
+
if (stats.relevant > 0) parts.push(`${stats.relevant} relevant`);
|
| 100 |
+
if (stats.irrelevant > 0) parts.push(`${stats.irrelevant} not relevant`);
|
| 101 |
+
if (stats.error > 0) parts.push(`${stats.error} failed`);
|
| 102 |
+
|
| 103 |
+
statsEl.textContent = parts.length > 0 ? parts.join(' / ') : 'No results yet';
|
| 104 |
+
statsEl.style.color = stats.relevant > 0 ? '#2e7d32' : '#666';
|
| 105 |
+
}
|
| 106 |
}
|
| 107 |
|
| 108 |
+
function renderQuerySources(queryIdx) {
|
| 109 |
+
const sourcesContainer = document.getElementById(`query-sources-${queryIdx}`);
|
| 110 |
+
if (!sourcesContainer || !queryData[queryIdx]) return;
|
| 111 |
+
|
| 112 |
+
const sources = queryData[queryIdx].sources;
|
| 113 |
+
|
| 114 |
+
// Filter sources based on toggle
|
| 115 |
+
const visibleSources = showIrrelevant ? sources : sources.filter(s => s.is_relevant);
|
| 116 |
+
|
| 117 |
+
if (visibleSources.length === 0) {
|
| 118 |
+
sourcesContainer.innerHTML = '<div class="no-sources">No sources to display</div>';
|
| 119 |
+
return;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
let html = '';
|
| 123 |
+
visibleSources.forEach((source, idx) => {
|
| 124 |
+
const sourceId = `source-${queryIdx}-${idx}`;
|
| 125 |
+
const statusClass = source.is_error ? 'error' : (source.is_relevant ? 'relevant' : 'irrelevant');
|
| 126 |
+
|
| 127 |
+
html += `
|
| 128 |
+
<div class="research-source ${statusClass}">
|
| 129 |
+
<div class="source-header" onclick="toggleSourceContent('${sourceId}')">
|
| 130 |
+
<span class="source-status-icon">${source.is_error ? '✗' : (source.is_relevant ? '✓' : '○')}</span>
|
| 131 |
+
<a href="${escapeHtml(source.url)}" target="_blank" class="source-url" onclick="event.stopPropagation()">${escapeHtml(source.title)}</a>
|
| 132 |
+
<span class="source-toggle">▼</span>
|
| 133 |
+
</div>
|
| 134 |
+
<div class="source-analysis" id="${sourceId}" style="display: none;">
|
| 135 |
+
${parseMarkdown(source.analysis)}
|
| 136 |
+
</div>
|
| 137 |
</div>
|
| 138 |
`;
|
| 139 |
+
});
|
| 140 |
+
|
| 141 |
+
sourcesContainer.innerHTML = html;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
function toggleSourceContent(sourceId) {
|
| 145 |
+
const content = document.getElementById(sourceId);
|
| 146 |
+
if (!content) return;
|
| 147 |
+
|
| 148 |
+
const header = content.previousElementSibling;
|
| 149 |
+
const toggle = header.querySelector('.source-toggle');
|
| 150 |
+
|
| 151 |
+
if (content.style.display === 'none') {
|
| 152 |
+
content.style.display = 'block';
|
| 153 |
+
toggle.textContent = '▲';
|
| 154 |
+
} else {
|
| 155 |
+
content.style.display = 'none';
|
| 156 |
+
toggle.textContent = '▼';
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
function toggleIrrelevantSources() {
|
| 161 |
+
showIrrelevant = !showIrrelevant;
|
| 162 |
+
|
| 163 |
+
// Update button text
|
| 164 |
+
const btn = document.querySelector('.toggle-irrelevant-btn');
|
| 165 |
+
if (btn) {
|
| 166 |
+
btn.textContent = showIrrelevant ? 'Hide irrelevant sources' : 'Show irrelevant sources';
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// Re-render all queries
|
| 170 |
+
Object.keys(queryData).forEach(queryIdx => {
|
| 171 |
+
renderQuerySources(parseInt(queryIdx));
|
| 172 |
+
});
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
function updateProgress(chatContainer, message, websitesVisited, maxWebsites) {
|
| 176 |
+
// Progress is now tracked per query via stats, so this is less important
|
| 177 |
+
// But we can still update the header
|
| 178 |
+
const container = getOrCreateResearchContainer(chatContainer);
|
| 179 |
+
const header = container.querySelector('.research-title');
|
| 180 |
+
if (websitesVisited !== undefined && maxWebsites !== undefined) {
|
| 181 |
+
header.textContent = `Research in progress... (${websitesVisited}/${maxWebsites} pages analyzed)`;
|
| 182 |
}
|
| 183 |
+
}
|
| 184 |
|
| 185 |
+
function createAssessmentMessage(chatContainer, sufficient, missingAspects, findingsCount) {
|
| 186 |
+
const container = getOrCreateResearchContainer(chatContainer);
|
| 187 |
+
const header = container.querySelector('.research-title');
|
| 188 |
+
|
| 189 |
+
if (sufficient) {
|
| 190 |
+
header.textContent = `Research complete - ${findingsCount} relevant sources found`;
|
| 191 |
+
} else {
|
| 192 |
+
header.textContent = `Continuing research - ${findingsCount} relevant sources found so far`;
|
| 193 |
+
}
|
| 194 |
}
|
| 195 |
|
| 196 |
function createReportMessage(chatContainer, content, sourcesCount, websitesVisited) {
|
| 197 |
+
// This function is kept for backwards compatibility
|
| 198 |
+
// but normally won't be called as reports use result_preview now
|
| 199 |
+
|
| 200 |
+
// Mark research as complete
|
| 201 |
+
if (researchContainer) {
|
| 202 |
+
const header = researchContainer.querySelector('.research-title');
|
| 203 |
+
header.textContent = `Research complete - ${sourcesCount} sources`;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
// Reset for next research
|
| 207 |
+
researchContainer = null;
|
| 208 |
+
queryData = {};
|
| 209 |
+
currentQueries = [];
|
| 210 |
+
showIrrelevant = false;
|
| 211 |
}
|
script.js
CHANGED
|
@@ -296,6 +296,7 @@ async function sendMessage(tabId) {
|
|
| 296 |
|
| 297 |
// Remove welcome message if it exists (only on first user message)
|
| 298 |
const welcomeMsg = chatContainer.querySelector('.welcome-message');
|
|
|
|
| 299 |
if (welcomeMsg) {
|
| 300 |
welcomeMsg.remove();
|
| 301 |
}
|
|
@@ -309,6 +310,11 @@ async function sendMessage(tabId) {
|
|
| 309 |
`;
|
| 310 |
chatContainer.appendChild(userMsg);
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
// Clear input and disable it during processing
|
| 313 |
input.value = '';
|
| 314 |
input.disabled = true;
|
|
@@ -331,6 +337,43 @@ async function sendMessage(tabId) {
|
|
| 331 |
setTabGenerating(tabId, false);
|
| 332 |
}
|
| 333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
function getNotebookTypeFromContainer(chatContainer) {
|
| 335 |
// Try to get type from data attribute first (for dynamically created notebooks)
|
| 336 |
const typeFromData = chatContainer.dataset.notebookType;
|
|
@@ -399,6 +442,9 @@ async function streamChatResponse(messages, chatContainer, notebookType, tabId)
|
|
| 399 |
model: modelToUse,
|
| 400 |
e2b_key: currentSettings.e2bKey || null,
|
| 401 |
serper_key: currentSettings.serperKey || null,
|
|
|
|
|
|
|
|
|
|
| 402 |
notebook_id: tabId.toString() // Send unique tab ID for sandbox sessions
|
| 403 |
})
|
| 404 |
});
|
|
@@ -454,7 +500,7 @@ async function streamChatResponse(messages, chatContainer, notebookType, tabId)
|
|
| 454 |
updateActionWidgetWithResult(tabId, data.content, data.figures);
|
| 455 |
|
| 456 |
} else if (data.type === 'result_preview') {
|
| 457 |
-
// Show result
|
| 458 |
// Replace <figure_x> tags with placeholders BEFORE markdown processing
|
| 459 |
let previewContent = data.content;
|
| 460 |
const figurePlaceholders = {};
|
|
@@ -491,11 +537,12 @@ async function streamChatResponse(messages, chatContainer, notebookType, tabId)
|
|
| 491 |
html = html.replace(new RegExp(placeholderId, 'g'), imageHtml);
|
| 492 |
}
|
| 493 |
|
|
|
|
| 494 |
const resultDiv = document.createElement('div');
|
| 495 |
-
resultDiv.className = '
|
| 496 |
resultDiv.innerHTML = `
|
| 497 |
-
<div class="
|
| 498 |
-
<div class="
|
| 499 |
`;
|
| 500 |
chatContainer.appendChild(resultDiv);
|
| 501 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
@@ -516,10 +563,18 @@ async function streamChatResponse(messages, chatContainer, notebookType, tabId)
|
|
| 516 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 517 |
|
| 518 |
} else if (data.type === 'source') {
|
| 519 |
-
// Research source found
|
| 520 |
-
createSourceMessage(chatContainer, data
|
| 521 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 522 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
} else if (data.type === 'assessment') {
|
| 524 |
// Research completeness assessment
|
| 525 |
createAssessmentMessage(chatContainer, data.sufficient, data.missing_aspects, data.findings_count);
|
|
@@ -688,22 +743,27 @@ function updateLastCodeCell(chatContainer, output, isError, images) {
|
|
| 688 |
|
| 689 |
function showActionWidget(chatContainer, action, message, targetTabId) {
|
| 690 |
const widget = document.createElement('div');
|
| 691 |
-
widget.className = 'action-widget';
|
| 692 |
-
widget.style.cursor = 'pointer';
|
| 693 |
widget.dataset.targetTabId = targetTabId;
|
| 694 |
widget.innerHTML = `
|
| 695 |
-
<div class="action-widget-header">
|
| 696 |
<span class="action-widget-icon">→</span>
|
| 697 |
-
<span class="action-widget-text">Opening ${action.toUpperCase()} notebook
|
| 698 |
<span class="action-widget-type">${action}</span>
|
| 699 |
</div>
|
| 700 |
-
<div class="action-widget-message"
|
| 701 |
`;
|
| 702 |
|
| 703 |
-
// Make
|
| 704 |
-
widget.
|
|
|
|
|
|
|
|
|
|
| 705 |
switchToTab(parseInt(targetTabId));
|
| 706 |
-
}
|
|
|
|
|
|
|
|
|
|
| 707 |
|
| 708 |
chatContainer.appendChild(widget);
|
| 709 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
@@ -716,10 +776,13 @@ function updateActionWidgetWithResult(tabId, resultContent, figures) {
|
|
| 716 |
const widget = actionWidgets[tabId];
|
| 717 |
if (!widget) return;
|
| 718 |
|
| 719 |
-
//
|
|
|
|
|
|
|
| 720 |
const statusText = widget.querySelector('.action-widget-text');
|
| 721 |
if (statusText) {
|
| 722 |
-
|
|
|
|
| 723 |
}
|
| 724 |
|
| 725 |
// Replace <figure_x> tags with placeholders BEFORE markdown processing
|
|
@@ -805,9 +868,12 @@ function escapeHtml(text) {
|
|
| 805 |
}
|
| 806 |
|
| 807 |
function parseMarkdown(text) {
|
| 808 |
-
//
|
| 809 |
let html = escapeHtml(text);
|
| 810 |
|
|
|
|
|
|
|
|
|
|
| 811 |
// Code blocks (```language\ncode\n```)
|
| 812 |
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
| 813 |
return `<pre><code>${code.trim()}</code></pre>`;
|
|
@@ -816,22 +882,157 @@ function parseMarkdown(text) {
|
|
| 816 |
// Inline code (`code`)
|
| 817 |
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
| 818 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
// Bold (**text**)
|
| 820 |
html = html.replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>');
|
| 821 |
|
| 822 |
// Italic (*text*)
|
| 823 |
html = html.replace(/\*([^\*]+)\*/g, '<em>$1</em>');
|
| 824 |
|
| 825 |
-
//
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 830 |
}
|
| 831 |
-
return para ? `<p>${para.replace(/\n/g, '<br>')}</p>` : '';
|
| 832 |
-
}).join('\n');
|
| 833 |
|
| 834 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 835 |
}
|
| 836 |
|
| 837 |
// Settings management
|
|
@@ -860,6 +1061,9 @@ function openSettings() {
|
|
| 860 |
document.getElementById('setting-model-code').value = settings.models?.code || '';
|
| 861 |
document.getElementById('setting-model-research').value = settings.models?.research || '';
|
| 862 |
document.getElementById('setting-model-chat').value = settings.models?.chat || '';
|
|
|
|
|
|
|
|
|
|
| 863 |
|
| 864 |
// Clear any status message
|
| 865 |
const status = document.getElementById('settingsStatus');
|
|
@@ -883,6 +1087,9 @@ function saveSettings() {
|
|
| 883 |
const modelCode = document.getElementById('setting-model-code').value.trim();
|
| 884 |
const modelResearch = document.getElementById('setting-model-research').value.trim();
|
| 885 |
const modelChat = document.getElementById('setting-model-chat').value.trim();
|
|
|
|
|
|
|
|
|
|
| 886 |
|
| 887 |
// Validate endpoint
|
| 888 |
if (!endpoint) {
|
|
@@ -902,6 +1109,9 @@ function saveSettings() {
|
|
| 902 |
settings.model = model;
|
| 903 |
settings.e2bKey = e2bKey;
|
| 904 |
settings.serperKey = serperKey;
|
|
|
|
|
|
|
|
|
|
| 905 |
settings.models = {
|
| 906 |
agent: modelAgent,
|
| 907 |
code: modelCode,
|
|
|
|
| 296 |
|
| 297 |
// Remove welcome message if it exists (only on first user message)
|
| 298 |
const welcomeMsg = chatContainer.querySelector('.welcome-message');
|
| 299 |
+
const isFirstMessage = welcomeMsg !== null;
|
| 300 |
if (welcomeMsg) {
|
| 301 |
welcomeMsg.remove();
|
| 302 |
}
|
|
|
|
| 310 |
`;
|
| 311 |
chatContainer.appendChild(userMsg);
|
| 312 |
|
| 313 |
+
// Generate a title for the notebook if this is the first message and not command center
|
| 314 |
+
if (isFirstMessage && tabId !== 0) {
|
| 315 |
+
generateNotebookTitle(tabId, message);
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
// Clear input and disable it during processing
|
| 319 |
input.value = '';
|
| 320 |
input.disabled = true;
|
|
|
|
| 337 |
setTabGenerating(tabId, false);
|
| 338 |
}
|
| 339 |
|
| 340 |
+
async function generateNotebookTitle(tabId, query) {
|
| 341 |
+
const currentSettings = getSettings();
|
| 342 |
+
const backendEndpoint = 'http://localhost:8000/api';
|
| 343 |
+
const llmEndpoint = currentSettings.endpoint || 'https://api.openai.com/v1';
|
| 344 |
+
const modelToUse = currentSettings.model || 'gpt-4';
|
| 345 |
+
|
| 346 |
+
try {
|
| 347 |
+
const response = await fetch(`${backendEndpoint}/generate-title`, {
|
| 348 |
+
method: 'POST',
|
| 349 |
+
headers: { 'Content-Type': 'application/json' },
|
| 350 |
+
body: JSON.stringify({
|
| 351 |
+
query: query,
|
| 352 |
+
endpoint: llmEndpoint,
|
| 353 |
+
token: currentSettings.token || null,
|
| 354 |
+
model: modelToUse
|
| 355 |
+
})
|
| 356 |
+
});
|
| 357 |
+
|
| 358 |
+
if (response.ok) {
|
| 359 |
+
const result = await response.json();
|
| 360 |
+
const title = result.title;
|
| 361 |
+
|
| 362 |
+
// Update the tab title
|
| 363 |
+
const tab = document.querySelector(`[data-tab-id="${tabId}"]`);
|
| 364 |
+
if (tab) {
|
| 365 |
+
const titleEl = tab.querySelector('.tab-title');
|
| 366 |
+
if (titleEl) {
|
| 367 |
+
titleEl.textContent = title.toUpperCase();
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
}
|
| 371 |
+
} catch (error) {
|
| 372 |
+
console.error('Failed to generate title:', error);
|
| 373 |
+
// Don't show error to user, just keep the default title
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
function getNotebookTypeFromContainer(chatContainer) {
|
| 378 |
// Try to get type from data attribute first (for dynamically created notebooks)
|
| 379 |
const typeFromData = chatContainer.dataset.notebookType;
|
|
|
|
| 442 |
model: modelToUse,
|
| 443 |
e2b_key: currentSettings.e2bKey || null,
|
| 444 |
serper_key: currentSettings.serperKey || null,
|
| 445 |
+
research_sub_agent_model: currentSettings.researchSubAgentModel || null,
|
| 446 |
+
research_parallel_workers: currentSettings.researchParallelWorkers || null,
|
| 447 |
+
research_max_websites: currentSettings.researchMaxWebsites || null,
|
| 448 |
notebook_id: tabId.toString() // Send unique tab ID for sandbox sessions
|
| 449 |
})
|
| 450 |
});
|
|
|
|
| 500 |
updateActionWidgetWithResult(tabId, data.content, data.figures);
|
| 501 |
|
| 502 |
} else if (data.type === 'result_preview') {
|
| 503 |
+
// Show result preview
|
| 504 |
// Replace <figure_x> tags with placeholders BEFORE markdown processing
|
| 505 |
let previewContent = data.content;
|
| 506 |
const figurePlaceholders = {};
|
|
|
|
| 537 |
html = html.replace(new RegExp(placeholderId, 'g'), imageHtml);
|
| 538 |
}
|
| 539 |
|
| 540 |
+
// Create result block
|
| 541 |
const resultDiv = document.createElement('div');
|
| 542 |
+
resultDiv.className = 'research-report';
|
| 543 |
resultDiv.innerHTML = `
|
| 544 |
+
<div class="report-header">Report</div>
|
| 545 |
+
<div class="report-content">${html}</div>
|
| 546 |
`;
|
| 547 |
chatContainer.appendChild(resultDiv);
|
| 548 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
|
|
| 563 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 564 |
|
| 565 |
} else if (data.type === 'source') {
|
| 566 |
+
// Research source found - now includes query grouping
|
| 567 |
+
createSourceMessage(chatContainer, data);
|
| 568 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 569 |
|
| 570 |
+
} else if (data.type === 'query_stats') {
|
| 571 |
+
// Update query statistics
|
| 572 |
+
updateQueryStats(data.query_index, {
|
| 573 |
+
relevant: data.relevant_count,
|
| 574 |
+
irrelevant: data.irrelevant_count,
|
| 575 |
+
error: data.error_count
|
| 576 |
+
});
|
| 577 |
+
|
| 578 |
} else if (data.type === 'assessment') {
|
| 579 |
// Research completeness assessment
|
| 580 |
createAssessmentMessage(chatContainer, data.sufficient, data.missing_aspects, data.findings_count);
|
|
|
|
| 743 |
|
| 744 |
function showActionWidget(chatContainer, action, message, targetTabId) {
|
| 745 |
const widget = document.createElement('div');
|
| 746 |
+
widget.className = 'action-widget running'; // Add 'running' class for animation
|
|
|
|
| 747 |
widget.dataset.targetTabId = targetTabId;
|
| 748 |
widget.innerHTML = `
|
| 749 |
+
<div class="action-widget-header" style="cursor: pointer;">
|
| 750 |
<span class="action-widget-icon">→</span>
|
| 751 |
+
<span class="action-widget-text">Opening ${action.toUpperCase()} notebook</span>
|
| 752 |
<span class="action-widget-type">${action}</span>
|
| 753 |
</div>
|
| 754 |
+
<div class="action-widget-message" style="cursor: pointer;">${escapeHtml(message)}</div>
|
| 755 |
`;
|
| 756 |
|
| 757 |
+
// Make header and message clickable to jump to the notebook
|
| 758 |
+
const header = widget.querySelector('.action-widget-header');
|
| 759 |
+
const messageEl = widget.querySelector('.action-widget-message');
|
| 760 |
+
|
| 761 |
+
const clickHandler = () => {
|
| 762 |
switchToTab(parseInt(targetTabId));
|
| 763 |
+
};
|
| 764 |
+
|
| 765 |
+
header.addEventListener('click', clickHandler);
|
| 766 |
+
messageEl.addEventListener('click', clickHandler);
|
| 767 |
|
| 768 |
chatContainer.appendChild(widget);
|
| 769 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
|
|
| 776 |
const widget = actionWidgets[tabId];
|
| 777 |
if (!widget) return;
|
| 778 |
|
| 779 |
+
// Remove running animation and update status text
|
| 780 |
+
widget.classList.remove('running');
|
| 781 |
+
|
| 782 |
const statusText = widget.querySelector('.action-widget-text');
|
| 783 |
if (statusText) {
|
| 784 |
+
// Remove trailing dots and change text
|
| 785 |
+
statusText.textContent = statusText.textContent.replace('Opening', 'Completed').replace(/\.+$/, '');
|
| 786 |
}
|
| 787 |
|
| 788 |
// Replace <figure_x> tags with placeholders BEFORE markdown processing
|
|
|
|
| 868 |
}
|
| 869 |
|
| 870 |
function parseMarkdown(text) {
|
| 871 |
+
// Comprehensive markdown parser
|
| 872 |
let html = escapeHtml(text);
|
| 873 |
|
| 874 |
+
// Remove any literal <br> tags that came through (convert to spaces in tables, newlines elsewhere)
|
| 875 |
+
// We'll handle this more carefully after table processing
|
| 876 |
+
|
| 877 |
// Code blocks (```language\ncode\n```)
|
| 878 |
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
| 879 |
return `<pre><code>${code.trim()}</code></pre>`;
|
|
|
|
| 882 |
// Inline code (`code`)
|
| 883 |
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
| 884 |
|
| 885 |
+
// Links [text](url) - process before bold/italic to avoid conflicts
|
| 886 |
+
html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, (match, text, url) => {
|
| 887 |
+
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
| 888 |
+
});
|
| 889 |
+
|
| 890 |
// Bold (**text**)
|
| 891 |
html = html.replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>');
|
| 892 |
|
| 893 |
// Italic (*text*)
|
| 894 |
html = html.replace(/\*([^\*]+)\*/g, '<em>$1</em>');
|
| 895 |
|
| 896 |
+
// Tables (before paragraph processing)
|
| 897 |
+
// First, clean up <br> tags in table cells
|
| 898 |
+
// Changed + to * to make data rows optional (allows tables with just headers)
|
| 899 |
+
html = html.replace(/(\|.+\|[\r\n]+)(\|[\s:|-]+\|[\r\n]+)((?:\|.+\|[\r\n]*)*)/g, (match, header, separator, rows) => {
|
| 900 |
+
// Clean escaped <br> tags in header and rows
|
| 901 |
+
const cleanedHeader = header.replace(/<br>/gi, ' ');
|
| 902 |
+
const cleanedRows = rows.replace(/<br>/gi, ' ');
|
| 903 |
+
|
| 904 |
+
// Split header and keep empty cells (don't filter them out)
|
| 905 |
+
const headerParts = cleanedHeader.trim().split('|');
|
| 906 |
+
// Remove first and last empty elements (from leading/trailing |)
|
| 907 |
+
if (headerParts[0].trim() === '') headerParts.shift();
|
| 908 |
+
if (headerParts[headerParts.length - 1].trim() === '') headerParts.pop();
|
| 909 |
+
const headerCells = headerParts.map(cell =>
|
| 910 |
+
`<th>${cell.trim()}</th>`
|
| 911 |
+
).join('');
|
| 912 |
+
|
| 913 |
+
// Only process rows if they exist
|
| 914 |
+
let rowsHtml = '';
|
| 915 |
+
if (cleanedRows.trim()) {
|
| 916 |
+
rowsHtml = cleanedRows.trim().split('\n').map(row => {
|
| 917 |
+
const rowParts = row.trim().split('|');
|
| 918 |
+
// Remove first and last empty elements (from leading/trailing |)
|
| 919 |
+
if (rowParts[0].trim() === '') rowParts.shift();
|
| 920 |
+
if (rowParts[rowParts.length - 1].trim() === '') rowParts.pop();
|
| 921 |
+
const cells = rowParts.map(cell =>
|
| 922 |
+
`<td>${cell.trim()}</td>`
|
| 923 |
+
).join('');
|
| 924 |
+
return cells ? `<tr>${cells}</tr>` : '';
|
| 925 |
+
}).filter(row => row).join('');
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
// Add newline after table to preserve line breaks with following content
|
| 929 |
+
return `<table class="markdown-table"><thead><tr>${headerCells}</tr></thead><tbody>${rowsHtml}</tbody></table>\n`;
|
| 930 |
+
});
|
| 931 |
+
|
| 932 |
+
// Now clean up any remaining <br> tags (convert to newlines for paragraph processing)
|
| 933 |
+
html = html.replace(/<br>/gi, '\n');
|
| 934 |
+
|
| 935 |
+
// Split into lines for processing
|
| 936 |
+
const lines = html.split('\n');
|
| 937 |
+
const processedLines = [];
|
| 938 |
+
let i = 0;
|
| 939 |
+
|
| 940 |
+
while (i < lines.length) {
|
| 941 |
+
const line = lines[i].trim();
|
| 942 |
+
|
| 943 |
+
// Skip empty lines
|
| 944 |
+
if (!line) {
|
| 945 |
+
i++;
|
| 946 |
+
continue;
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
// Skip horizontal rules / separator lines (lines with only dashes, possibly with spaces)
|
| 950 |
+
if (line.match(/^[\s-]+$/)) {
|
| 951 |
+
i++;
|
| 952 |
+
continue;
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
// Don't process special elements
|
| 956 |
+
if (line.startsWith('<pre>') || line.startsWith('<table') || line.startsWith('__FIGURE_')) {
|
| 957 |
+
processedLines.push(line);
|
| 958 |
+
i++;
|
| 959 |
+
continue;
|
| 960 |
+
}
|
| 961 |
+
|
| 962 |
+
// Check for headings (must start with ###, ##, or # followed by space or content)
|
| 963 |
+
const h3Match = line.match(/^###\s*(.*)$/);
|
| 964 |
+
if (h3Match) {
|
| 965 |
+
processedLines.push(`<h3>${h3Match[1]}</h3>`);
|
| 966 |
+
i++;
|
| 967 |
+
continue;
|
| 968 |
+
}
|
| 969 |
+
|
| 970 |
+
const h2Match = line.match(/^##\s*(.*)$/);
|
| 971 |
+
if (h2Match) {
|
| 972 |
+
processedLines.push(`<h2>${h2Match[1]}</h2>`);
|
| 973 |
+
i++;
|
| 974 |
+
continue;
|
| 975 |
}
|
|
|
|
|
|
|
| 976 |
|
| 977 |
+
const h1Match = line.match(/^#\s*(.*)$/);
|
| 978 |
+
if (h1Match) {
|
| 979 |
+
processedLines.push(`<h1>${h1Match[1]}</h1>`);
|
| 980 |
+
i++;
|
| 981 |
+
continue;
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
// Check for unordered list
|
| 985 |
+
if (line.match(/^[-*+]\s/)) {
|
| 986 |
+
const listItems = [];
|
| 987 |
+
while (i < lines.length && lines[i].trim().match(/^[-*+]\s/)) {
|
| 988 |
+
const match = lines[i].trim().match(/^[-*+]\s(.+)$/);
|
| 989 |
+
if (match) listItems.push(`<li>${match[1]}</li>`);
|
| 990 |
+
i++;
|
| 991 |
+
}
|
| 992 |
+
processedLines.push(`<ul>${listItems.join('')}</ul>`);
|
| 993 |
+
continue;
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
// Check for ordered list
|
| 997 |
+
if (line.match(/^\d+\.\s/)) {
|
| 998 |
+
const listItems = [];
|
| 999 |
+
while (i < lines.length && lines[i].trim().match(/^\d+\.\s/)) {
|
| 1000 |
+
const match = lines[i].trim().match(/^\d+\.\s(.+)$/);
|
| 1001 |
+
if (match) listItems.push(`<li>${match[1]}</li>`);
|
| 1002 |
+
i++;
|
| 1003 |
+
}
|
| 1004 |
+
processedLines.push(`<ol>${listItems.join('')}</ol>`);
|
| 1005 |
+
continue;
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
+
// Collect paragraph lines (until we hit a blank line or special element)
|
| 1009 |
+
const paragraphLines = [];
|
| 1010 |
+
while (i < lines.length) {
|
| 1011 |
+
const currentLine = lines[i].trim();
|
| 1012 |
+
|
| 1013 |
+
// Stop at blank line
|
| 1014 |
+
if (!currentLine) break;
|
| 1015 |
+
|
| 1016 |
+
// Stop at special elements
|
| 1017 |
+
if (currentLine.startsWith('<pre>') ||
|
| 1018 |
+
currentLine.startsWith('<table') ||
|
| 1019 |
+
currentLine.startsWith('__FIGURE_') ||
|
| 1020 |
+
currentLine.match(/^#{1,3}\s*/) ||
|
| 1021 |
+
currentLine.match(/^[-*+]\s/) ||
|
| 1022 |
+
currentLine.match(/^\d+\.\s/)) {
|
| 1023 |
+
break;
|
| 1024 |
+
}
|
| 1025 |
+
|
| 1026 |
+
paragraphLines.push(currentLine);
|
| 1027 |
+
i++;
|
| 1028 |
+
}
|
| 1029 |
+
|
| 1030 |
+
if (paragraphLines.length > 0) {
|
| 1031 |
+
processedLines.push(`<p>${paragraphLines.join(' ')}</p>`);
|
| 1032 |
+
}
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
return processedLines.join('\n');
|
| 1036 |
}
|
| 1037 |
|
| 1038 |
// Settings management
|
|
|
|
| 1061 |
document.getElementById('setting-model-code').value = settings.models?.code || '';
|
| 1062 |
document.getElementById('setting-model-research').value = settings.models?.research || '';
|
| 1063 |
document.getElementById('setting-model-chat').value = settings.models?.chat || '';
|
| 1064 |
+
document.getElementById('setting-research-sub-agent-model').value = settings.researchSubAgentModel || '';
|
| 1065 |
+
document.getElementById('setting-research-parallel-workers').value = settings.researchParallelWorkers || '';
|
| 1066 |
+
document.getElementById('setting-research-max-websites').value = settings.researchMaxWebsites || '';
|
| 1067 |
|
| 1068 |
// Clear any status message
|
| 1069 |
const status = document.getElementById('settingsStatus');
|
|
|
|
| 1087 |
const modelCode = document.getElementById('setting-model-code').value.trim();
|
| 1088 |
const modelResearch = document.getElementById('setting-model-research').value.trim();
|
| 1089 |
const modelChat = document.getElementById('setting-model-chat').value.trim();
|
| 1090 |
+
const researchSubAgentModel = document.getElementById('setting-research-sub-agent-model').value.trim();
|
| 1091 |
+
const researchParallelWorkers = document.getElementById('setting-research-parallel-workers').value.trim();
|
| 1092 |
+
const researchMaxWebsites = document.getElementById('setting-research-max-websites').value.trim();
|
| 1093 |
|
| 1094 |
// Validate endpoint
|
| 1095 |
if (!endpoint) {
|
|
|
|
| 1109 |
settings.model = model;
|
| 1110 |
settings.e2bKey = e2bKey;
|
| 1111 |
settings.serperKey = serperKey;
|
| 1112 |
+
settings.researchSubAgentModel = researchSubAgentModel;
|
| 1113 |
+
settings.researchParallelWorkers = researchParallelWorkers ? parseInt(researchParallelWorkers) : null;
|
| 1114 |
+
settings.researchMaxWebsites = researchMaxWebsites ? parseInt(researchMaxWebsites) : null;
|
| 1115 |
settings.models = {
|
| 1116 |
agent: modelAgent,
|
| 1117 |
code: modelCode,
|
style.css
CHANGED
|
@@ -245,7 +245,7 @@ body {
|
|
| 245 |
background: #f5f5f5;
|
| 246 |
padding: 15px 20px;
|
| 247 |
border-bottom: 1px solid #ccc;
|
| 248 |
-
display:
|
| 249 |
justify-content: space-between;
|
| 250 |
align-items: center;
|
| 251 |
gap: 20px;
|
|
@@ -280,9 +280,10 @@ body {
|
|
| 280 |
}
|
| 281 |
|
| 282 |
.chat-container {
|
| 283 |
-
max-width:
|
|
|
|
| 284 |
margin: 0 auto;
|
| 285 |
-
font-size:
|
| 286 |
}
|
| 287 |
|
| 288 |
.jupyter-notebook-container {
|
|
@@ -354,6 +355,43 @@ body {
|
|
| 354 |
word-wrap: break-word;
|
| 355 |
}
|
| 356 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
.message-content code {
|
| 358 |
background: #e0e0e0;
|
| 359 |
padding: 2px 6px;
|
|
@@ -685,27 +723,39 @@ body {
|
|
| 685 |
/* Action Widget */
|
| 686 |
.action-widget {
|
| 687 |
margin: 8px 0;
|
| 688 |
-
padding: 10px 12px;
|
| 689 |
-
background: #e8f5e9;
|
| 690 |
border: 1px solid #1b5e20;
|
| 691 |
border-left: 4px solid #1b5e20;
|
| 692 |
border-radius: 4px;
|
| 693 |
font-size: 12px;
|
| 694 |
display: flex;
|
| 695 |
flex-direction: column;
|
| 696 |
-
gap: 6px;
|
| 697 |
transition: all 0.2s;
|
|
|
|
| 698 |
}
|
| 699 |
|
| 700 |
-
.action-widget
|
| 701 |
-
|
| 702 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
}
|
| 704 |
|
| 705 |
.action-widget-header {
|
| 706 |
display: flex;
|
| 707 |
align-items: center;
|
| 708 |
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
}
|
| 710 |
|
| 711 |
.action-widget-icon {
|
|
@@ -729,19 +779,53 @@ body {
|
|
| 729 |
.action-widget-message {
|
| 730 |
font-size: 10px;
|
| 731 |
color: #666;
|
| 732 |
-
padding: 6px 8px;
|
| 733 |
-
background:
|
| 734 |
-
border-radius: 3px;
|
| 735 |
font-style: italic;
|
| 736 |
line-height: 1.4;
|
| 737 |
}
|
| 738 |
|
| 739 |
.action-widget-result {
|
| 740 |
-
|
| 741 |
-
padding: 10px 0;
|
| 742 |
font-size: 12px;
|
| 743 |
line-height: 1.6;
|
| 744 |
color: #1a1a1a;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 745 |
}
|
| 746 |
|
| 747 |
.action-widget-result-header {
|
|
@@ -818,148 +902,192 @@ body {
|
|
| 818 |
}
|
| 819 |
|
| 820 |
/* Research Notebook Styles */
|
| 821 |
-
.research-
|
| 822 |
margin: 16px 0;
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
border: 1px solid #1976d2;
|
| 826 |
-
border-left: 4px solid #1976d2;
|
| 827 |
border-radius: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 828 |
display: flex;
|
|
|
|
| 829 |
align-items: center;
|
| 830 |
-
|
|
|
|
|
|
|
| 831 |
font-size: 13px;
|
|
|
|
|
|
|
| 832 |
}
|
| 833 |
|
| 834 |
-
.
|
| 835 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
}
|
| 837 |
|
| 838 |
-
.
|
| 839 |
-
|
| 840 |
-
color: #
|
| 841 |
-
line-height: 1.5;
|
| 842 |
}
|
| 843 |
|
| 844 |
-
.
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 850 |
font-weight: 500;
|
| 851 |
-
|
|
|
|
| 852 |
}
|
| 853 |
|
| 854 |
-
.
|
| 855 |
-
margin:
|
| 856 |
-
|
| 857 |
-
background: #f5f5f5;
|
| 858 |
-
border: 1px solid #ccc;
|
| 859 |
border-radius: 4px;
|
|
|
|
| 860 |
}
|
| 861 |
|
| 862 |
-
.
|
| 863 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 864 |
font-weight: 500;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 865 |
color: #666;
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
margin-bottom: 10px;
|
| 869 |
}
|
| 870 |
|
| 871 |
-
.
|
| 872 |
-
|
| 873 |
-
padding: 0;
|
| 874 |
-
margin: 0;
|
| 875 |
}
|
| 876 |
|
| 877 |
-
.
|
| 878 |
-
padding:
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
font-size: 12px;
|
| 884 |
-
color: #1a1a1a;
|
| 885 |
}
|
| 886 |
|
| 887 |
-
.
|
| 888 |
-
margin-bottom:
|
| 889 |
}
|
| 890 |
|
| 891 |
-
.research-progress {
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
background: #fff3e0;
|
| 895 |
-
border: 1px solid #f57c00;
|
| 896 |
-
border-left: 4px solid #f57c00;
|
| 897 |
-
border-radius: 4px;
|
| 898 |
}
|
| 899 |
|
| 900 |
-
.progress-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
}
|
| 905 |
|
| 906 |
.progress-bar {
|
|
|
|
| 907 |
height: 6px;
|
| 908 |
-
background: #
|
| 909 |
border-radius: 3px;
|
| 910 |
overflow: hidden;
|
| 911 |
-
margin-bottom: 6px;
|
| 912 |
}
|
| 913 |
|
| 914 |
.progress-fill {
|
| 915 |
height: 100%;
|
| 916 |
-
background: #
|
| 917 |
transition: width 0.3s ease;
|
| 918 |
}
|
| 919 |
|
| 920 |
-
.progress-
|
| 921 |
font-size: 11px;
|
| 922 |
color: #666;
|
| 923 |
-
|
| 924 |
}
|
| 925 |
|
| 926 |
.research-source {
|
| 927 |
-
margin:
|
| 928 |
-
|
|
|
|
| 929 |
background: white;
|
| 930 |
-
border: 1px solid #ccc;
|
| 931 |
-
border-radius: 4px;
|
| 932 |
-
transition: all 0.2s;
|
| 933 |
}
|
| 934 |
|
| 935 |
-
.research-source
|
| 936 |
-
|
| 937 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 938 |
}
|
| 939 |
|
| 940 |
.source-header {
|
| 941 |
display: flex;
|
| 942 |
align-items: center;
|
| 943 |
-
gap:
|
| 944 |
-
|
|
|
|
|
|
|
| 945 |
}
|
| 946 |
|
| 947 |
-
.source-
|
| 948 |
-
background: #
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
font-size:
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 956 |
}
|
| 957 |
|
| 958 |
.source-url {
|
| 959 |
color: #1976d2;
|
| 960 |
text-decoration: none;
|
| 961 |
font-size: 12px;
|
| 962 |
-
font-weight: 500;
|
| 963 |
flex: 1;
|
| 964 |
overflow: hidden;
|
| 965 |
text-overflow: ellipsis;
|
|
@@ -970,14 +1098,53 @@ body {
|
|
| 970 |
text-decoration: underline;
|
| 971 |
}
|
| 972 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
.source-analysis {
|
| 974 |
font-size: 12px;
|
| 975 |
line-height: 1.6;
|
| 976 |
-
color: #
|
| 977 |
-
padding:
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 981 |
}
|
| 982 |
|
| 983 |
.research-assessment {
|
|
@@ -1048,36 +1215,24 @@ body {
|
|
| 1048 |
}
|
| 1049 |
|
| 1050 |
.research-report {
|
| 1051 |
-
margin:
|
| 1052 |
-
padding: 20px;
|
| 1053 |
background: white;
|
| 1054 |
-
border: 1px solid #
|
| 1055 |
border-radius: 4px;
|
|
|
|
| 1056 |
}
|
| 1057 |
|
| 1058 |
.report-header {
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
margin-bottom: 15px;
|
| 1064 |
-
border-bottom: 2px solid #1b5e20;
|
| 1065 |
-
}
|
| 1066 |
-
|
| 1067 |
-
.report-title {
|
| 1068 |
-
font-size: 16px;
|
| 1069 |
-
font-weight: 500;
|
| 1070 |
-
color: #1b5e20;
|
| 1071 |
-
letter-spacing: 0.5px;
|
| 1072 |
-
}
|
| 1073 |
-
|
| 1074 |
-
.report-stats {
|
| 1075 |
-
font-size: 11px;
|
| 1076 |
-
color: #666;
|
| 1077 |
font-weight: 500;
|
|
|
|
| 1078 |
}
|
| 1079 |
|
| 1080 |
.report-content {
|
|
|
|
| 1081 |
font-size: 13px;
|
| 1082 |
line-height: 1.8;
|
| 1083 |
color: #1a1a1a;
|
|
@@ -1123,7 +1278,57 @@ body {
|
|
| 1123 |
color: #1a1a1a;
|
| 1124 |
}
|
| 1125 |
|
| 1126 |
-
.report-content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1127 |
background: #f5f5f5;
|
| 1128 |
padding: 2px 6px;
|
| 1129 |
border-radius: 3px;
|
|
|
|
| 245 |
background: #f5f5f5;
|
| 246 |
padding: 15px 20px;
|
| 247 |
border-bottom: 1px solid #ccc;
|
| 248 |
+
display: none; /* Hidden for cleaner UI */
|
| 249 |
justify-content: space-between;
|
| 250 |
align-items: center;
|
| 251 |
gap: 20px;
|
|
|
|
| 280 |
}
|
| 281 |
|
| 282 |
.chat-container {
|
| 283 |
+
max-width: 1000px;
|
| 284 |
+
width: 100%;
|
| 285 |
margin: 0 auto;
|
| 286 |
+
font-size: 12px;
|
| 287 |
}
|
| 288 |
|
| 289 |
.jupyter-notebook-container {
|
|
|
|
| 355 |
word-wrap: break-word;
|
| 356 |
}
|
| 357 |
|
| 358 |
+
.message-content ul,
|
| 359 |
+
.message-content ol {
|
| 360 |
+
margin: 8px 0;
|
| 361 |
+
padding-left: 24px;
|
| 362 |
+
list-style-position: outside;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
.message-content li {
|
| 366 |
+
margin-bottom: 4px;
|
| 367 |
+
white-space: normal;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.message-content h1,
|
| 371 |
+
.message-content h2,
|
| 372 |
+
.message-content h3 {
|
| 373 |
+
margin: 12px 0 8px 0;
|
| 374 |
+
font-weight: 500;
|
| 375 |
+
white-space: normal;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
.message-content h1 {
|
| 379 |
+
font-size: 16px;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.message-content h2 {
|
| 383 |
+
font-size: 15px;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.message-content h3 {
|
| 387 |
+
font-size: 14px;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.message-content p {
|
| 391 |
+
margin-bottom: 8px;
|
| 392 |
+
white-space: pre-wrap;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
.message-content code {
|
| 396 |
background: #e0e0e0;
|
| 397 |
padding: 2px 6px;
|
|
|
|
| 723 |
/* Action Widget */
|
| 724 |
.action-widget {
|
| 725 |
margin: 8px 0;
|
|
|
|
|
|
|
| 726 |
border: 1px solid #1b5e20;
|
| 727 |
border-left: 4px solid #1b5e20;
|
| 728 |
border-radius: 4px;
|
| 729 |
font-size: 12px;
|
| 730 |
display: flex;
|
| 731 |
flex-direction: column;
|
|
|
|
| 732 |
transition: all 0.2s;
|
| 733 |
+
overflow: hidden;
|
| 734 |
}
|
| 735 |
|
| 736 |
+
.action-widget.running {
|
| 737 |
+
animation: pulse-border 2s ease-in-out infinite;
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
@keyframes pulse-border {
|
| 741 |
+
0%, 100% {
|
| 742 |
+
border-left-color: #1b5e20;
|
| 743 |
+
}
|
| 744 |
+
50% {
|
| 745 |
+
border-left-color: #4caf50;
|
| 746 |
+
}
|
| 747 |
}
|
| 748 |
|
| 749 |
.action-widget-header {
|
| 750 |
display: flex;
|
| 751 |
align-items: center;
|
| 752 |
gap: 8px;
|
| 753 |
+
padding: 10px 12px;
|
| 754 |
+
background: #e8f5e9;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
.action-widget-header:hover {
|
| 758 |
+
background: #c8e6c9;
|
| 759 |
}
|
| 760 |
|
| 761 |
.action-widget-icon {
|
|
|
|
| 779 |
.action-widget-message {
|
| 780 |
font-size: 10px;
|
| 781 |
color: #666;
|
| 782 |
+
padding: 6px 8px 10px 12px;
|
| 783 |
+
background: #e8f5e9;
|
|
|
|
| 784 |
font-style: italic;
|
| 785 |
line-height: 1.4;
|
| 786 |
}
|
| 787 |
|
| 788 |
.action-widget-result {
|
| 789 |
+
padding: 12px;
|
|
|
|
| 790 |
font-size: 12px;
|
| 791 |
line-height: 1.6;
|
| 792 |
color: #1a1a1a;
|
| 793 |
+
background: white;
|
| 794 |
+
border-top: 1px solid #e0e0e0;
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
.action-widget-result ul,
|
| 798 |
+
.action-widget-result ol {
|
| 799 |
+
margin: 8px 0;
|
| 800 |
+
padding-left: 24px;
|
| 801 |
+
list-style-position: outside;
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
.action-widget-result li {
|
| 805 |
+
margin-bottom: 4px;
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
.action-widget-result h1,
|
| 809 |
+
.action-widget-result h2,
|
| 810 |
+
.action-widget-result h3 {
|
| 811 |
+
margin: 12px 0 8px 0;
|
| 812 |
+
font-weight: 500;
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
.action-widget-result h1 {
|
| 816 |
+
font-size: 14px;
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
.action-widget-result h2 {
|
| 820 |
+
font-size: 13px;
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
.action-widget-result h3 {
|
| 824 |
+
font-size: 12px;
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
.action-widget-result p {
|
| 828 |
+
margin-bottom: 8px;
|
| 829 |
}
|
| 830 |
|
| 831 |
.action-widget-result-header {
|
|
|
|
| 902 |
}
|
| 903 |
|
| 904 |
/* Research Notebook Styles */
|
| 905 |
+
.research-container {
|
| 906 |
margin: 16px 0;
|
| 907 |
+
background: white;
|
| 908 |
+
border: 1px solid #e0e0e0;
|
|
|
|
|
|
|
| 909 |
border-radius: 4px;
|
| 910 |
+
overflow: hidden;
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
.research-header {
|
| 914 |
+
padding: 12px 16px;
|
| 915 |
+
background: #f8f8f8;
|
| 916 |
+
border-bottom: 1px solid #e0e0e0;
|
| 917 |
display: flex;
|
| 918 |
+
justify-content: space-between;
|
| 919 |
align-items: center;
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
.research-title {
|
| 923 |
font-size: 13px;
|
| 924 |
+
font-weight: 500;
|
| 925 |
+
color: #333;
|
| 926 |
}
|
| 927 |
|
| 928 |
+
.toggle-irrelevant-btn {
|
| 929 |
+
padding: 6px 12px;
|
| 930 |
+
background: white;
|
| 931 |
+
border: 1px solid #ccc;
|
| 932 |
+
border-radius: 3px;
|
| 933 |
+
font-size: 11px;
|
| 934 |
+
cursor: pointer;
|
| 935 |
+
transition: all 0.2s;
|
| 936 |
}
|
| 937 |
|
| 938 |
+
.toggle-irrelevant-btn:hover {
|
| 939 |
+
background: #f5f5f5;
|
| 940 |
+
border-color: #999;
|
|
|
|
| 941 |
}
|
| 942 |
|
| 943 |
+
.research-body {
|
| 944 |
+
padding: 16px;
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
.research-queries-section {
|
| 948 |
+
margin-bottom: 16px;
|
| 949 |
+
}
|
| 950 |
+
|
| 951 |
+
.queries-title {
|
| 952 |
+
font-size: 12px;
|
| 953 |
font-weight: 500;
|
| 954 |
+
color: #666;
|
| 955 |
+
margin-bottom: 12px;
|
| 956 |
}
|
| 957 |
|
| 958 |
+
.query-group {
|
| 959 |
+
margin-bottom: 20px;
|
| 960 |
+
border: 1px solid #e0e0e0;
|
|
|
|
|
|
|
| 961 |
border-radius: 4px;
|
| 962 |
+
background: white;
|
| 963 |
}
|
| 964 |
|
| 965 |
+
.query-group:last-child {
|
| 966 |
+
margin-bottom: 0;
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
.query-header {
|
| 970 |
+
padding: 10px 12px;
|
| 971 |
+
background: #f8f8f8;
|
| 972 |
+
border-bottom: 1px solid #e0e0e0;
|
| 973 |
+
display: flex;
|
| 974 |
+
justify-content: space-between;
|
| 975 |
+
align-items: center;
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
.query-text {
|
| 979 |
+
font-size: 12px;
|
| 980 |
+
color: #333;
|
| 981 |
font-weight: 500;
|
| 982 |
+
flex: 1;
|
| 983 |
+
}
|
| 984 |
+
|
| 985 |
+
.query-stats {
|
| 986 |
+
font-size: 11px;
|
| 987 |
color: #666;
|
| 988 |
+
white-space: nowrap;
|
| 989 |
+
margin-left: 12px;
|
|
|
|
| 990 |
}
|
| 991 |
|
| 992 |
+
.query-sources {
|
| 993 |
+
padding: 8px;
|
|
|
|
|
|
|
| 994 |
}
|
| 995 |
|
| 996 |
+
.no-sources {
|
| 997 |
+
padding: 12px;
|
| 998 |
+
text-align: center;
|
| 999 |
+
color: #999;
|
| 1000 |
+
font-size: 11px;
|
| 1001 |
+
font-style: italic;
|
|
|
|
|
|
|
| 1002 |
}
|
| 1003 |
|
| 1004 |
+
.research-sources-section {
|
| 1005 |
+
margin-bottom: 16px;
|
| 1006 |
}
|
| 1007 |
|
| 1008 |
+
.research-progress-section {
|
| 1009 |
+
padding-top: 12px;
|
| 1010 |
+
border-top: 1px solid #f0f0f0;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1011 |
}
|
| 1012 |
|
| 1013 |
+
.progress-bar-container {
|
| 1014 |
+
display: flex;
|
| 1015 |
+
align-items: center;
|
| 1016 |
+
gap: 12px;
|
| 1017 |
}
|
| 1018 |
|
| 1019 |
.progress-bar {
|
| 1020 |
+
flex: 1;
|
| 1021 |
height: 6px;
|
| 1022 |
+
background: #e0e0e0;
|
| 1023 |
border-radius: 3px;
|
| 1024 |
overflow: hidden;
|
|
|
|
| 1025 |
}
|
| 1026 |
|
| 1027 |
.progress-fill {
|
| 1028 |
height: 100%;
|
| 1029 |
+
background: #4caf50;
|
| 1030 |
transition: width 0.3s ease;
|
| 1031 |
}
|
| 1032 |
|
| 1033 |
+
.progress-text {
|
| 1034 |
font-size: 11px;
|
| 1035 |
color: #666;
|
| 1036 |
+
white-space: nowrap;
|
| 1037 |
}
|
| 1038 |
|
| 1039 |
.research-source {
|
| 1040 |
+
margin: 6px 0;
|
| 1041 |
+
border: 1px solid #e0e0e0;
|
| 1042 |
+
border-radius: 3px;
|
| 1043 |
background: white;
|
|
|
|
|
|
|
|
|
|
| 1044 |
}
|
| 1045 |
|
| 1046 |
+
.research-source.irrelevant {
|
| 1047 |
+
opacity: 0.6;
|
| 1048 |
+
background: #fafafa;
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
.research-source.error {
|
| 1052 |
+
background: #fff5f5;
|
| 1053 |
+
border-color: #ffcdd2;
|
| 1054 |
}
|
| 1055 |
|
| 1056 |
.source-header {
|
| 1057 |
display: flex;
|
| 1058 |
align-items: center;
|
| 1059 |
+
gap: 8px;
|
| 1060 |
+
padding: 8px 10px;
|
| 1061 |
+
cursor: pointer;
|
| 1062 |
+
user-select: none;
|
| 1063 |
}
|
| 1064 |
|
| 1065 |
+
.source-header:hover {
|
| 1066 |
+
background: #f8f8f8;
|
| 1067 |
+
}
|
| 1068 |
+
|
| 1069 |
+
.source-status-icon {
|
| 1070 |
+
font-size: 12px;
|
| 1071 |
+
min-width: 16px;
|
| 1072 |
+
text-align: center;
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
.research-source.relevant .source-status-icon {
|
| 1076 |
+
color: #2e7d32;
|
| 1077 |
+
}
|
| 1078 |
+
|
| 1079 |
+
.research-source.irrelevant .source-status-icon {
|
| 1080 |
+
color: #999;
|
| 1081 |
+
}
|
| 1082 |
+
|
| 1083 |
+
.research-source.error .source-status-icon {
|
| 1084 |
+
color: #d32f2f;
|
| 1085 |
}
|
| 1086 |
|
| 1087 |
.source-url {
|
| 1088 |
color: #1976d2;
|
| 1089 |
text-decoration: none;
|
| 1090 |
font-size: 12px;
|
|
|
|
| 1091 |
flex: 1;
|
| 1092 |
overflow: hidden;
|
| 1093 |
text-overflow: ellipsis;
|
|
|
|
| 1098 |
text-decoration: underline;
|
| 1099 |
}
|
| 1100 |
|
| 1101 |
+
.source-toggle {
|
| 1102 |
+
color: #999;
|
| 1103 |
+
font-size: 10px;
|
| 1104 |
+
}
|
| 1105 |
+
|
| 1106 |
.source-analysis {
|
| 1107 |
font-size: 12px;
|
| 1108 |
line-height: 1.6;
|
| 1109 |
+
color: #333;
|
| 1110 |
+
padding: 0 12px 12px 12px;
|
| 1111 |
+
border-top: 1px solid #e0e0e0;
|
| 1112 |
+
overflow-wrap: break-word;
|
| 1113 |
+
word-wrap: break-word;
|
| 1114 |
+
}
|
| 1115 |
+
|
| 1116 |
+
.source-analysis ul,
|
| 1117 |
+
.source-analysis ol {
|
| 1118 |
+
margin: 8px 0;
|
| 1119 |
+
padding-left: 24px;
|
| 1120 |
+
list-style-position: outside;
|
| 1121 |
+
}
|
| 1122 |
+
|
| 1123 |
+
.source-analysis li {
|
| 1124 |
+
margin-bottom: 4px;
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
.source-analysis h1,
|
| 1128 |
+
.source-analysis h2,
|
| 1129 |
+
.source-analysis h3 {
|
| 1130 |
+
margin: 12px 0 8px 0;
|
| 1131 |
+
font-weight: 500;
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
.source-analysis h1 {
|
| 1135 |
+
font-size: 15px;
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
.source-analysis h2 {
|
| 1139 |
+
font-size: 14px;
|
| 1140 |
+
}
|
| 1141 |
+
|
| 1142 |
+
.source-analysis h3 {
|
| 1143 |
+
font-size: 13px;
|
| 1144 |
+
}
|
| 1145 |
+
|
| 1146 |
+
.source-analysis p {
|
| 1147 |
+
margin-bottom: 8px;
|
| 1148 |
}
|
| 1149 |
|
| 1150 |
.research-assessment {
|
|
|
|
| 1215 |
}
|
| 1216 |
|
| 1217 |
.research-report {
|
| 1218 |
+
margin: 16px 0;
|
|
|
|
| 1219 |
background: white;
|
| 1220 |
+
border: 1px solid #e0e0e0;
|
| 1221 |
border-radius: 4px;
|
| 1222 |
+
overflow: hidden;
|
| 1223 |
}
|
| 1224 |
|
| 1225 |
.report-header {
|
| 1226 |
+
padding: 12px 16px;
|
| 1227 |
+
background: #f8f8f8;
|
| 1228 |
+
border-bottom: 1px solid #e0e0e0;
|
| 1229 |
+
font-size: 13px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1230 |
font-weight: 500;
|
| 1231 |
+
color: #333;
|
| 1232 |
}
|
| 1233 |
|
| 1234 |
.report-content {
|
| 1235 |
+
padding: 16px;
|
| 1236 |
font-size: 13px;
|
| 1237 |
line-height: 1.8;
|
| 1238 |
color: #1a1a1a;
|
|
|
|
| 1278 |
color: #1a1a1a;
|
| 1279 |
}
|
| 1280 |
|
| 1281 |
+
.report-content a,
|
| 1282 |
+
.source-analysis a,
|
| 1283 |
+
.action-widget-result a {
|
| 1284 |
+
color: #1976d2;
|
| 1285 |
+
text-decoration: none;
|
| 1286 |
+
border-bottom: 1px solid transparent;
|
| 1287 |
+
transition: border-color 0.2s;
|
| 1288 |
+
}
|
| 1289 |
+
|
| 1290 |
+
.report-content a:hover,
|
| 1291 |
+
.source-analysis a:hover,
|
| 1292 |
+
.action-widget-result a:hover {
|
| 1293 |
+
border-bottom-color: #1976d2;
|
| 1294 |
+
}
|
| 1295 |
+
|
| 1296 |
+
/* Markdown tables */
|
| 1297 |
+
.markdown-table {
|
| 1298 |
+
border-collapse: collapse;
|
| 1299 |
+
width: 100%;
|
| 1300 |
+
margin: 12px 0;
|
| 1301 |
+
font-size: 12px;
|
| 1302 |
+
background: white;
|
| 1303 |
+
border: 1px solid #e0e0e0;
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
.markdown-table th {
|
| 1307 |
+
background: #f8f8f8;
|
| 1308 |
+
padding: 8px 12px;
|
| 1309 |
+
text-align: left;
|
| 1310 |
+
font-weight: 500;
|
| 1311 |
+
color: #333;
|
| 1312 |
+
border-bottom: 2px solid #e0e0e0;
|
| 1313 |
+
}
|
| 1314 |
+
|
| 1315 |
+
.markdown-table td {
|
| 1316 |
+
padding: 8px 12px;
|
| 1317 |
+
border-bottom: 1px solid #f0f0f0;
|
| 1318 |
+
color: #333;
|
| 1319 |
+
}
|
| 1320 |
+
|
| 1321 |
+
.markdown-table tr:last-child td {
|
| 1322 |
+
border-bottom: none;
|
| 1323 |
+
}
|
| 1324 |
+
|
| 1325 |
+
.markdown-table tr:hover {
|
| 1326 |
+
background: #fafafa;
|
| 1327 |
+
}
|
| 1328 |
+
|
| 1329 |
+
.report-content code,
|
| 1330 |
+
.result-preview-content code,
|
| 1331 |
+
.action-widget-result code {
|
| 1332 |
background: #f5f5f5;
|
| 1333 |
padding: 2px 6px;
|
| 1334 |
border-radius: 3px;
|