Spaces:
Sleeping
Sleeping
Initial deployment
Browse files- .dockerignore +57 -0
- .gitignore +70 -0
- Dockerfile +60 -0
- README.md +58 -4
- backend/pyproject.toml +65 -0
- backend/src/__init__.py +18 -0
- backend/src/agent.py +919 -0
- backend/src/config.py +143 -0
- backend/src/main.py +236 -0
- backend/src/models.py +51 -0
- backend/src/prompts.py +110 -0
- backend/src/services/__init__.py +2 -0
- backend/src/services/notes.py +58 -0
- backend/src/services/planner.py +158 -0
- backend/src/services/reporter.py +79 -0
- backend/src/services/search.py +245 -0
- backend/src/services/summarizer.py +135 -0
- backend/src/services/text_processing.py +16 -0
- backend/src/services/tool_events.py +215 -0
- backend/src/utils.py +84 -0
- frontend/.gitignore +3 -0
- frontend/index.html +12 -0
- frontend/package-lock.json +1758 -0
- frontend/package.json +22 -0
- frontend/src/App.vue +2304 -0
- frontend/src/env.d.ts +7 -0
- frontend/src/main.ts +6 -0
- frontend/src/services/api.ts +96 -0
- frontend/src/style.css +18 -0
- frontend/tsconfig.json +22 -0
- frontend/tsconfig.node.json +9 -0
- frontend/vite.config.ts +9 -0
.dockerignore
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment files - DO NOT include in Docker image
|
| 2 |
+
.env
|
| 3 |
+
.env.local
|
| 4 |
+
.env.*.local
|
| 5 |
+
backend/.env
|
| 6 |
+
frontend/.env.local
|
| 7 |
+
|
| 8 |
+
# Python
|
| 9 |
+
__pycache__/
|
| 10 |
+
*.py[cod]
|
| 11 |
+
*$py.class
|
| 12 |
+
*.so
|
| 13 |
+
.Python
|
| 14 |
+
*.egg-info/
|
| 15 |
+
.eggs/
|
| 16 |
+
*.egg
|
| 17 |
+
.pytest_cache/
|
| 18 |
+
.mypy_cache/
|
| 19 |
+
.ruff_cache/
|
| 20 |
+
venv/
|
| 21 |
+
.venv/
|
| 22 |
+
ENV/
|
| 23 |
+
|
| 24 |
+
# Node.js
|
| 25 |
+
node_modules/
|
| 26 |
+
frontend/node_modules/
|
| 27 |
+
frontend/dist/
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-debug.log*
|
| 30 |
+
yarn-error.log*
|
| 31 |
+
|
| 32 |
+
# IDE
|
| 33 |
+
.idea/
|
| 34 |
+
.vscode/
|
| 35 |
+
*.swp
|
| 36 |
+
*.swo
|
| 37 |
+
*~
|
| 38 |
+
|
| 39 |
+
# Git
|
| 40 |
+
.git/
|
| 41 |
+
.gitignore
|
| 42 |
+
|
| 43 |
+
# OS
|
| 44 |
+
.DS_Store
|
| 45 |
+
Thumbs.db
|
| 46 |
+
|
| 47 |
+
# Notes (generated at runtime)
|
| 48 |
+
backend/src/notes/
|
| 49 |
+
|
| 50 |
+
# Documentation
|
| 51 |
+
*.md
|
| 52 |
+
!README.md
|
| 53 |
+
|
| 54 |
+
# Tests
|
| 55 |
+
tests/
|
| 56 |
+
*_test.py
|
| 57 |
+
test_*.py
|
.gitignore
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment files - NEVER commit these
|
| 2 |
+
.env
|
| 3 |
+
.env.local
|
| 4 |
+
.env.*.local
|
| 5 |
+
backend/.env
|
| 6 |
+
frontend/.env.local
|
| 7 |
+
|
| 8 |
+
# Python
|
| 9 |
+
__pycache__/
|
| 10 |
+
*.py[cod]
|
| 11 |
+
*$py.class
|
| 12 |
+
*.so
|
| 13 |
+
.Python
|
| 14 |
+
*.egg-info/
|
| 15 |
+
.eggs/
|
| 16 |
+
*.egg
|
| 17 |
+
.pytest_cache/
|
| 18 |
+
.mypy_cache/
|
| 19 |
+
.ruff_cache/
|
| 20 |
+
venv/
|
| 21 |
+
.venv/
|
| 22 |
+
ENV/
|
| 23 |
+
env/
|
| 24 |
+
|
| 25 |
+
# Node.js
|
| 26 |
+
node_modules/
|
| 27 |
+
npm-debug.log*
|
| 28 |
+
yarn-debug.log*
|
| 29 |
+
yarn-error.log*
|
| 30 |
+
.npm
|
| 31 |
+
|
| 32 |
+
# Build outputs
|
| 33 |
+
frontend/dist/
|
| 34 |
+
build/
|
| 35 |
+
dist/
|
| 36 |
+
|
| 37 |
+
# IDE
|
| 38 |
+
.idea/
|
| 39 |
+
.vscode/
|
| 40 |
+
*.swp
|
| 41 |
+
*.swo
|
| 42 |
+
*~
|
| 43 |
+
*.sublime-*
|
| 44 |
+
|
| 45 |
+
# OS
|
| 46 |
+
.DS_Store
|
| 47 |
+
.DS_Store?
|
| 48 |
+
._*
|
| 49 |
+
.Spotlight-V100
|
| 50 |
+
.Trashes
|
| 51 |
+
ehthumbs.db
|
| 52 |
+
Thumbs.db
|
| 53 |
+
|
| 54 |
+
# Notes (generated at runtime)
|
| 55 |
+
backend/src/notes/*.md
|
| 56 |
+
|
| 57 |
+
# Logs
|
| 58 |
+
*.log
|
| 59 |
+
logs/
|
| 60 |
+
|
| 61 |
+
# Coverage
|
| 62 |
+
.coverage
|
| 63 |
+
htmlcov/
|
| 64 |
+
.tox/
|
| 65 |
+
.nox/
|
| 66 |
+
|
| 67 |
+
# Temporary files
|
| 68 |
+
*.tmp
|
| 69 |
+
*.temp
|
| 70 |
+
.cache/
|
Dockerfile
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================
|
| 2 |
+
# Stage 1: Build Frontend
|
| 3 |
+
# ============================================
|
| 4 |
+
FROM node:20-alpine AS frontend-builder
|
| 5 |
+
|
| 6 |
+
WORKDIR /app/frontend
|
| 7 |
+
|
| 8 |
+
# Copy package files
|
| 9 |
+
COPY frontend/package*.json ./
|
| 10 |
+
|
| 11 |
+
# Install dependencies
|
| 12 |
+
RUN npm ci
|
| 13 |
+
|
| 14 |
+
# Copy frontend source
|
| 15 |
+
COPY frontend/ ./
|
| 16 |
+
|
| 17 |
+
# Set API base URL to same origin (relative path)
|
| 18 |
+
ENV VITE_API_BASE_URL=""
|
| 19 |
+
|
| 20 |
+
# Build frontend
|
| 21 |
+
RUN npm run build
|
| 22 |
+
|
| 23 |
+
# ============================================
|
| 24 |
+
# Stage 2: Python Backend + Serve Frontend
|
| 25 |
+
# ============================================
|
| 26 |
+
FROM python:3.12-slim
|
| 27 |
+
|
| 28 |
+
WORKDIR /app
|
| 29 |
+
|
| 30 |
+
# Install system dependencies
|
| 31 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 32 |
+
curl \
|
| 33 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 34 |
+
|
| 35 |
+
# Copy backend requirements and install Python dependencies
|
| 36 |
+
COPY backend/pyproject.toml ./
|
| 37 |
+
RUN pip install --no-cache-dir .
|
| 38 |
+
|
| 39 |
+
# Copy backend source code
|
| 40 |
+
COPY backend/src/ ./src/
|
| 41 |
+
|
| 42 |
+
# Copy built frontend to serve as static files
|
| 43 |
+
COPY --from=frontend-builder /app/frontend/dist ./static
|
| 44 |
+
|
| 45 |
+
# Create notes directory
|
| 46 |
+
RUN mkdir -p ./src/notes
|
| 47 |
+
|
| 48 |
+
# Set working directory to src for imports
|
| 49 |
+
WORKDIR /app/src
|
| 50 |
+
|
| 51 |
+
# Expose port 7860 (Hugging Face Spaces default)
|
| 52 |
+
EXPOSE 7860
|
| 53 |
+
|
| 54 |
+
# Environment variables (will be overridden by HF Secrets)
|
| 55 |
+
ENV HOST=0.0.0.0
|
| 56 |
+
ENV PORT=7860
|
| 57 |
+
ENV CORS_ORIGINS="*"
|
| 58 |
+
|
| 59 |
+
# Start the application
|
| 60 |
+
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,10 +1,64 @@
|
|
| 1 |
---
|
| 2 |
title: Deep Research Agent
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Deep Research Agent
|
| 3 |
+
emoji: 🔍
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Deep Research Agent
|
| 12 |
+
|
| 13 |
+
A fully automated web research and summarization assistant powered by LangGraph.
|
| 14 |
+
|
| 15 |
+
## Features
|
| 16 |
+
|
| 17 |
+
- 🔍 Intelligent web search using DuckDuckGo, Tavily, or Perplexity
|
| 18 |
+
- 📝 Automated research planning and execution
|
| 19 |
+
- 📊 Structured research reports with sources
|
| 20 |
+
- 🔄 Real-time streaming progress updates
|
| 21 |
+
|
| 22 |
+
## Configuration
|
| 23 |
+
|
| 24 |
+
This Space requires the following secrets to be configured in the Settings:
|
| 25 |
+
|
| 26 |
+
### Required Secrets
|
| 27 |
+
|
| 28 |
+
| Secret Name | Description |
|
| 29 |
+
|-------------|-------------|
|
| 30 |
+
| `LLM_PROVIDER` | LLM provider: `custom`, `ollama`, or `lmstudio` |
|
| 31 |
+
| `LLM_MODEL_ID` | Model name (e.g., `chatgpt-4o-latest`, `gpt-4-turbo`) |
|
| 32 |
+
| `LLM_API_KEY` | Your OpenAI API key |
|
| 33 |
+
| `LLM_BASE_URL` | API base URL (e.g., `https://api.openai.com/v1`) |
|
| 34 |
+
|
| 35 |
+
### Optional Secrets
|
| 36 |
+
|
| 37 |
+
| Secret Name | Description | Default |
|
| 38 |
+
|-------------|-------------|---------|
|
| 39 |
+
| `SEARCH_API` | Search backend: `duckduckgo`, `tavily`, `perplexity`, `searxng` | `duckduckgo` |
|
| 40 |
+
| `TAVILY_API_KEY` | Tavily API key (if using Tavily search) | - |
|
| 41 |
+
| `PERPLEXITY_API_KEY` | Perplexity API key (if using Perplexity search) | - |
|
| 42 |
+
| `MAX_WEB_RESEARCH_LOOPS` | Maximum research iterations | `3` |
|
| 43 |
+
| `FETCH_FULL_PAGE` | Fetch full page content | `True` |
|
| 44 |
+
| `LLM_TIMEOUT` | API timeout in seconds | `60` |
|
| 45 |
+
|
| 46 |
+
## Usage
|
| 47 |
+
|
| 48 |
+
1. Enter your research topic in the input field
|
| 49 |
+
2. Click "Start Research" to begin
|
| 50 |
+
3. Watch the real-time progress as the agent:
|
| 51 |
+
- Plans research tasks
|
| 52 |
+
- Searches the web
|
| 53 |
+
- Summarizes findings
|
| 54 |
+
- Generates a comprehensive report
|
| 55 |
+
|
| 56 |
+
## Tech Stack
|
| 57 |
+
|
| 58 |
+
- **Backend**: FastAPI + LangGraph + LangChain
|
| 59 |
+
- **Frontend**: Vue.js 3 + Vite
|
| 60 |
+
- **Search**: DuckDuckGo (default) / Tavily / Perplexity
|
| 61 |
+
|
| 62 |
+
## License
|
| 63 |
+
|
| 64 |
+
MIT License
|
backend/pyproject.toml
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "langgraph-deep-researcher"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Fully local web research and summarization assistant powered by LangGraph."
|
| 5 |
+
authors = [
|
| 6 |
+
{ name = "Lance Martin" }
|
| 7 |
+
]
|
| 8 |
+
license = "MIT"
|
| 9 |
+
requires-python = ">=3.10"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"fastapi>=0.115.0",
|
| 12 |
+
"langgraph>=0.2.0",
|
| 13 |
+
"langchain>=0.3.0",
|
| 14 |
+
"langchain-openai>=0.2.0",
|
| 15 |
+
"langchain-community>=0.3.0",
|
| 16 |
+
"tavily-python>=0.5.0",
|
| 17 |
+
"python-dotenv==1.0.1",
|
| 18 |
+
"requests>=2.31.0",
|
| 19 |
+
"openai>=1.12.0",
|
| 20 |
+
"uvicorn[standard]>=0.32.0",
|
| 21 |
+
"ddgs>=9.6.1",
|
| 22 |
+
"loguru>=0.7.3",
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
[project.optional-dependencies]
|
| 26 |
+
dev = ["mypy>=1.11.1", "ruff>=0.6.1"]
|
| 27 |
+
|
| 28 |
+
[build-system]
|
| 29 |
+
requires = ["setuptools>=73.0.0", "wheel"]
|
| 30 |
+
build-backend = "setuptools.build_meta"
|
| 31 |
+
|
| 32 |
+
[tool.setuptools.packages.find]
|
| 33 |
+
where = ["src"]
|
| 34 |
+
|
| 35 |
+
[tool.setuptools.package-data]
|
| 36 |
+
"*" = ["py.typed"]
|
| 37 |
+
|
| 38 |
+
[tool.ruff]
|
| 39 |
+
lint.select = [
|
| 40 |
+
"E", # pycodestyle
|
| 41 |
+
"F", # pyflakes
|
| 42 |
+
"I", # isort
|
| 43 |
+
"D", # pydocstyle
|
| 44 |
+
"D401", # First line should be in imperative mood
|
| 45 |
+
"T201",
|
| 46 |
+
"UP",
|
| 47 |
+
]
|
| 48 |
+
lint.ignore = [
|
| 49 |
+
"UP006",
|
| 50 |
+
"UP007",
|
| 51 |
+
"UP035",
|
| 52 |
+
"D417",
|
| 53 |
+
"E501",
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
[tool.ruff.lint.per-file-ignores]
|
| 57 |
+
"tests/*" = ["D", "UP"]
|
| 58 |
+
|
| 59 |
+
[tool.ruff.lint.pydocstyle]
|
| 60 |
+
convention = "google"
|
| 61 |
+
|
| 62 |
+
[dependency-groups]
|
| 63 |
+
dev = [
|
| 64 |
+
"ruff>=0.12.7",
|
| 65 |
+
]
|
backend/src/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LangGraph Deep Research - A deep research assistant powered by LangGraph."""
|
| 2 |
+
|
| 3 |
+
__version__ = "0.1.0"
|
| 4 |
+
|
| 5 |
+
from .agent import DeepResearchAgent
|
| 6 |
+
from .config import Configuration, SearchAPI
|
| 7 |
+
from .models import SummaryState, SummaryStateInput, SummaryStateOutput, TodoItem
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"DeepResearchAgent",
|
| 11 |
+
"Configuration",
|
| 12 |
+
"SearchAPI",
|
| 13 |
+
"SummaryState",
|
| 14 |
+
"SummaryStateInput",
|
| 15 |
+
"SummaryStateOutput",
|
| 16 |
+
"TodoItem",
|
| 17 |
+
]
|
| 18 |
+
|
backend/src/agent.py
ADDED
|
@@ -0,0 +1,919 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Orchestrator coordinating the deep research workflow using LangGraph."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import re
|
| 7 |
+
import operator
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from queue import Empty, Queue
|
| 10 |
+
from threading import Lock, Thread
|
| 11 |
+
from typing import Any, Annotated, Iterator, TypedDict, Optional, Callable
|
| 12 |
+
|
| 13 |
+
from langchain_openai import ChatOpenAI
|
| 14 |
+
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
|
| 15 |
+
from langchain_core.tools import tool
|
| 16 |
+
from langgraph.graph import StateGraph, END
|
| 17 |
+
|
| 18 |
+
from config import Configuration
|
| 19 |
+
from prompts import (
|
| 20 |
+
report_writer_instructions,
|
| 21 |
+
task_summarizer_instructions,
|
| 22 |
+
todo_planner_system_prompt,
|
| 23 |
+
todo_planner_instructions,
|
| 24 |
+
get_current_date,
|
| 25 |
+
)
|
| 26 |
+
from models import SummaryState, SummaryStateOutput, TodoItem
|
| 27 |
+
from services.search import dispatch_search, prepare_research_context
|
| 28 |
+
from utils import strip_thinking_tokens
|
| 29 |
+
|
| 30 |
+
logger = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ============================================================================
|
| 34 |
+
# State Schema
|
| 35 |
+
# ============================================================================
|
| 36 |
+
class ResearchState(TypedDict, total=False):
|
| 37 |
+
"""State schema for the research workflow graph."""
|
| 38 |
+
research_topic: str
|
| 39 |
+
todo_items: list[TodoItem]
|
| 40 |
+
current_task_index: int
|
| 41 |
+
web_research_results: Annotated[list[str], operator.add]
|
| 42 |
+
sources_gathered: Annotated[list[str], operator.add]
|
| 43 |
+
research_loop_count: int
|
| 44 |
+
structured_report: Optional[str]
|
| 45 |
+
report_note_id: Optional[str]
|
| 46 |
+
report_note_path: Optional[str]
|
| 47 |
+
# Internal tracking
|
| 48 |
+
messages: list[Any]
|
| 49 |
+
config: Configuration
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ============================================================================
|
| 53 |
+
# Note Tool Implementation
|
| 54 |
+
# ============================================================================
|
| 55 |
+
class NoteTool:
|
| 56 |
+
"""Simple file-based note tool for persisting task notes."""
|
| 57 |
+
|
| 58 |
+
def __init__(self, workspace: str = "./notes"):
|
| 59 |
+
self.workspace = Path(workspace)
|
| 60 |
+
self.workspace.mkdir(parents=True, exist_ok=True)
|
| 61 |
+
self._id_counter = 0
|
| 62 |
+
self._lock = Lock()
|
| 63 |
+
|
| 64 |
+
def _generate_id(self) -> str:
|
| 65 |
+
with self._lock:
|
| 66 |
+
self._id_counter += 1
|
| 67 |
+
import time
|
| 68 |
+
return f"note_{int(time.time())}_{self._id_counter}"
|
| 69 |
+
|
| 70 |
+
def run(self, params: dict[str, Any]) -> str:
|
| 71 |
+
"""Execute note action: create, read, update, list."""
|
| 72 |
+
action = params.get("action", "read")
|
| 73 |
+
|
| 74 |
+
if action == "create":
|
| 75 |
+
return self._create_note(params)
|
| 76 |
+
elif action == "read":
|
| 77 |
+
return self._read_note(params)
|
| 78 |
+
elif action == "update":
|
| 79 |
+
return self._update_note(params)
|
| 80 |
+
elif action == "list":
|
| 81 |
+
return self._list_notes(params)
|
| 82 |
+
else:
|
| 83 |
+
return f"❌ Unknown action: {action}"
|
| 84 |
+
|
| 85 |
+
def _create_note(self, params: dict[str, Any]) -> str:
|
| 86 |
+
note_id = self._generate_id()
|
| 87 |
+
title = params.get("title", "Untitled")
|
| 88 |
+
note_type = params.get("note_type", "general")
|
| 89 |
+
tags = params.get("tags", [])
|
| 90 |
+
content = params.get("content", "")
|
| 91 |
+
task_id = params.get("task_id")
|
| 92 |
+
|
| 93 |
+
note_path = self.workspace / f"{note_id}.md"
|
| 94 |
+
|
| 95 |
+
frontmatter = f"""---
|
| 96 |
+
id: {note_id}
|
| 97 |
+
title: {title}
|
| 98 |
+
type: {note_type}
|
| 99 |
+
tags: {tags}
|
| 100 |
+
task_id: {task_id}
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
"""
|
| 104 |
+
note_path.write_text(frontmatter + content, encoding="utf-8")
|
| 105 |
+
return f"✅ Note created\nID: {note_id}\nPath: {note_path}"
|
| 106 |
+
|
| 107 |
+
def _read_note(self, params: dict[str, Any]) -> str:
|
| 108 |
+
note_id = params.get("note_id")
|
| 109 |
+
if not note_id:
|
| 110 |
+
return "❌ Missing note_id parameter"
|
| 111 |
+
|
| 112 |
+
note_path = self.workspace / f"{note_id}.md"
|
| 113 |
+
if not note_path.exists():
|
| 114 |
+
return f"❌ Note does not exist: {note_id}"
|
| 115 |
+
|
| 116 |
+
content = note_path.read_text(encoding="utf-8")
|
| 117 |
+
return f"✅ Note content:\n{content}"
|
| 118 |
+
|
| 119 |
+
def _update_note(self, params: dict[str, Any]) -> str:
|
| 120 |
+
note_id = params.get("note_id")
|
| 121 |
+
if not note_id:
|
| 122 |
+
return "❌ Missing note_id parameter"
|
| 123 |
+
|
| 124 |
+
note_path = self.workspace / f"{note_id}.md"
|
| 125 |
+
if not note_path.exists():
|
| 126 |
+
return f"❌ Note does not exist: {note_id}"
|
| 127 |
+
|
| 128 |
+
# Read existing content
|
| 129 |
+
existing = note_path.read_text(encoding="utf-8")
|
| 130 |
+
|
| 131 |
+
# Update frontmatter if provided
|
| 132 |
+
title = params.get("title")
|
| 133 |
+
content = params.get("content", "")
|
| 134 |
+
|
| 135 |
+
# Simple append strategy
|
| 136 |
+
if content:
|
| 137 |
+
updated = existing + "\n\n---\nUpdate:\n" + content
|
| 138 |
+
note_path.write_text(updated, encoding="utf-8")
|
| 139 |
+
|
| 140 |
+
return f"✅ Note updated\nID: {note_id}"
|
| 141 |
+
|
| 142 |
+
def _list_notes(self, params: dict[str, Any]) -> str:
|
| 143 |
+
notes = list(self.workspace.glob("*.md"))
|
| 144 |
+
if not notes:
|
| 145 |
+
return "📝 No notes yet"
|
| 146 |
+
|
| 147 |
+
result = "📝 Note list:\n"
|
| 148 |
+
for note in notes:
|
| 149 |
+
result += f"- {note.stem}\n"
|
| 150 |
+
return result
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# ============================================================================
|
| 154 |
+
# Tool Call Tracker
|
| 155 |
+
# ============================================================================
|
| 156 |
+
class ToolCallTracker:
|
| 157 |
+
"""Collects tool call events for SSE streaming."""
|
| 158 |
+
|
| 159 |
+
def __init__(self, notes_workspace: Optional[str] = None):
|
| 160 |
+
self._notes_workspace = notes_workspace
|
| 161 |
+
self._events: list[dict[str, Any]] = []
|
| 162 |
+
self._cursor = 0
|
| 163 |
+
self._lock = Lock()
|
| 164 |
+
self._event_sink: Optional[Callable[[dict[str, Any]], None]] = None
|
| 165 |
+
|
| 166 |
+
def record(self, event: dict[str, Any]) -> None:
|
| 167 |
+
with self._lock:
|
| 168 |
+
event["id"] = len(self._events) + 1
|
| 169 |
+
self._events.append(event)
|
| 170 |
+
|
| 171 |
+
sink = self._event_sink
|
| 172 |
+
if sink:
|
| 173 |
+
sink({"type": "tool_call", **event})
|
| 174 |
+
|
| 175 |
+
def drain(self, step: Optional[int] = None) -> list[dict[str, Any]]:
|
| 176 |
+
with self._lock:
|
| 177 |
+
if self._cursor >= len(self._events):
|
| 178 |
+
return []
|
| 179 |
+
new_events = self._events[self._cursor:]
|
| 180 |
+
self._cursor = len(self._events)
|
| 181 |
+
|
| 182 |
+
payloads = []
|
| 183 |
+
for event in new_events:
|
| 184 |
+
payload = {"type": "tool_call", **event}
|
| 185 |
+
if step is not None:
|
| 186 |
+
payload["step"] = step
|
| 187 |
+
payloads.append(payload)
|
| 188 |
+
return payloads
|
| 189 |
+
|
| 190 |
+
def set_event_sink(self, sink: Optional[Callable[[dict[str, Any]], None]]) -> None:
|
| 191 |
+
self._event_sink = sink
|
| 192 |
+
|
| 193 |
+
def as_dicts(self) -> list[dict[str, Any]]:
|
| 194 |
+
with self._lock:
|
| 195 |
+
return list(self._events)
|
| 196 |
+
|
| 197 |
+
def reset(self) -> None:
|
| 198 |
+
with self._lock:
|
| 199 |
+
self._events.clear()
|
| 200 |
+
self._cursor = 0
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
# ============================================================================
|
| 204 |
+
# Deep Research Agent using LangGraph
|
| 205 |
+
# ============================================================================
|
| 206 |
+
class DeepResearchAgent:
|
| 207 |
+
"""Coordinator orchestrating TODO-based research workflow using LangGraph."""
|
| 208 |
+
|
| 209 |
+
def __init__(self, config: Configuration | None = None) -> None:
|
| 210 |
+
"""Initialize the coordinator with configuration and LangGraph components."""
|
| 211 |
+
self.config = config or Configuration.from_env()
|
| 212 |
+
self.llm = self._init_llm()
|
| 213 |
+
|
| 214 |
+
# Note tool setup
|
| 215 |
+
self.note_tool = (
|
| 216 |
+
NoteTool(workspace=self.config.notes_workspace)
|
| 217 |
+
if self.config.enable_notes
|
| 218 |
+
else None
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
# Tool call tracking
|
| 222 |
+
self._tool_tracker = ToolCallTracker(
|
| 223 |
+
self.config.notes_workspace if self.config.enable_notes else None
|
| 224 |
+
)
|
| 225 |
+
self._tool_event_sink_enabled = False
|
| 226 |
+
self._state_lock = Lock()
|
| 227 |
+
|
| 228 |
+
# Build the graph
|
| 229 |
+
self.graph = self._build_graph()
|
| 230 |
+
self._last_search_notices: list[str] = []
|
| 231 |
+
|
| 232 |
+
def _init_llm(self) -> ChatOpenAI:
|
| 233 |
+
"""Initialize ChatOpenAI with configuration preferences."""
|
| 234 |
+
llm_kwargs: dict[str, Any] = {
|
| 235 |
+
"temperature": 0.0,
|
| 236 |
+
"streaming": True,
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
model_id = self.config.llm_model_id or self.config.local_llm
|
| 240 |
+
if model_id:
|
| 241 |
+
llm_kwargs["model"] = model_id
|
| 242 |
+
|
| 243 |
+
provider = (self.config.llm_provider or "").strip()
|
| 244 |
+
|
| 245 |
+
if provider == "ollama":
|
| 246 |
+
llm_kwargs["base_url"] = self.config.sanitized_ollama_url()
|
| 247 |
+
llm_kwargs["api_key"] = self.config.llm_api_key or "ollama"
|
| 248 |
+
elif provider == "lmstudio":
|
| 249 |
+
llm_kwargs["base_url"] = self.config.lmstudio_base_url
|
| 250 |
+
if self.config.llm_api_key:
|
| 251 |
+
llm_kwargs["api_key"] = self.config.llm_api_key
|
| 252 |
+
else:
|
| 253 |
+
llm_kwargs["api_key"] = "lm-studio"
|
| 254 |
+
else:
|
| 255 |
+
if self.config.llm_base_url:
|
| 256 |
+
llm_kwargs["base_url"] = self.config.llm_base_url
|
| 257 |
+
if self.config.llm_api_key:
|
| 258 |
+
llm_kwargs["api_key"] = self.config.llm_api_key
|
| 259 |
+
|
| 260 |
+
return ChatOpenAI(**llm_kwargs)
|
| 261 |
+
|
| 262 |
+
def _build_graph(self) -> StateGraph:
|
| 263 |
+
"""Build the LangGraph workflow."""
|
| 264 |
+
workflow = StateGraph(ResearchState)
|
| 265 |
+
|
| 266 |
+
# Add nodes
|
| 267 |
+
workflow.add_node("plan_research", self._plan_research_node)
|
| 268 |
+
workflow.add_node("execute_tasks", self._execute_tasks_node)
|
| 269 |
+
workflow.add_node("generate_report", self._generate_report_node)
|
| 270 |
+
|
| 271 |
+
# Define edges
|
| 272 |
+
workflow.set_entry_point("plan_research")
|
| 273 |
+
workflow.add_edge("plan_research", "execute_tasks")
|
| 274 |
+
workflow.add_edge("execute_tasks", "generate_report")
|
| 275 |
+
workflow.add_edge("generate_report", END)
|
| 276 |
+
|
| 277 |
+
return workflow.compile()
|
| 278 |
+
|
| 279 |
+
# -------------------------------------------------------------------------
|
| 280 |
+
# Graph Nodes
|
| 281 |
+
# -------------------------------------------------------------------------
|
| 282 |
+
def _plan_research_node(self, state: ResearchState) -> dict[str, Any]:
|
| 283 |
+
"""Planning node: break research topic into actionable tasks."""
|
| 284 |
+
topic = state.get("research_topic", "")
|
| 285 |
+
|
| 286 |
+
system_prompt = todo_planner_system_prompt.strip()
|
| 287 |
+
user_prompt = todo_planner_instructions.format(
|
| 288 |
+
current_date=get_current_date(),
|
| 289 |
+
research_topic=topic,
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
messages = [
|
| 293 |
+
SystemMessage(content=system_prompt),
|
| 294 |
+
HumanMessage(content=user_prompt),
|
| 295 |
+
]
|
| 296 |
+
|
| 297 |
+
response = self.llm.invoke(messages)
|
| 298 |
+
response_text = response.content
|
| 299 |
+
|
| 300 |
+
if self.config.strip_thinking_tokens:
|
| 301 |
+
response_text = strip_thinking_tokens(response_text)
|
| 302 |
+
|
| 303 |
+
logger.info("Planner raw output (truncated): %s", response_text[:500])
|
| 304 |
+
|
| 305 |
+
# Parse tasks from response
|
| 306 |
+
todo_items = self._parse_todo_items(response_text, topic)
|
| 307 |
+
|
| 308 |
+
# Create notes for each task if enabled
|
| 309 |
+
if self.note_tool:
|
| 310 |
+
for task in todo_items:
|
| 311 |
+
result = self.note_tool.run({
|
| 312 |
+
"action": "create",
|
| 313 |
+
"task_id": task.id,
|
| 314 |
+
"title": f"Task {task.id}: {task.title}",
|
| 315 |
+
"note_type": "task_state",
|
| 316 |
+
"tags": ["deep_research", f"task_{task.id}"],
|
| 317 |
+
"content": f"Task objective: {task.intent}\nSearch query: {task.query}",
|
| 318 |
+
})
|
| 319 |
+
# Extract note_id from result
|
| 320 |
+
note_id = self._extract_note_id(result)
|
| 321 |
+
if note_id:
|
| 322 |
+
task.note_id = note_id
|
| 323 |
+
task.note_path = str(Path(self.config.notes_workspace) / f"{note_id}.md")
|
| 324 |
+
|
| 325 |
+
self._tool_tracker.record({
|
| 326 |
+
"agent": "Research Planning Expert",
|
| 327 |
+
"tool": "note",
|
| 328 |
+
"parameters": {"action": "create", "task_id": task.id},
|
| 329 |
+
"result": result,
|
| 330 |
+
"task_id": task.id,
|
| 331 |
+
"note_id": note_id,
|
| 332 |
+
})
|
| 333 |
+
|
| 334 |
+
titles = [task.title for task in todo_items]
|
| 335 |
+
logger.info("Planner produced %d tasks: %s", len(todo_items), titles)
|
| 336 |
+
|
| 337 |
+
return {
|
| 338 |
+
"todo_items": todo_items,
|
| 339 |
+
"current_task_index": 0,
|
| 340 |
+
"research_loop_count": 0,
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
def _execute_tasks_node(self, state: ResearchState) -> dict[str, Any]:
|
| 344 |
+
"""Execute research tasks: search and summarize each task."""
|
| 345 |
+
todo_items = state.get("todo_items", [])
|
| 346 |
+
topic = state.get("research_topic", "")
|
| 347 |
+
loop_count = state.get("research_loop_count", 0)
|
| 348 |
+
|
| 349 |
+
web_results: list[str] = []
|
| 350 |
+
sources: list[str] = []
|
| 351 |
+
|
| 352 |
+
for task in todo_items:
|
| 353 |
+
task.status = "in_progress"
|
| 354 |
+
|
| 355 |
+
# Execute search
|
| 356 |
+
search_result, notices, answer_text, backend = dispatch_search(
|
| 357 |
+
task.query,
|
| 358 |
+
self.config,
|
| 359 |
+
loop_count,
|
| 360 |
+
)
|
| 361 |
+
self._last_search_notices = notices
|
| 362 |
+
task.notices = notices
|
| 363 |
+
|
| 364 |
+
if not search_result or not search_result.get("results"):
|
| 365 |
+
task.status = "skipped"
|
| 366 |
+
continue
|
| 367 |
+
|
| 368 |
+
# Prepare context
|
| 369 |
+
sources_summary, context = prepare_research_context(
|
| 370 |
+
search_result, answer_text, self.config
|
| 371 |
+
)
|
| 372 |
+
task.sources_summary = sources_summary
|
| 373 |
+
web_results.append(context)
|
| 374 |
+
sources.append(sources_summary)
|
| 375 |
+
|
| 376 |
+
# Summarize task
|
| 377 |
+
summary = self._summarize_task(topic, task, context)
|
| 378 |
+
task.summary = summary
|
| 379 |
+
task.status = "completed"
|
| 380 |
+
|
| 381 |
+
# Update note if enabled
|
| 382 |
+
if self.note_tool and task.note_id:
|
| 383 |
+
result = self.note_tool.run({
|
| 384 |
+
"action": "update",
|
| 385 |
+
"note_id": task.note_id,
|
| 386 |
+
"task_id": task.id,
|
| 387 |
+
"content": f"## Task Summary\n{summary}\n\n## Sources\n{sources_summary}",
|
| 388 |
+
})
|
| 389 |
+
self._tool_tracker.record({
|
| 390 |
+
"agent": "Task Summary Expert",
|
| 391 |
+
"tool": "note",
|
| 392 |
+
"parameters": {"action": "update", "note_id": task.note_id},
|
| 393 |
+
"result": result,
|
| 394 |
+
"task_id": task.id,
|
| 395 |
+
"note_id": task.note_id,
|
| 396 |
+
})
|
| 397 |
+
|
| 398 |
+
loop_count += 1
|
| 399 |
+
|
| 400 |
+
return {
|
| 401 |
+
"todo_items": todo_items,
|
| 402 |
+
"web_research_results": web_results,
|
| 403 |
+
"sources_gathered": sources,
|
| 404 |
+
"research_loop_count": loop_count,
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
def _generate_report_node(self, state: ResearchState) -> dict[str, Any]:
|
| 408 |
+
"""Generate the final structured report."""
|
| 409 |
+
topic = state.get("research_topic", "")
|
| 410 |
+
todo_items = state.get("todo_items", [])
|
| 411 |
+
|
| 412 |
+
# Build task overview
|
| 413 |
+
tasks_block = []
|
| 414 |
+
for task in todo_items:
|
| 415 |
+
summary_block = task.summary or "No information available"
|
| 416 |
+
sources_block = task.sources_summary or "No sources available"
|
| 417 |
+
tasks_block.append(
|
| 418 |
+
f"### Task {task.id}: {task.title}\n"
|
| 419 |
+
f"- Objective: {task.intent}\n"
|
| 420 |
+
f"- Search query: {task.query}\n"
|
| 421 |
+
f"- Status: {task.status}\n"
|
| 422 |
+
f"- Summary:\n{summary_block}\n"
|
| 423 |
+
f"- Sources:\n{sources_block}\n"
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
prompt = (
|
| 427 |
+
f"Research topic: {topic}\n"
|
| 428 |
+
f"Task overview:\n{''.join(tasks_block)}\n"
|
| 429 |
+
"Based on the above task summaries, please write a structured research report."
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
messages = [
|
| 433 |
+
SystemMessage(content=report_writer_instructions.strip()),
|
| 434 |
+
HumanMessage(content=prompt),
|
| 435 |
+
]
|
| 436 |
+
|
| 437 |
+
response = self.llm.invoke(messages)
|
| 438 |
+
report_text = response.content
|
| 439 |
+
|
| 440 |
+
if self.config.strip_thinking_tokens:
|
| 441 |
+
report_text = strip_thinking_tokens(report_text)
|
| 442 |
+
|
| 443 |
+
report_text = report_text.strip() or "Report generation failed, please check input."
|
| 444 |
+
|
| 445 |
+
# Create conclusion note if enabled
|
| 446 |
+
report_note_id = None
|
| 447 |
+
report_note_path = None
|
| 448 |
+
if self.note_tool and report_text:
|
| 449 |
+
result = self.note_tool.run({
|
| 450 |
+
"action": "create",
|
| 451 |
+
"title": f"Research Report: {topic}",
|
| 452 |
+
"note_type": "conclusion",
|
| 453 |
+
"tags": ["deep_research", "report"],
|
| 454 |
+
"content": report_text,
|
| 455 |
+
})
|
| 456 |
+
report_note_id = self._extract_note_id(result)
|
| 457 |
+
if report_note_id:
|
| 458 |
+
report_note_path = str(Path(self.config.notes_workspace) / f"{report_note_id}.md")
|
| 459 |
+
|
| 460 |
+
self._tool_tracker.record({
|
| 461 |
+
"agent": "Report Writing Expert",
|
| 462 |
+
"tool": "note",
|
| 463 |
+
"parameters": {"action": "create", "note_type": "conclusion"},
|
| 464 |
+
"result": result,
|
| 465 |
+
"note_id": report_note_id,
|
| 466 |
+
})
|
| 467 |
+
|
| 468 |
+
return {
|
| 469 |
+
"structured_report": report_text,
|
| 470 |
+
"report_note_id": report_note_id,
|
| 471 |
+
"report_note_path": report_note_path,
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
# -------------------------------------------------------------------------
|
| 475 |
+
# Helper Methods
|
| 476 |
+
# -------------------------------------------------------------------------
|
| 477 |
+
def _summarize_task(self, topic: str, task: TodoItem, context: str) -> str:
|
| 478 |
+
"""Generate summary for a single task."""
|
| 479 |
+
prompt = (
|
| 480 |
+
f"Task topic: {topic}\n"
|
| 481 |
+
f"Task name: {task.title}\n"
|
| 482 |
+
f"Task objective: {task.intent}\n"
|
| 483 |
+
f"Search query: {task.query}\n"
|
| 484 |
+
f"Task context:\n{context}\n"
|
| 485 |
+
"Please generate a detailed task summary."
|
| 486 |
+
)
|
| 487 |
+
|
| 488 |
+
messages = [
|
| 489 |
+
SystemMessage(content=task_summarizer_instructions.strip()),
|
| 490 |
+
HumanMessage(content=prompt),
|
| 491 |
+
]
|
| 492 |
+
|
| 493 |
+
response = self.llm.invoke(messages)
|
| 494 |
+
summary_text = response.content
|
| 495 |
+
|
| 496 |
+
if self.config.strip_thinking_tokens:
|
| 497 |
+
summary_text = strip_thinking_tokens(summary_text)
|
| 498 |
+
|
| 499 |
+
return summary_text.strip() or "No information available"
|
| 500 |
+
|
| 501 |
+
def _parse_todo_items(self, response: str, topic: str) -> list[TodoItem]:
|
| 502 |
+
"""Parse planner output into TodoItem list."""
|
| 503 |
+
import json
|
| 504 |
+
|
| 505 |
+
text = response.strip()
|
| 506 |
+
tasks_payload: list[dict[str, Any]] = []
|
| 507 |
+
|
| 508 |
+
# Try to extract JSON
|
| 509 |
+
start = text.find("{")
|
| 510 |
+
end = text.rfind("}")
|
| 511 |
+
if start != -1 and end != -1 and end > start:
|
| 512 |
+
try:
|
| 513 |
+
json_obj = json.loads(text[start:end + 1])
|
| 514 |
+
if isinstance(json_obj, dict) and "tasks" in json_obj:
|
| 515 |
+
tasks_payload = json_obj["tasks"]
|
| 516 |
+
except json.JSONDecodeError:
|
| 517 |
+
pass
|
| 518 |
+
|
| 519 |
+
if not tasks_payload:
|
| 520 |
+
start = text.find("[")
|
| 521 |
+
end = text.rfind("]")
|
| 522 |
+
if start != -1 and end != -1 and end > start:
|
| 523 |
+
try:
|
| 524 |
+
tasks_payload = json.loads(text[start:end + 1])
|
| 525 |
+
except json.JSONDecodeError:
|
| 526 |
+
pass
|
| 527 |
+
|
| 528 |
+
# Create TodoItems
|
| 529 |
+
todo_items: list[TodoItem] = []
|
| 530 |
+
for idx, item in enumerate(tasks_payload, start=1):
|
| 531 |
+
if not isinstance(item, dict):
|
| 532 |
+
continue
|
| 533 |
+
title = str(item.get("title") or f"Task{idx}").strip()
|
| 534 |
+
intent = str(item.get("intent") or "Focus on key issues of the topic").strip()
|
| 535 |
+
query = str(item.get("query") or topic).strip() or topic
|
| 536 |
+
|
| 537 |
+
todo_items.append(TodoItem(
|
| 538 |
+
id=idx,
|
| 539 |
+
title=title,
|
| 540 |
+
intent=intent,
|
| 541 |
+
query=query,
|
| 542 |
+
))
|
| 543 |
+
|
| 544 |
+
# Fallback if no tasks parsed
|
| 545 |
+
if not todo_items:
|
| 546 |
+
todo_items.append(TodoItem(
|
| 547 |
+
id=1,
|
| 548 |
+
title="Basic Background Overview",
|
| 549 |
+
intent="Collect core background and latest developments on the topic",
|
| 550 |
+
query=f"{topic} latest developments" if topic else "Basic background overview",
|
| 551 |
+
))
|
| 552 |
+
|
| 553 |
+
return todo_items
|
| 554 |
+
|
| 555 |
+
@staticmethod
|
| 556 |
+
def _extract_note_id(response: str) -> Optional[str]:
|
| 557 |
+
"""Extract note ID from tool response."""
|
| 558 |
+
if not response:
|
| 559 |
+
return None
|
| 560 |
+
match = re.search(r"ID:\s*([^\n]+)", response)
|
| 561 |
+
return match.group(1).strip() if match else None
|
| 562 |
+
|
| 563 |
+
def _set_tool_event_sink(self, sink: Callable[[dict[str, Any]], None] | None) -> None:
|
| 564 |
+
"""Enable or disable immediate tool event callbacks."""
|
| 565 |
+
self._tool_event_sink_enabled = sink is not None
|
| 566 |
+
self._tool_tracker.set_event_sink(sink)
|
| 567 |
+
|
| 568 |
+
# -------------------------------------------------------------------------
|
| 569 |
+
# Public API
|
| 570 |
+
# -------------------------------------------------------------------------
|
| 571 |
+
def run(self, topic: str) -> SummaryStateOutput:
|
| 572 |
+
"""Execute the research workflow and return the final report."""
|
| 573 |
+
initial_state: ResearchState = {
|
| 574 |
+
"research_topic": topic,
|
| 575 |
+
"todo_items": [],
|
| 576 |
+
"current_task_index": 0,
|
| 577 |
+
"web_research_results": [],
|
| 578 |
+
"sources_gathered": [],
|
| 579 |
+
"research_loop_count": 0,
|
| 580 |
+
"structured_report": None,
|
| 581 |
+
"report_note_id": None,
|
| 582 |
+
"report_note_path": None,
|
| 583 |
+
"messages": [],
|
| 584 |
+
"config": self.config,
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
# Run the graph
|
| 588 |
+
final_state = self.graph.invoke(initial_state)
|
| 589 |
+
|
| 590 |
+
report = final_state.get("structured_report", "")
|
| 591 |
+
todo_items = final_state.get("todo_items", [])
|
| 592 |
+
|
| 593 |
+
return SummaryStateOutput(
|
| 594 |
+
running_summary=report,
|
| 595 |
+
report_markdown=report,
|
| 596 |
+
todo_items=todo_items,
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
def run_stream(self, topic: str) -> Iterator[dict[str, Any]]:
|
| 600 |
+
"""Execute the workflow yielding incremental progress events."""
|
| 601 |
+
logger.debug("Starting streaming research: topic=%s", topic)
|
| 602 |
+
yield {"type": "status", "message": "Initializing research workflow"}
|
| 603 |
+
|
| 604 |
+
# Plan phase
|
| 605 |
+
yield {"type": "status", "message": "Planning research tasks..."}
|
| 606 |
+
|
| 607 |
+
system_prompt = todo_planner_system_prompt.strip()
|
| 608 |
+
user_prompt = todo_planner_instructions.format(
|
| 609 |
+
current_date=get_current_date(),
|
| 610 |
+
research_topic=topic,
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
messages = [
|
| 614 |
+
SystemMessage(content=system_prompt),
|
| 615 |
+
HumanMessage(content=user_prompt),
|
| 616 |
+
]
|
| 617 |
+
|
| 618 |
+
response = self.llm.invoke(messages)
|
| 619 |
+
response_text = response.content
|
| 620 |
+
|
| 621 |
+
if self.config.strip_thinking_tokens:
|
| 622 |
+
response_text = strip_thinking_tokens(response_text)
|
| 623 |
+
|
| 624 |
+
todo_items = self._parse_todo_items(response_text, topic)
|
| 625 |
+
|
| 626 |
+
# Create notes for tasks
|
| 627 |
+
if self.note_tool:
|
| 628 |
+
for task in todo_items:
|
| 629 |
+
result = self.note_tool.run({
|
| 630 |
+
"action": "create",
|
| 631 |
+
"task_id": task.id,
|
| 632 |
+
"title": f"Task {task.id}: {task.title}",
|
| 633 |
+
"note_type": "task_state",
|
| 634 |
+
"tags": ["deep_research", f"task_{task.id}"],
|
| 635 |
+
"content": f"Task objective: {task.intent}\nSearch query: {task.query}",
|
| 636 |
+
})
|
| 637 |
+
note_id = self._extract_note_id(result)
|
| 638 |
+
if note_id:
|
| 639 |
+
task.note_id = note_id
|
| 640 |
+
task.note_path = str(Path(self.config.notes_workspace) / f"{note_id}.md")
|
| 641 |
+
|
| 642 |
+
# Setup channel mapping for streaming
|
| 643 |
+
channel_map: dict[int, dict[str, Any]] = {}
|
| 644 |
+
for index, task in enumerate(todo_items, start=1):
|
| 645 |
+
token = f"task_{task.id}"
|
| 646 |
+
task.stream_token = token
|
| 647 |
+
channel_map[task.id] = {"step": index, "token": token}
|
| 648 |
+
|
| 649 |
+
yield {
|
| 650 |
+
"type": "todo_list",
|
| 651 |
+
"tasks": [self._serialize_task(t) for t in todo_items],
|
| 652 |
+
"step": 0,
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
# Execute tasks with streaming
|
| 656 |
+
event_queue: Queue[dict[str, Any]] = Queue()
|
| 657 |
+
|
| 658 |
+
def enqueue(event: dict[str, Any], task: Optional[TodoItem] = None, step_override: Optional[int] = None) -> None:
|
| 659 |
+
payload = dict(event)
|
| 660 |
+
target_task_id = payload.get("task_id")
|
| 661 |
+
if task is not None:
|
| 662 |
+
target_task_id = task.id
|
| 663 |
+
payload["task_id"] = task.id
|
| 664 |
+
|
| 665 |
+
channel = channel_map.get(target_task_id) if target_task_id else None
|
| 666 |
+
if channel:
|
| 667 |
+
payload.setdefault("step", channel["step"])
|
| 668 |
+
payload["stream_token"] = channel["token"]
|
| 669 |
+
if step_override is not None:
|
| 670 |
+
payload["step"] = step_override
|
| 671 |
+
event_queue.put(payload)
|
| 672 |
+
|
| 673 |
+
def tool_event_sink(event: dict[str, Any]) -> None:
|
| 674 |
+
enqueue(event)
|
| 675 |
+
|
| 676 |
+
self._set_tool_event_sink(tool_event_sink)
|
| 677 |
+
|
| 678 |
+
threads: list[Thread] = []
|
| 679 |
+
state = SummaryState(research_topic=topic)
|
| 680 |
+
state.todo_items = todo_items
|
| 681 |
+
|
| 682 |
+
def worker(task: TodoItem, step: int) -> None:
|
| 683 |
+
try:
|
| 684 |
+
enqueue({
|
| 685 |
+
"type": "task_status",
|
| 686 |
+
"task_id": task.id,
|
| 687 |
+
"status": "in_progress",
|
| 688 |
+
"title": task.title,
|
| 689 |
+
"intent": task.intent,
|
| 690 |
+
"note_id": task.note_id,
|
| 691 |
+
"note_path": task.note_path,
|
| 692 |
+
}, task=task)
|
| 693 |
+
|
| 694 |
+
# Execute search
|
| 695 |
+
search_result, notices, answer_text, backend = dispatch_search(
|
| 696 |
+
task.query, self.config, state.research_loop_count
|
| 697 |
+
)
|
| 698 |
+
task.notices = notices
|
| 699 |
+
|
| 700 |
+
for notice in notices:
|
| 701 |
+
if notice:
|
| 702 |
+
enqueue({
|
| 703 |
+
"type": "status",
|
| 704 |
+
"message": notice,
|
| 705 |
+
"task_id": task.id,
|
| 706 |
+
}, task=task)
|
| 707 |
+
|
| 708 |
+
if not search_result or not search_result.get("results"):
|
| 709 |
+
task.status = "skipped"
|
| 710 |
+
enqueue({
|
| 711 |
+
"type": "task_status",
|
| 712 |
+
"task_id": task.id,
|
| 713 |
+
"status": "skipped",
|
| 714 |
+
"title": task.title,
|
| 715 |
+
"intent": task.intent,
|
| 716 |
+
"note_id": task.note_id,
|
| 717 |
+
"note_path": task.note_path,
|
| 718 |
+
}, task=task)
|
| 719 |
+
return
|
| 720 |
+
|
| 721 |
+
# Prepare context
|
| 722 |
+
sources_summary, context = prepare_research_context(
|
| 723 |
+
search_result, answer_text, self.config
|
| 724 |
+
)
|
| 725 |
+
task.sources_summary = sources_summary
|
| 726 |
+
|
| 727 |
+
with self._state_lock:
|
| 728 |
+
state.web_research_results.append(context)
|
| 729 |
+
state.sources_gathered.append(sources_summary)
|
| 730 |
+
state.research_loop_count += 1
|
| 731 |
+
|
| 732 |
+
enqueue({
|
| 733 |
+
"type": "sources",
|
| 734 |
+
"task_id": task.id,
|
| 735 |
+
"latest_sources": sources_summary,
|
| 736 |
+
"raw_context": context,
|
| 737 |
+
"backend": backend,
|
| 738 |
+
"note_id": task.note_id,
|
| 739 |
+
"note_path": task.note_path,
|
| 740 |
+
}, task=task)
|
| 741 |
+
|
| 742 |
+
# Stream summarization
|
| 743 |
+
prompt = (
|
| 744 |
+
f"Task topic: {topic}\n"
|
| 745 |
+
f"Task name: {task.title}\n"
|
| 746 |
+
f"Task objective: {task.intent}\n"
|
| 747 |
+
f"Search query: {task.query}\n"
|
| 748 |
+
f"Task context:\n{context}\n"
|
| 749 |
+
"Please generate a detailed task summary."
|
| 750 |
+
)
|
| 751 |
+
|
| 752 |
+
summary_messages = [
|
| 753 |
+
SystemMessage(content=task_summarizer_instructions.strip()),
|
| 754 |
+
HumanMessage(content=prompt),
|
| 755 |
+
]
|
| 756 |
+
|
| 757 |
+
summary_chunks: list[str] = []
|
| 758 |
+
for chunk in self.llm.stream(summary_messages):
|
| 759 |
+
chunk_text = chunk.content
|
| 760 |
+
if chunk_text:
|
| 761 |
+
summary_chunks.append(chunk_text)
|
| 762 |
+
# Strip thinking tokens from visible output
|
| 763 |
+
visible_chunk = chunk_text
|
| 764 |
+
if self.config.strip_thinking_tokens and "<think>" not in chunk_text:
|
| 765 |
+
enqueue({
|
| 766 |
+
"type": "task_summary_chunk",
|
| 767 |
+
"task_id": task.id,
|
| 768 |
+
"content": visible_chunk,
|
| 769 |
+
"note_id": task.note_id,
|
| 770 |
+
}, task=task)
|
| 771 |
+
|
| 772 |
+
full_summary = "".join(summary_chunks)
|
| 773 |
+
if self.config.strip_thinking_tokens:
|
| 774 |
+
full_summary = strip_thinking_tokens(full_summary)
|
| 775 |
+
|
| 776 |
+
task.summary = full_summary.strip() or "No information available"
|
| 777 |
+
task.status = "completed"
|
| 778 |
+
|
| 779 |
+
# Update note
|
| 780 |
+
if self.note_tool and task.note_id:
|
| 781 |
+
self.note_tool.run({
|
| 782 |
+
"action": "update",
|
| 783 |
+
"note_id": task.note_id,
|
| 784 |
+
"task_id": task.id,
|
| 785 |
+
"content": f"## Task Summary\n{task.summary}\n\n## Sources\n{sources_summary}",
|
| 786 |
+
})
|
| 787 |
+
|
| 788 |
+
enqueue({
|
| 789 |
+
"type": "task_status",
|
| 790 |
+
"task_id": task.id,
|
| 791 |
+
"status": "completed",
|
| 792 |
+
"summary": task.summary,
|
| 793 |
+
"sources_summary": task.sources_summary,
|
| 794 |
+
"note_id": task.note_id,
|
| 795 |
+
"note_path": task.note_path,
|
| 796 |
+
}, task=task)
|
| 797 |
+
|
| 798 |
+
except Exception as exc:
|
| 799 |
+
logger.exception("Task execution failed", exc_info=exc)
|
| 800 |
+
enqueue({
|
| 801 |
+
"type": "task_status",
|
| 802 |
+
"task_id": task.id,
|
| 803 |
+
"status": "failed",
|
| 804 |
+
"detail": str(exc),
|
| 805 |
+
"title": task.title,
|
| 806 |
+
"intent": task.intent,
|
| 807 |
+
"note_id": task.note_id,
|
| 808 |
+
"note_path": task.note_path,
|
| 809 |
+
}, task=task)
|
| 810 |
+
finally:
|
| 811 |
+
enqueue({"type": "__task_done__", "task_id": task.id})
|
| 812 |
+
|
| 813 |
+
# Start worker threads
|
| 814 |
+
for task in todo_items:
|
| 815 |
+
step = channel_map.get(task.id, {}).get("step", 0)
|
| 816 |
+
thread = Thread(target=worker, args=(task, step), daemon=True)
|
| 817 |
+
threads.append(thread)
|
| 818 |
+
thread.start()
|
| 819 |
+
|
| 820 |
+
# Yield events from queue
|
| 821 |
+
active_workers = len(todo_items)
|
| 822 |
+
finished_workers = 0
|
| 823 |
+
|
| 824 |
+
try:
|
| 825 |
+
while finished_workers < active_workers:
|
| 826 |
+
event = event_queue.get()
|
| 827 |
+
if event.get("type") == "__task_done__":
|
| 828 |
+
finished_workers += 1
|
| 829 |
+
continue
|
| 830 |
+
yield event
|
| 831 |
+
|
| 832 |
+
# Drain remaining events
|
| 833 |
+
while True:
|
| 834 |
+
try:
|
| 835 |
+
event = event_queue.get_nowait()
|
| 836 |
+
except Empty:
|
| 837 |
+
break
|
| 838 |
+
if event.get("type") != "__task_done__":
|
| 839 |
+
yield event
|
| 840 |
+
finally:
|
| 841 |
+
self._set_tool_event_sink(None)
|
| 842 |
+
for thread in threads:
|
| 843 |
+
thread.join()
|
| 844 |
+
|
| 845 |
+
# Generate final report
|
| 846 |
+
yield {"type": "status", "message": "Generating research report..."}
|
| 847 |
+
|
| 848 |
+
tasks_block = []
|
| 849 |
+
for task in todo_items:
|
| 850 |
+
summary_block = task.summary or "No information available"
|
| 851 |
+
sources_block = task.sources_summary or "No sources available"
|
| 852 |
+
tasks_block.append(
|
| 853 |
+
f"### Task {task.id}: {task.title}\n"
|
| 854 |
+
f"- Objective: {task.intent}\n"
|
| 855 |
+
f"- Search query: {task.query}\n"
|
| 856 |
+
f"- Status: {task.status}\n"
|
| 857 |
+
f"- Summary:\n{summary_block}\n"
|
| 858 |
+
f"- Sources:\n{sources_block}\n"
|
| 859 |
+
)
|
| 860 |
+
|
| 861 |
+
report_prompt = (
|
| 862 |
+
f"Research topic: {topic}\n"
|
| 863 |
+
f"Task overview:\n{''.join(tasks_block)}\n"
|
| 864 |
+
"Based on the above task summaries, please write a structured research report."
|
| 865 |
+
)
|
| 866 |
+
|
| 867 |
+
report_messages = [
|
| 868 |
+
SystemMessage(content=report_writer_instructions.strip()),
|
| 869 |
+
HumanMessage(content=report_prompt),
|
| 870 |
+
]
|
| 871 |
+
|
| 872 |
+
report = self.llm.invoke(report_messages).content
|
| 873 |
+
if self.config.strip_thinking_tokens:
|
| 874 |
+
report = strip_thinking_tokens(report)
|
| 875 |
+
report = report.strip() or "Report generation failed"
|
| 876 |
+
|
| 877 |
+
# Create conclusion note
|
| 878 |
+
report_note_id = None
|
| 879 |
+
report_note_path = None
|
| 880 |
+
if self.note_tool:
|
| 881 |
+
result = self.note_tool.run({
|
| 882 |
+
"action": "create",
|
| 883 |
+
"title": f"Research Report: {topic}",
|
| 884 |
+
"note_type": "conclusion",
|
| 885 |
+
"tags": ["deep_research", "report"],
|
| 886 |
+
"content": report,
|
| 887 |
+
})
|
| 888 |
+
report_note_id = self._extract_note_id(result)
|
| 889 |
+
if report_note_id:
|
| 890 |
+
report_note_path = str(Path(self.config.notes_workspace) / f"{report_note_id}.md")
|
| 891 |
+
|
| 892 |
+
yield {
|
| 893 |
+
"type": "final_report",
|
| 894 |
+
"report": report,
|
| 895 |
+
"note_id": report_note_id,
|
| 896 |
+
"note_path": report_note_path,
|
| 897 |
+
}
|
| 898 |
+
yield {"type": "done"}
|
| 899 |
+
|
| 900 |
+
def _serialize_task(self, task: TodoItem) -> dict[str, Any]:
|
| 901 |
+
"""Convert task dataclass to serializable dict for frontend."""
|
| 902 |
+
return {
|
| 903 |
+
"id": task.id,
|
| 904 |
+
"title": task.title,
|
| 905 |
+
"intent": task.intent,
|
| 906 |
+
"query": task.query,
|
| 907 |
+
"status": task.status,
|
| 908 |
+
"summary": task.summary,
|
| 909 |
+
"sources_summary": task.sources_summary,
|
| 910 |
+
"note_id": task.note_id,
|
| 911 |
+
"note_path": task.note_path,
|
| 912 |
+
"stream_token": task.stream_token,
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
|
| 916 |
+
def run_deep_research(topic: str, config: Configuration | None = None) -> SummaryStateOutput:
|
| 917 |
+
"""Convenience function mirroring the class-based API."""
|
| 918 |
+
agent = DeepResearchAgent(config=config)
|
| 919 |
+
return agent.run(topic)
|
backend/src/config.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from enum import Enum
|
| 3 |
+
from typing import Any, Optional
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class SearchAPI(Enum):
|
| 9 |
+
PERPLEXITY = "perplexity"
|
| 10 |
+
TAVILY = "tavily"
|
| 11 |
+
DUCKDUCKGO = "duckduckgo"
|
| 12 |
+
SEARXNG = "searxng"
|
| 13 |
+
ADVANCED = "advanced"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class Configuration(BaseModel):
|
| 17 |
+
"""Configuration options for the deep research assistant."""
|
| 18 |
+
|
| 19 |
+
max_web_research_loops: int = Field(
|
| 20 |
+
default=3,
|
| 21 |
+
title="Research Depth",
|
| 22 |
+
description="Number of research iterations to perform",
|
| 23 |
+
)
|
| 24 |
+
local_llm: str = Field(
|
| 25 |
+
default="llama3.2",
|
| 26 |
+
title="Local Model Name",
|
| 27 |
+
description="Name of the locally hosted LLM (Ollama/LMStudio)",
|
| 28 |
+
)
|
| 29 |
+
llm_provider: str = Field(
|
| 30 |
+
default="ollama",
|
| 31 |
+
title="LLM Provider",
|
| 32 |
+
description="Provider identifier (ollama, lmstudio, or custom)",
|
| 33 |
+
)
|
| 34 |
+
search_api: SearchAPI = Field(
|
| 35 |
+
default=SearchAPI.DUCKDUCKGO,
|
| 36 |
+
title="Search API",
|
| 37 |
+
description="Web search API to use",
|
| 38 |
+
)
|
| 39 |
+
enable_notes: bool = Field(
|
| 40 |
+
default=True,
|
| 41 |
+
title="Enable Notes",
|
| 42 |
+
description="Whether to store task progress in NoteTool",
|
| 43 |
+
)
|
| 44 |
+
notes_workspace: str = Field(
|
| 45 |
+
default="./notes",
|
| 46 |
+
title="Notes Workspace",
|
| 47 |
+
description="Directory for NoteTool to persist task notes",
|
| 48 |
+
)
|
| 49 |
+
fetch_full_page: bool = Field(
|
| 50 |
+
default=True,
|
| 51 |
+
title="Fetch Full Page",
|
| 52 |
+
description="Include the full page content in the search results",
|
| 53 |
+
)
|
| 54 |
+
ollama_base_url: str = Field(
|
| 55 |
+
default="http://localhost:11434",
|
| 56 |
+
title="Ollama Base URL",
|
| 57 |
+
description="Base URL for Ollama API (without /v1 suffix)",
|
| 58 |
+
)
|
| 59 |
+
lmstudio_base_url: str = Field(
|
| 60 |
+
default="http://localhost:1234/v1",
|
| 61 |
+
title="LMStudio Base URL",
|
| 62 |
+
description="Base URL for LMStudio OpenAI-compatible API",
|
| 63 |
+
)
|
| 64 |
+
strip_thinking_tokens: bool = Field(
|
| 65 |
+
default=True,
|
| 66 |
+
title="Strip Thinking Tokens",
|
| 67 |
+
description="Whether to strip <think> tokens from model responses",
|
| 68 |
+
)
|
| 69 |
+
use_tool_calling: bool = Field(
|
| 70 |
+
default=False,
|
| 71 |
+
title="Use Tool Calling",
|
| 72 |
+
description="Use tool calling instead of JSON mode for structured output",
|
| 73 |
+
)
|
| 74 |
+
llm_api_key: Optional[str] = Field(
|
| 75 |
+
default=None,
|
| 76 |
+
title="LLM API Key",
|
| 77 |
+
description="Optional API key when using custom OpenAI-compatible services",
|
| 78 |
+
)
|
| 79 |
+
llm_base_url: Optional[str] = Field(
|
| 80 |
+
default=None,
|
| 81 |
+
title="LLM Base URL",
|
| 82 |
+
description="Optional base URL when using custom OpenAI-compatible services",
|
| 83 |
+
)
|
| 84 |
+
llm_model_id: Optional[str] = Field(
|
| 85 |
+
default=None,
|
| 86 |
+
title="LLM Model ID",
|
| 87 |
+
description="Optional model identifier for custom OpenAI-compatible services",
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
@classmethod
|
| 91 |
+
def from_env(cls, overrides: Optional[dict[str, Any]] = None) -> "Configuration":
|
| 92 |
+
"""Create a configuration object using environment variables and overrides."""
|
| 93 |
+
|
| 94 |
+
raw_values: dict[str, Any] = {}
|
| 95 |
+
|
| 96 |
+
# Load values from environment variables based on field names
|
| 97 |
+
for field_name in cls.model_fields.keys():
|
| 98 |
+
env_key = field_name.upper()
|
| 99 |
+
if env_key in os.environ:
|
| 100 |
+
raw_values[field_name] = os.environ[env_key]
|
| 101 |
+
|
| 102 |
+
# Additional mappings for explicit env names
|
| 103 |
+
env_aliases = {
|
| 104 |
+
"local_llm": os.getenv("LOCAL_LLM"),
|
| 105 |
+
"llm_provider": os.getenv("LLM_PROVIDER"),
|
| 106 |
+
"llm_api_key": os.getenv("LLM_API_KEY"),
|
| 107 |
+
"llm_model_id": os.getenv("LLM_MODEL_ID"),
|
| 108 |
+
"llm_base_url": os.getenv("LLM_BASE_URL"),
|
| 109 |
+
"lmstudio_base_url": os.getenv("LMSTUDIO_BASE_URL"),
|
| 110 |
+
"ollama_base_url": os.getenv("OLLAMA_BASE_URL"),
|
| 111 |
+
"max_web_research_loops": os.getenv("MAX_WEB_RESEARCH_LOOPS"),
|
| 112 |
+
"fetch_full_page": os.getenv("FETCH_FULL_PAGE"),
|
| 113 |
+
"strip_thinking_tokens": os.getenv("STRIP_THINKING_TOKENS"),
|
| 114 |
+
"use_tool_calling": os.getenv("USE_TOOL_CALLING"),
|
| 115 |
+
"search_api": os.getenv("SEARCH_API"),
|
| 116 |
+
"enable_notes": os.getenv("ENABLE_NOTES"),
|
| 117 |
+
"notes_workspace": os.getenv("NOTES_WORKSPACE"),
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
for key, value in env_aliases.items():
|
| 121 |
+
if value is not None:
|
| 122 |
+
raw_values.setdefault(key, value)
|
| 123 |
+
|
| 124 |
+
if overrides:
|
| 125 |
+
for key, value in overrides.items():
|
| 126 |
+
if value is not None:
|
| 127 |
+
raw_values[key] = value
|
| 128 |
+
|
| 129 |
+
return cls(**raw_values)
|
| 130 |
+
|
| 131 |
+
def sanitized_ollama_url(self) -> str:
|
| 132 |
+
"""Ensure Ollama base URL includes the /v1 suffix required by OpenAI clients."""
|
| 133 |
+
|
| 134 |
+
base = self.ollama_base_url.rstrip("/")
|
| 135 |
+
if not base.endswith("/v1"):
|
| 136 |
+
base = f"{base}/v1"
|
| 137 |
+
return base
|
| 138 |
+
|
| 139 |
+
def resolved_model(self) -> Optional[str]:
|
| 140 |
+
"""Best-effort resolution of the model identifier to use."""
|
| 141 |
+
|
| 142 |
+
return self.llm_model_id or self.local_llm
|
| 143 |
+
|
backend/src/main.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI entrypoint exposing the DeepResearchAgent via HTTP."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any, Dict, Iterator, Optional
|
| 10 |
+
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
|
| 13 |
+
# Load .env file before importing config
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
from fastapi import FastAPI, HTTPException
|
| 17 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 18 |
+
from fastapi.responses import FileResponse, StreamingResponse
|
| 19 |
+
from fastapi.staticfiles import StaticFiles
|
| 20 |
+
from loguru import logger
|
| 21 |
+
from pydantic import BaseModel, Field
|
| 22 |
+
|
| 23 |
+
from config import Configuration, SearchAPI
|
| 24 |
+
from agent import DeepResearchAgent
|
| 25 |
+
|
| 26 |
+
# Static files directory (for production deployment)
|
| 27 |
+
STATIC_DIR = Path(__file__).parent.parent / "static"
|
| 28 |
+
|
| 29 |
+
# Add console log handler
|
| 30 |
+
logger.add(
|
| 31 |
+
sys.stderr,
|
| 32 |
+
level="INFO",
|
| 33 |
+
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <4}</level> | <cyan>using_function:{function}</cyan> | <cyan>{file}:{line}</cyan> | <level>{message}</level>",
|
| 34 |
+
colorize=True,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# Add error log handler
|
| 39 |
+
logger.add(
|
| 40 |
+
sink=sys.stderr,
|
| 41 |
+
level="ERROR",
|
| 42 |
+
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <4}</level> | <cyan>using_function:{function}</cyan> | <cyan>{file}:{line}</cyan> | <level>{message}</level>",
|
| 43 |
+
colorize=True,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class ResearchRequest(BaseModel):
|
| 48 |
+
"""Payload for triggering a research run."""
|
| 49 |
+
|
| 50 |
+
topic: str = Field(..., description="Research topic supplied by the user")
|
| 51 |
+
search_api: SearchAPI | None = Field(
|
| 52 |
+
default=None,
|
| 53 |
+
description="Override the default search backend configured via env",
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class ResearchResponse(BaseModel):
|
| 58 |
+
"""HTTP response containing the generated report and structured tasks."""
|
| 59 |
+
|
| 60 |
+
report_markdown: str = Field(
|
| 61 |
+
..., description="Markdown-formatted research report including sections"
|
| 62 |
+
)
|
| 63 |
+
todo_items: list[dict[str, Any]] = Field(
|
| 64 |
+
default_factory=list,
|
| 65 |
+
description="Structured TODO items with summaries and sources",
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _mask_secret(value: Optional[str], visible: int = 4) -> str:
|
| 70 |
+
"""Mask sensitive tokens while keeping leading and trailing characters."""
|
| 71 |
+
if not value:
|
| 72 |
+
return "unset"
|
| 73 |
+
|
| 74 |
+
if len(value) <= visible * 2:
|
| 75 |
+
return "*" * len(value)
|
| 76 |
+
|
| 77 |
+
return f"{value[:visible]}...{value[-visible:]}"
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _build_config(payload: ResearchRequest) -> Configuration:
|
| 81 |
+
overrides: Dict[str, Any] = {}
|
| 82 |
+
|
| 83 |
+
if payload.search_api is not None:
|
| 84 |
+
overrides["search_api"] = payload.search_api
|
| 85 |
+
|
| 86 |
+
return Configuration.from_env(overrides=overrides)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def create_app() -> FastAPI:
|
| 90 |
+
app = FastAPI(title="LangGraph Deep Researcher")
|
| 91 |
+
|
| 92 |
+
app.add_middleware(
|
| 93 |
+
CORSMiddleware,
|
| 94 |
+
allow_origins=["*"],
|
| 95 |
+
allow_credentials=True,
|
| 96 |
+
allow_methods=["*"],
|
| 97 |
+
allow_headers=["*"],
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
@app.on_event("startup")
|
| 101 |
+
def log_startup_configuration() -> None:
|
| 102 |
+
config = Configuration.from_env()
|
| 103 |
+
|
| 104 |
+
if config.llm_provider == "ollama":
|
| 105 |
+
base_url = config.sanitized_ollama_url()
|
| 106 |
+
elif config.llm_provider == "lmstudio":
|
| 107 |
+
base_url = config.lmstudio_base_url
|
| 108 |
+
else:
|
| 109 |
+
base_url = config.llm_base_url or "unset"
|
| 110 |
+
|
| 111 |
+
logger.info(
|
| 112 |
+
"DeepResearch configuration loaded: provider=%s model=%s base_url=%s search_api=%s "
|
| 113 |
+
"max_loops=%s fetch_full_page=%s tool_calling=%s strip_thinking=%s api_key=%s",
|
| 114 |
+
config.llm_provider,
|
| 115 |
+
config.resolved_model() or "unset",
|
| 116 |
+
base_url,
|
| 117 |
+
(config.search_api.value if isinstance(config.search_api, SearchAPI) else config.search_api),
|
| 118 |
+
config.max_web_research_loops,
|
| 119 |
+
config.fetch_full_page,
|
| 120 |
+
config.use_tool_calling,
|
| 121 |
+
config.strip_thinking_tokens,
|
| 122 |
+
_mask_secret(config.llm_api_key),
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
@app.get("/healthz")
|
| 126 |
+
def health_check() -> Dict[str, str]:
|
| 127 |
+
return {"status": "ok"}
|
| 128 |
+
|
| 129 |
+
@app.post("/research", response_model=ResearchResponse)
|
| 130 |
+
def run_research(payload: ResearchRequest) -> ResearchResponse:
|
| 131 |
+
try:
|
| 132 |
+
config = _build_config(payload)
|
| 133 |
+
agent = DeepResearchAgent(config=config)
|
| 134 |
+
result = agent.run(payload.topic)
|
| 135 |
+
except ValueError as exc: # Likely due to unsupported configuration
|
| 136 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 137 |
+
except Exception as exc: # pragma: no cover - defensive guardrail
|
| 138 |
+
raise HTTPException(status_code=500, detail="Research failed") from exc
|
| 139 |
+
|
| 140 |
+
todo_payload = [
|
| 141 |
+
{
|
| 142 |
+
"id": item.id,
|
| 143 |
+
"title": item.title,
|
| 144 |
+
"intent": item.intent,
|
| 145 |
+
"query": item.query,
|
| 146 |
+
"status": item.status,
|
| 147 |
+
"summary": item.summary,
|
| 148 |
+
"sources_summary": item.sources_summary,
|
| 149 |
+
"note_id": item.note_id,
|
| 150 |
+
"note_path": item.note_path,
|
| 151 |
+
}
|
| 152 |
+
for item in result.todo_items
|
| 153 |
+
]
|
| 154 |
+
|
| 155 |
+
return ResearchResponse(
|
| 156 |
+
report_markdown=(result.report_markdown or result.running_summary or ""),
|
| 157 |
+
todo_items=todo_payload,
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
@app.post("/research/stream")
|
| 161 |
+
def stream_research(payload: ResearchRequest) -> StreamingResponse:
|
| 162 |
+
try:
|
| 163 |
+
config = _build_config(payload)
|
| 164 |
+
agent = DeepResearchAgent(config=config)
|
| 165 |
+
except ValueError as exc:
|
| 166 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 167 |
+
|
| 168 |
+
def event_iterator() -> Iterator[str]:
|
| 169 |
+
try:
|
| 170 |
+
for event in agent.run_stream(payload.topic):
|
| 171 |
+
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
|
| 172 |
+
except Exception as exc: # pragma: no cover - defensive guardrail
|
| 173 |
+
logger.exception("Streaming research failed")
|
| 174 |
+
error_payload = {"type": "error", "detail": str(exc)}
|
| 175 |
+
yield f"data: {json.dumps(error_payload, ensure_ascii=False)}\n\n"
|
| 176 |
+
|
| 177 |
+
return StreamingResponse(
|
| 178 |
+
event_iterator(),
|
| 179 |
+
media_type="text/event-stream",
|
| 180 |
+
headers={
|
| 181 |
+
"Cache-Control": "no-cache",
|
| 182 |
+
"Connection": "keep-alive",
|
| 183 |
+
},
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# Serve static frontend files in production (when static directory exists)
|
| 187 |
+
if STATIC_DIR.exists() and STATIC_DIR.is_dir():
|
| 188 |
+
logger.info(f"Serving static files from {STATIC_DIR}")
|
| 189 |
+
|
| 190 |
+
# Mount assets directory first (CSS, JS, images)
|
| 191 |
+
assets_dir = STATIC_DIR / "assets"
|
| 192 |
+
if assets_dir.exists():
|
| 193 |
+
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
|
| 194 |
+
|
| 195 |
+
# Serve index.html for root path
|
| 196 |
+
@app.get("/")
|
| 197 |
+
async def serve_index() -> FileResponse:
|
| 198 |
+
"""Serve the main index.html."""
|
| 199 |
+
return FileResponse(STATIC_DIR / "index.html")
|
| 200 |
+
|
| 201 |
+
# Serve favicon and other root-level static files
|
| 202 |
+
@app.get("/favicon.ico")
|
| 203 |
+
async def serve_favicon() -> FileResponse:
|
| 204 |
+
"""Serve favicon."""
|
| 205 |
+
favicon_path = STATIC_DIR / "favicon.ico"
|
| 206 |
+
if favicon_path.exists():
|
| 207 |
+
return FileResponse(favicon_path)
|
| 208 |
+
raise HTTPException(status_code=404, detail="Favicon not found")
|
| 209 |
+
|
| 210 |
+
# Catch-all for SPA routing (must be last)
|
| 211 |
+
@app.get("/{full_path:path}")
|
| 212 |
+
async def serve_spa(full_path: str) -> FileResponse:
|
| 213 |
+
"""Serve the SPA index.html for client-side routing."""
|
| 214 |
+
# Check if requesting a static file that exists
|
| 215 |
+
file_path = STATIC_DIR / full_path
|
| 216 |
+
if file_path.exists() and file_path.is_file():
|
| 217 |
+
return FileResponse(file_path)
|
| 218 |
+
# Otherwise serve index.html for SPA routing
|
| 219 |
+
return FileResponse(STATIC_DIR / "index.html")
|
| 220 |
+
|
| 221 |
+
return app
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
app = create_app()
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
if __name__ == "__main__":
|
| 228 |
+
import uvicorn
|
| 229 |
+
|
| 230 |
+
uvicorn.run(
|
| 231 |
+
"main:app",
|
| 232 |
+
host="0.0.0.0",
|
| 233 |
+
port=8000,
|
| 234 |
+
reload=True,
|
| 235 |
+
log_level="info"
|
| 236 |
+
)
|
backend/src/models.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""State models used by the deep research workflow."""
|
| 2 |
+
|
| 3 |
+
import operator
|
| 4 |
+
from dataclasses import dataclass, field
|
| 5 |
+
from typing import List, Optional
|
| 6 |
+
|
| 7 |
+
from typing_extensions import Annotated
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass(kw_only=True)
|
| 11 |
+
class TodoItem:
|
| 12 |
+
"""A single todo task item."""
|
| 13 |
+
|
| 14 |
+
id: int
|
| 15 |
+
title: str
|
| 16 |
+
intent: str
|
| 17 |
+
query: str
|
| 18 |
+
status: str = field(default="pending")
|
| 19 |
+
summary: Optional[str] = field(default=None)
|
| 20 |
+
sources_summary: Optional[str] = field(default=None)
|
| 21 |
+
notices: list[str] = field(default_factory=list)
|
| 22 |
+
note_id: Optional[str] = field(default=None)
|
| 23 |
+
note_path: Optional[str] = field(default=None)
|
| 24 |
+
stream_token: Optional[str] = field(default=None)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass(kw_only=True)
|
| 28 |
+
class SummaryState:
|
| 29 |
+
research_topic: str = field(default=None) # Report topic
|
| 30 |
+
search_query: str = field(default=None) # Deprecated placeholder
|
| 31 |
+
web_research_results: Annotated[list, operator.add] = field(default_factory=list)
|
| 32 |
+
sources_gathered: Annotated[list, operator.add] = field(default_factory=list)
|
| 33 |
+
research_loop_count: int = field(default=0) # Research loop count
|
| 34 |
+
running_summary: str = field(default=None) # Legacy summary field
|
| 35 |
+
todo_items: Annotated[list, operator.add] = field(default_factory=list)
|
| 36 |
+
structured_report: Optional[str] = field(default=None)
|
| 37 |
+
report_note_id: Optional[str] = field(default=None)
|
| 38 |
+
report_note_path: Optional[str] = field(default=None)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass(kw_only=True)
|
| 42 |
+
class SummaryStateInput:
|
| 43 |
+
research_topic: str = field(default=None) # Report topic
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@dataclass(kw_only=True)
|
| 47 |
+
class SummaryStateOutput:
|
| 48 |
+
running_summary: str = field(default=None) # Backward-compatible text
|
| 49 |
+
report_markdown: Optional[str] = field(default=None)
|
| 50 |
+
todo_items: List[TodoItem] = field(default_factory=list)
|
| 51 |
+
|
backend/src/prompts.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
# Get current date in a readable format
|
| 5 |
+
def get_current_date():
|
| 6 |
+
return datetime.now().strftime("%B %d, %Y")
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
todo_planner_system_prompt = """
|
| 11 |
+
You are a research planning expert. Please break down complex topics into a limited set of complementary tasks.
|
| 12 |
+
- Tasks should be complementary and avoid duplication;
|
| 13 |
+
- Each task should have a clear intent and actionable search direction;
|
| 14 |
+
- Output must be structured, concise, and suitable for subsequent collaboration.
|
| 15 |
+
|
| 16 |
+
<GOAL>
|
| 17 |
+
1. Based on the research topic, identify 3-5 key research tasks;
|
| 18 |
+
2. Each task needs a clear objective intent and appropriate web search queries;
|
| 19 |
+
3. Tasks should avoid overlap and collectively cover the user's problem domain;
|
| 20 |
+
4. When creating or updating tasks, you must call the `note` tool to sync task information (this is the only way to write to notes).
|
| 21 |
+
</GOAL>
|
| 22 |
+
|
| 23 |
+
<NOTE_COLLAB>
|
| 24 |
+
- Call the `note` tool to create/update structured notes for each task, using JSON parameter format:
|
| 25 |
+
- Create example: `[TOOL_CALL:note:{"action":"create","task_id":1,"title":"Task 1: Background Overview","note_type":"task_state","tags":["deep_research","task_1"],"content":"Please record task overview, system prompts, source overview, task summary"}]`
|
| 26 |
+
- Update example: `[TOOL_CALL:note:{"action":"update","note_id":"<existing_ID>","task_id":1,"title":"Task 1: Background Overview","note_type":"task_state","tags":["deep_research","task_1"],"content":"...new content..."}]`
|
| 27 |
+
- `tags` must include `deep_research` and `task_{task_id}` so other Agents can find them
|
| 28 |
+
</NOTE_COLLAB>
|
| 29 |
+
|
| 30 |
+
<TOOLS>
|
| 31 |
+
You must call the note tool named `note` to record or update tasks, using JSON parameters:
|
| 32 |
+
```
|
| 33 |
+
[TOOL_CALL:note:{"action":"create","task_id":1,"title":"Task 1: Background Overview","note_type":"task_state","tags":["deep_research","task_1"],"content":"..."}]
|
| 34 |
+
```
|
| 35 |
+
</TOOLS>
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
todo_planner_instructions = """
|
| 40 |
+
|
| 41 |
+
<CONTEXT>
|
| 42 |
+
Current date: {current_date}
|
| 43 |
+
Research topic: {research_topic}
|
| 44 |
+
</CONTEXT>
|
| 45 |
+
|
| 46 |
+
<FORMAT>
|
| 47 |
+
Please respond strictly in JSON format:
|
| 48 |
+
{{
|
| 49 |
+
"tasks": [
|
| 50 |
+
{{
|
| 51 |
+
"title": "Task name (within 10 words, highlight key points)",
|
| 52 |
+
"intent": "Core problem the task aims to solve, described in 1-2 sentences",
|
| 53 |
+
"query": "Recommended search keywords"
|
| 54 |
+
}}
|
| 55 |
+
]
|
| 56 |
+
}}
|
| 57 |
+
</FORMAT>
|
| 58 |
+
|
| 59 |
+
If the topic information is insufficient to plan tasks, output an empty array: {{"tasks": []}}. Use the note tool to record your thought process if necessary.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
task_summarizer_instructions = """
|
| 64 |
+
You are a research execution expert. Based on the given context, generate a summary for a specific task. Provide detailed and thorough summaries rather than superficial overviews. Be innovative, break conventional thinking, and expand from multiple dimensions including principles, applications, pros and cons, engineering practices, comparisons, and historical evolution.
|
| 65 |
+
|
| 66 |
+
<GOAL>
|
| 67 |
+
1. Identify 3-5 key findings related to the task intent;
|
| 68 |
+
2. Clearly explain the meaning and value of each finding, citing factual data when available;
|
| 69 |
+
</GOAL>
|
| 70 |
+
|
| 71 |
+
<NOTES>
|
| 72 |
+
- Task notes are created by the planning expert. The note ID will be provided at call time; please first call `[TOOL_CALL:note:{"action":"read","note_id":"<note_id>"}]` to get the latest state.
|
| 73 |
+
- After updating the task summary, use `[TOOL_CALL:note:{"action":"update","note_id":"<note_id>","task_id":{task_id},"title":"Task {task_id}: ...","note_type":"task_state","tags":["deep_research","task_{task_id}"],"content":"..."}]` to write back to notes, maintaining the original structure and appending new information.
|
| 74 |
+
- If no note ID is found, please create one first with `task_{task_id}` in `tags` before continuing.
|
| 75 |
+
</NOTES>
|
| 76 |
+
|
| 77 |
+
<FORMAT>
|
| 78 |
+
- Use Markdown for output;
|
| 79 |
+
- Start with section heading: "Task Summary";
|
| 80 |
+
- Express key findings using ordered or unordered lists;
|
| 81 |
+
- If the task has no valid results, output "No information available".
|
| 82 |
+
- The final summary presented to users must NOT contain `[TOOL_CALL:...]` directives.
|
| 83 |
+
</FORMAT>
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
report_writer_instructions = """
|
| 88 |
+
You are a professional analysis report writer. Based on the input task summaries and reference information, generate a structured research report.
|
| 89 |
+
|
| 90 |
+
<REPORT_TEMPLATE>
|
| 91 |
+
1. **Background Overview**: Briefly describe the importance and context of the research topic.
|
| 92 |
+
2. **Core Insights**: Extract 3-5 most important conclusions, annotated with reference/task numbers.
|
| 93 |
+
3. **Evidence & Data**: List supporting facts or metrics, citing key points from task summaries.
|
| 94 |
+
4. **Risks & Challenges**: Analyze potential issues, limitations, or hypotheses yet to be verified.
|
| 95 |
+
5. **Reference Sources**: List key source entries by task (title + link).
|
| 96 |
+
</REPORT_TEMPLATE>
|
| 97 |
+
|
| 98 |
+
<REQUIREMENTS>
|
| 99 |
+
- Report should use Markdown;
|
| 100 |
+
- Each section should be clearly separated, no additional cover page or epilogue;
|
| 101 |
+
- If information is missing for a section, state "No relevant information available";
|
| 102 |
+
- When citing sources, use task titles or source titles to ensure traceability.
|
| 103 |
+
- Output to users must NOT contain residual `[TOOL_CALL:...]` directives.
|
| 104 |
+
</REQUIREMENTS>
|
| 105 |
+
|
| 106 |
+
<NOTES>
|
| 107 |
+
- Before generating the report, call `[TOOL_CALL:note:{"action":"read","note_id":"<note_id>"}]` for each note_id to read task notes.
|
| 108 |
+
- If you need to save results at the report level, you can create a new `conclusion` type note, for example: `[TOOL_CALL:note:{"action":"create","title":"Research Report: {research_topic}","note_type":"conclusion","tags":["deep_research","report"],"content":"...report highlights..."}]`.
|
| 109 |
+
</NOTES>
|
| 110 |
+
"""
|
backend/src/services/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Domain services for the deep researcher workflow."""
|
| 2 |
+
|
backend/src/services/notes.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Helpers for coordinating note tool usage instructions."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
from models import TodoItem
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def build_note_guidance(task: TodoItem) -> str:
|
| 11 |
+
"""Generate note tool usage guidance for a specific task."""
|
| 12 |
+
|
| 13 |
+
tags_list = ["deep_research", f"task_{task.id}"]
|
| 14 |
+
tags_literal = json.dumps(tags_list, ensure_ascii=False)
|
| 15 |
+
|
| 16 |
+
if task.note_id:
|
| 17 |
+
read_payload = json.dumps({"action": "read", "note_id": task.note_id}, ensure_ascii=False)
|
| 18 |
+
update_payload = json.dumps(
|
| 19 |
+
{
|
| 20 |
+
"action": "update",
|
| 21 |
+
"note_id": task.note_id,
|
| 22 |
+
"task_id": task.id,
|
| 23 |
+
"title": f"Task {task.id}: {task.title}",
|
| 24 |
+
"note_type": "task_state",
|
| 25 |
+
"tags": tags_list,
|
| 26 |
+
"content": "Please add the new information from this round to the task overview",
|
| 27 |
+
},
|
| 28 |
+
ensure_ascii=False,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
return (
|
| 32 |
+
"Note collaboration guide:\n"
|
| 33 |
+
f"- Current task note ID: {task.note_id}.\n"
|
| 34 |
+
f"- Before writing the summary, you must call: [TOOL_CALL:note:{read_payload}] to get the latest content.\n"
|
| 35 |
+
f"- After completing analysis, call: [TOOL_CALL:note:{update_payload}] to sync incremental information.\n"
|
| 36 |
+
"- When updating, maintain the original paragraph structure and add new content to the corresponding sections.\n"
|
| 37 |
+
f"- Recommended to keep tags as {tags_literal} so other Agents can quickly locate it.\n"
|
| 38 |
+
"- After successfully syncing to notes, then output the summary for the user.\n"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
create_payload = json.dumps(
|
| 42 |
+
{
|
| 43 |
+
"action": "create",
|
| 44 |
+
"task_id": task.id,
|
| 45 |
+
"title": f"Task {task.id}: {task.title}",
|
| 46 |
+
"note_type": "task_state",
|
| 47 |
+
"tags": tags_list,
|
| 48 |
+
"content": "Please record task overview, sources overview",
|
| 49 |
+
},
|
| 50 |
+
ensure_ascii=False,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
return (
|
| 54 |
+
"Note collaboration guide:\n"
|
| 55 |
+
f"- No note has been created for this task yet, please first call: [TOOL_CALL:note:{create_payload}].\n"
|
| 56 |
+
"- After successful creation, record the returned note_id and reuse it in all subsequent updates.\n"
|
| 57 |
+
"- After syncing notes, then output the summary for the user.\n"
|
| 58 |
+
)
|
backend/src/services/planner.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Service responsible for converting the research topic into actionable tasks."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import re
|
| 8 |
+
from typing import Any, List, Optional
|
| 9 |
+
|
| 10 |
+
from models import TodoItem
|
| 11 |
+
from config import Configuration
|
| 12 |
+
from prompts import get_current_date, todo_planner_instructions
|
| 13 |
+
from utils import strip_thinking_tokens
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
TOOL_CALL_PATTERN = re.compile(
|
| 18 |
+
r"\[TOOL_CALL:(?P<tool>[^:]+):(?P<body>[^\]]+)\]",
|
| 19 |
+
re.IGNORECASE,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
class PlanningService:
|
| 23 |
+
"""Wraps the planner agent to produce structured TODO items."""
|
| 24 |
+
|
| 25 |
+
def __init__(self, planner_agent: ToolAwareSimpleAgent, config: Configuration) -> None:
|
| 26 |
+
self._agent = planner_agent
|
| 27 |
+
self._config = config
|
| 28 |
+
|
| 29 |
+
def plan_todo_list(self, state: SummaryState) -> List[TodoItem]:
|
| 30 |
+
"""Ask the planner agent to break the topic into actionable tasks."""
|
| 31 |
+
|
| 32 |
+
prompt = todo_planner_instructions.format(
|
| 33 |
+
current_date=get_current_date(),
|
| 34 |
+
research_topic=state.research_topic,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
response = self._agent.run(prompt)
|
| 38 |
+
self._agent.clear_history()
|
| 39 |
+
|
| 40 |
+
logger.info("Planner raw output (truncated): %s", response[:500])
|
| 41 |
+
|
| 42 |
+
tasks_payload = self._extract_tasks(response)
|
| 43 |
+
todo_items: List[TodoItem] = []
|
| 44 |
+
|
| 45 |
+
for idx, item in enumerate(tasks_payload, start=1):
|
| 46 |
+
title = str(item.get("title") or f"Task{idx}").strip()
|
| 47 |
+
intent = str(item.get("intent") or "Focus on key issues of the topic").strip()
|
| 48 |
+
query = str(item.get("query") or state.research_topic).strip()
|
| 49 |
+
|
| 50 |
+
if not query:
|
| 51 |
+
query = state.research_topic
|
| 52 |
+
|
| 53 |
+
task = TodoItem(
|
| 54 |
+
id=idx,
|
| 55 |
+
title=title,
|
| 56 |
+
intent=intent,
|
| 57 |
+
query=query,
|
| 58 |
+
)
|
| 59 |
+
todo_items.append(task)
|
| 60 |
+
|
| 61 |
+
state.todo_items = todo_items
|
| 62 |
+
|
| 63 |
+
titles = [task.title for task in todo_items]
|
| 64 |
+
logger.info("Planner produced %d tasks: %s", len(todo_items), titles)
|
| 65 |
+
return todo_items
|
| 66 |
+
|
| 67 |
+
@staticmethod
|
| 68 |
+
def create_fallback_task(state: SummaryState) -> TodoItem:
|
| 69 |
+
"""Create a minimal fallback task when planning failed."""
|
| 70 |
+
|
| 71 |
+
return TodoItem(
|
| 72 |
+
id=1,
|
| 73 |
+
title="Basic Background Overview",
|
| 74 |
+
intent="Collect core background and latest developments on the topic",
|
| 75 |
+
query=f"{state.research_topic} latest developments" if state.research_topic else "Basic background overview",
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# ------------------------------------------------------------------
|
| 79 |
+
# Parsing helpers
|
| 80 |
+
# ------------------------------------------------------------------
|
| 81 |
+
def _extract_tasks(self, raw_response: str) -> List[dict[str, Any]]:
|
| 82 |
+
"""Parse planner output into a list of task dictionaries."""
|
| 83 |
+
|
| 84 |
+
text = raw_response.strip()
|
| 85 |
+
if self._config.strip_thinking_tokens:
|
| 86 |
+
text = strip_thinking_tokens(text)
|
| 87 |
+
|
| 88 |
+
json_payload = self._extract_json_payload(text)
|
| 89 |
+
tasks: List[dict[str, Any]] = []
|
| 90 |
+
|
| 91 |
+
if isinstance(json_payload, dict):
|
| 92 |
+
candidate = json_payload.get("tasks")
|
| 93 |
+
if isinstance(candidate, list):
|
| 94 |
+
for item in candidate:
|
| 95 |
+
if isinstance(item, dict):
|
| 96 |
+
tasks.append(item)
|
| 97 |
+
elif isinstance(json_payload, list):
|
| 98 |
+
for item in json_payload:
|
| 99 |
+
if isinstance(item, dict):
|
| 100 |
+
tasks.append(item)
|
| 101 |
+
|
| 102 |
+
if not tasks:
|
| 103 |
+
tool_payload = self._extract_tool_payload(text)
|
| 104 |
+
if tool_payload and isinstance(tool_payload.get("tasks"), list):
|
| 105 |
+
for item in tool_payload["tasks"]:
|
| 106 |
+
if isinstance(item, dict):
|
| 107 |
+
tasks.append(item)
|
| 108 |
+
|
| 109 |
+
return tasks
|
| 110 |
+
|
| 111 |
+
def _extract_json_payload(self, text: str) -> Optional[dict[str, Any] | list]:
|
| 112 |
+
"""Try to locate and parse a JSON object or array from the text."""
|
| 113 |
+
|
| 114 |
+
start = text.find("{")
|
| 115 |
+
end = text.rfind("}")
|
| 116 |
+
if start != -1 and end != -1 and end > start:
|
| 117 |
+
candidate = text[start : end + 1]
|
| 118 |
+
try:
|
| 119 |
+
return json.loads(candidate)
|
| 120 |
+
except json.JSONDecodeError:
|
| 121 |
+
pass
|
| 122 |
+
|
| 123 |
+
start = text.find("[")
|
| 124 |
+
end = text.rfind("]")
|
| 125 |
+
if start != -1 and end != -1 and end > start:
|
| 126 |
+
candidate = text[start : end + 1]
|
| 127 |
+
try:
|
| 128 |
+
return json.loads(candidate)
|
| 129 |
+
except json.JSONDecodeError:
|
| 130 |
+
return None
|
| 131 |
+
|
| 132 |
+
return None
|
| 133 |
+
|
| 134 |
+
def _extract_tool_payload(self, text: str) -> Optional[dict[str, Any]]:
|
| 135 |
+
"""Parse the first TOOL_CALL expression in the output."""
|
| 136 |
+
|
| 137 |
+
match = TOOL_CALL_PATTERN.search(text)
|
| 138 |
+
if not match:
|
| 139 |
+
return None
|
| 140 |
+
|
| 141 |
+
body = match.group("body")
|
| 142 |
+
|
| 143 |
+
try:
|
| 144 |
+
payload = json.loads(body)
|
| 145 |
+
if isinstance(payload, dict):
|
| 146 |
+
return payload
|
| 147 |
+
except json.JSONDecodeError:
|
| 148 |
+
pass
|
| 149 |
+
|
| 150 |
+
parts = [segment.strip() for segment in body.split(",") if segment.strip()]
|
| 151 |
+
payload: dict[str, Any] = {}
|
| 152 |
+
for part in parts:
|
| 153 |
+
if "=" not in part:
|
| 154 |
+
continue
|
| 155 |
+
key, value = part.split("=", 1)
|
| 156 |
+
payload[key.strip()] = value.strip().strip('"').strip("'")
|
| 157 |
+
|
| 158 |
+
return payload or None
|
backend/src/services/reporter.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Service that consolidates task results into the final report."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
from langchain_openai import ChatOpenAI
|
| 8 |
+
from langchain_core.messages import HumanMessage, SystemMessage
|
| 9 |
+
|
| 10 |
+
from models import SummaryState
|
| 11 |
+
from config import Configuration
|
| 12 |
+
from utils import strip_thinking_tokens
|
| 13 |
+
from services.text_processing import strip_tool_calls
|
| 14 |
+
from prompts import report_writer_instructions
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def generate_report(
|
| 18 |
+
llm: ChatOpenAI,
|
| 19 |
+
state: SummaryState,
|
| 20 |
+
config: Configuration,
|
| 21 |
+
) -> str:
|
| 22 |
+
"""Generate a structured report based on completed tasks."""
|
| 23 |
+
|
| 24 |
+
tasks_block = []
|
| 25 |
+
for task in state.todo_items:
|
| 26 |
+
summary_block = task.summary or "No information available"
|
| 27 |
+
sources_block = task.sources_summary or "No sources available"
|
| 28 |
+
tasks_block.append(
|
| 29 |
+
f"### Task {task.id}: {task.title}\n"
|
| 30 |
+
f"- Objective: {task.intent}\n"
|
| 31 |
+
f"- Search query: {task.query}\n"
|
| 32 |
+
f"- Status: {task.status}\n"
|
| 33 |
+
f"- Summary:\n{summary_block}\n"
|
| 34 |
+
f"- Sources:\n{sources_block}\n"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
note_references = []
|
| 38 |
+
for task in state.todo_items:
|
| 39 |
+
if task.note_id:
|
| 40 |
+
note_references.append(
|
| 41 |
+
f"- Task {task.id} '{task.title}': note_id={task.note_id}"
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
notes_section = "\n".join(note_references) if note_references else "- No task notes available"
|
| 45 |
+
|
| 46 |
+
read_template = json.dumps({"action": "read", "note_id": "<note_id>"}, ensure_ascii=False)
|
| 47 |
+
create_conclusion_template = json.dumps(
|
| 48 |
+
{
|
| 49 |
+
"action": "create",
|
| 50 |
+
"title": f"Research Report: {state.research_topic}",
|
| 51 |
+
"note_type": "conclusion",
|
| 52 |
+
"tags": ["deep_research", "report"],
|
| 53 |
+
"content": "Please summarize the final report highlights here",
|
| 54 |
+
},
|
| 55 |
+
ensure_ascii=False,
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
prompt = (
|
| 59 |
+
f"Research topic: {state.research_topic}\n"
|
| 60 |
+
f"Task overview:\n{''.join(tasks_block)}\n"
|
| 61 |
+
f"Available task notes:\n{notes_section}\n"
|
| 62 |
+
f"Please use the format [TOOL_CALL:note:{read_template}] to read each task note, then compile all information to write the report.\n"
|
| 63 |
+
f"If you need to output a summary conclusion, you can call [TOOL_CALL:note:{create_conclusion_template}] to save report highlights."
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
messages = [
|
| 67 |
+
SystemMessage(content=report_writer_instructions.strip()),
|
| 68 |
+
HumanMessage(content=prompt),
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
response = llm.invoke(messages)
|
| 72 |
+
report_text = response.content.strip()
|
| 73 |
+
|
| 74 |
+
if config.strip_thinking_tokens:
|
| 75 |
+
report_text = strip_thinking_tokens(report_text)
|
| 76 |
+
|
| 77 |
+
report_text = strip_tool_calls(report_text).strip()
|
| 78 |
+
|
| 79 |
+
return report_text or "Report generation failed, please check input."
|
backend/src/services/search.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Search dispatch helpers using DuckDuckGo and Tavily."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Any, Optional, Tuple
|
| 7 |
+
|
| 8 |
+
from config import Configuration
|
| 9 |
+
from utils import (
|
| 10 |
+
deduplicate_and_format_sources,
|
| 11 |
+
format_sources,
|
| 12 |
+
get_config_value,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
MAX_TOKENS_PER_SOURCE = 2000
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _search_duckduckgo(query: str, max_results: int = 5) -> dict[str, Any]:
|
| 21 |
+
"""Execute search using DuckDuckGo."""
|
| 22 |
+
try:
|
| 23 |
+
from ddgs import DDGS
|
| 24 |
+
|
| 25 |
+
with DDGS() as ddgs:
|
| 26 |
+
results = list(ddgs.text(query, max_results=max_results))
|
| 27 |
+
|
| 28 |
+
formatted_results = []
|
| 29 |
+
for r in results:
|
| 30 |
+
formatted_results.append({
|
| 31 |
+
"title": r.get("title", ""),
|
| 32 |
+
"url": r.get("href", r.get("link", "")),
|
| 33 |
+
"content": r.get("body", r.get("snippet", "")),
|
| 34 |
+
})
|
| 35 |
+
|
| 36 |
+
return {
|
| 37 |
+
"results": formatted_results,
|
| 38 |
+
"backend": "duckduckgo",
|
| 39 |
+
"answer": None,
|
| 40 |
+
"notices": [],
|
| 41 |
+
}
|
| 42 |
+
except Exception as e:
|
| 43 |
+
logger.exception("DuckDuckGo search failed: %s", e)
|
| 44 |
+
return {
|
| 45 |
+
"results": [],
|
| 46 |
+
"backend": "duckduckgo",
|
| 47 |
+
"answer": None,
|
| 48 |
+
"notices": [f"Search failed: {str(e)}"],
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _search_tavily(query: str, max_results: int = 5) -> dict[str, Any]:
|
| 53 |
+
"""Execute search using Tavily API."""
|
| 54 |
+
try:
|
| 55 |
+
import os
|
| 56 |
+
from tavily import TavilyClient
|
| 57 |
+
|
| 58 |
+
api_key = os.getenv("TAVILY_API_KEY")
|
| 59 |
+
if not api_key:
|
| 60 |
+
return {
|
| 61 |
+
"results": [],
|
| 62 |
+
"backend": "tavily",
|
| 63 |
+
"answer": None,
|
| 64 |
+
"notices": ["Missing TAVILY_API_KEY environment variable"],
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
client = TavilyClient(api_key=api_key)
|
| 68 |
+
response = client.search(query, max_results=max_results)
|
| 69 |
+
|
| 70 |
+
formatted_results = []
|
| 71 |
+
for r in response.get("results", []):
|
| 72 |
+
formatted_results.append({
|
| 73 |
+
"title": r.get("title", ""),
|
| 74 |
+
"url": r.get("url", ""),
|
| 75 |
+
"content": r.get("content", ""),
|
| 76 |
+
"raw_content": r.get("raw_content"),
|
| 77 |
+
})
|
| 78 |
+
|
| 79 |
+
return {
|
| 80 |
+
"results": formatted_results,
|
| 81 |
+
"backend": "tavily",
|
| 82 |
+
"answer": response.get("answer"),
|
| 83 |
+
"notices": [],
|
| 84 |
+
}
|
| 85 |
+
except Exception as e:
|
| 86 |
+
logger.exception("Tavily search failed: %s", e)
|
| 87 |
+
return {
|
| 88 |
+
"results": [],
|
| 89 |
+
"backend": "tavily",
|
| 90 |
+
"answer": None,
|
| 91 |
+
"notices": [f"Search failed: {str(e)}"],
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _search_perplexity(query: str, max_results: int = 5) -> dict[str, Any]:
|
| 96 |
+
"""Execute search using Perplexity API."""
|
| 97 |
+
try:
|
| 98 |
+
import os
|
| 99 |
+
from openai import OpenAI
|
| 100 |
+
|
| 101 |
+
api_key = os.getenv("PERPLEXITY_API_KEY")
|
| 102 |
+
if not api_key:
|
| 103 |
+
return {
|
| 104 |
+
"results": [],
|
| 105 |
+
"backend": "perplexity",
|
| 106 |
+
"answer": None,
|
| 107 |
+
"notices": ["Missing PERPLEXITY_API_KEY environment variable"],
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
client = OpenAI(api_key=api_key, base_url="https://api.perplexity.ai")
|
| 111 |
+
|
| 112 |
+
response = client.chat.completions.create(
|
| 113 |
+
model="llama-3.1-sonar-small-128k-online",
|
| 114 |
+
messages=[{"role": "user", "content": query}],
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
answer = response.choices[0].message.content if response.choices else None
|
| 118 |
+
|
| 119 |
+
# Perplexity returns answer text, not structured results
|
| 120 |
+
return {
|
| 121 |
+
"results": [{
|
| 122 |
+
"title": "Perplexity Answer",
|
| 123 |
+
"url": "",
|
| 124 |
+
"content": answer or "",
|
| 125 |
+
}] if answer else [],
|
| 126 |
+
"backend": "perplexity",
|
| 127 |
+
"answer": answer,
|
| 128 |
+
"notices": [],
|
| 129 |
+
}
|
| 130 |
+
except Exception as e:
|
| 131 |
+
logger.exception("Perplexity search failed: %s", e)
|
| 132 |
+
return {
|
| 133 |
+
"results": [],
|
| 134 |
+
"backend": "perplexity",
|
| 135 |
+
"answer": None,
|
| 136 |
+
"notices": [f"Search failed: {str(e)}"],
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def _search_searxng(query: str, max_results: int = 5, base_url: str = "http://localhost:8888") -> dict[str, Any]:
|
| 141 |
+
"""Execute search using SearXNG instance."""
|
| 142 |
+
try:
|
| 143 |
+
import requests
|
| 144 |
+
|
| 145 |
+
params = {
|
| 146 |
+
"q": query,
|
| 147 |
+
"format": "json",
|
| 148 |
+
"engines": "google,bing,duckduckgo",
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
response = requests.get(f"{base_url}/search", params=params, timeout=30)
|
| 152 |
+
response.raise_for_status()
|
| 153 |
+
data = response.json()
|
| 154 |
+
|
| 155 |
+
formatted_results = []
|
| 156 |
+
for r in data.get("results", [])[:max_results]:
|
| 157 |
+
formatted_results.append({
|
| 158 |
+
"title": r.get("title", ""),
|
| 159 |
+
"url": r.get("url", ""),
|
| 160 |
+
"content": r.get("content", ""),
|
| 161 |
+
})
|
| 162 |
+
|
| 163 |
+
return {
|
| 164 |
+
"results": formatted_results,
|
| 165 |
+
"backend": "searxng",
|
| 166 |
+
"answer": None,
|
| 167 |
+
"notices": [],
|
| 168 |
+
}
|
| 169 |
+
except Exception as e:
|
| 170 |
+
logger.exception("SearXNG search failed: %s", e)
|
| 171 |
+
return {
|
| 172 |
+
"results": [],
|
| 173 |
+
"backend": "searxng",
|
| 174 |
+
"answer": None,
|
| 175 |
+
"notices": [f"Search failed: {str(e)}"],
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def dispatch_search(
|
| 180 |
+
query: str,
|
| 181 |
+
config: Configuration,
|
| 182 |
+
loop_count: int,
|
| 183 |
+
) -> Tuple[dict[str, Any] | None, list[str], Optional[str], str]:
|
| 184 |
+
"""Execute configured search backend and normalize response payload."""
|
| 185 |
+
|
| 186 |
+
search_api = get_config_value(config.search_api)
|
| 187 |
+
max_results = 5
|
| 188 |
+
|
| 189 |
+
try:
|
| 190 |
+
if search_api == "tavily":
|
| 191 |
+
payload = _search_tavily(query, max_results)
|
| 192 |
+
elif search_api == "perplexity":
|
| 193 |
+
payload = _search_perplexity(query, max_results)
|
| 194 |
+
elif search_api == "searxng":
|
| 195 |
+
payload = _search_searxng(query, max_results)
|
| 196 |
+
elif search_api == "advanced":
|
| 197 |
+
# Try Tavily first, fall back to DuckDuckGo
|
| 198 |
+
payload = _search_tavily(query, max_results)
|
| 199 |
+
if not payload.get("results"):
|
| 200 |
+
payload = _search_duckduckgo(query, max_results)
|
| 201 |
+
else:
|
| 202 |
+
# Default to DuckDuckGo
|
| 203 |
+
payload = _search_duckduckgo(query, max_results)
|
| 204 |
+
except Exception as exc:
|
| 205 |
+
logger.exception("Search backend %s failed: %s", search_api, exc)
|
| 206 |
+
raise
|
| 207 |
+
|
| 208 |
+
notices = list(payload.get("notices") or [])
|
| 209 |
+
backend_label = str(payload.get("backend") or search_api)
|
| 210 |
+
answer_text = payload.get("answer")
|
| 211 |
+
results = payload.get("results", [])
|
| 212 |
+
|
| 213 |
+
if notices:
|
| 214 |
+
for notice in notices:
|
| 215 |
+
logger.info("Search notice (%s): %s", backend_label, notice)
|
| 216 |
+
|
| 217 |
+
logger.info(
|
| 218 |
+
"Search backend=%s resolved_backend=%s answer=%s results=%s",
|
| 219 |
+
search_api,
|
| 220 |
+
backend_label,
|
| 221 |
+
bool(answer_text),
|
| 222 |
+
len(results),
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
return payload, notices, answer_text, backend_label
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def prepare_research_context(
|
| 229 |
+
search_result: dict[str, Any] | None,
|
| 230 |
+
answer_text: Optional[str],
|
| 231 |
+
config: Configuration,
|
| 232 |
+
) -> tuple[str, str]:
|
| 233 |
+
"""Build structured context and source summary for downstream agents."""
|
| 234 |
+
|
| 235 |
+
sources_summary = format_sources(search_result)
|
| 236 |
+
context = deduplicate_and_format_sources(
|
| 237 |
+
search_result or {"results": []},
|
| 238 |
+
max_tokens_per_source=MAX_TOKENS_PER_SOURCE,
|
| 239 |
+
fetch_full_page=config.fetch_full_page,
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
if answer_text:
|
| 243 |
+
context = f"AI Direct Answer:\n{answer_text}\n\n{context}"
|
| 244 |
+
|
| 245 |
+
return sources_summary, context
|
backend/src/services/summarizer.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Task summarization utilities."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Iterator
|
| 6 |
+
from typing import Tuple, Callable
|
| 7 |
+
|
| 8 |
+
from langchain_openai import ChatOpenAI
|
| 9 |
+
from langchain_core.messages import HumanMessage, SystemMessage
|
| 10 |
+
|
| 11 |
+
from models import SummaryState, TodoItem
|
| 12 |
+
from config import Configuration
|
| 13 |
+
from utils import strip_thinking_tokens
|
| 14 |
+
from services.notes import build_note_guidance
|
| 15 |
+
from services.text_processing import strip_tool_calls
|
| 16 |
+
from prompts import task_summarizer_instructions
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def summarize_task(
|
| 20 |
+
llm: ChatOpenAI,
|
| 21 |
+
state: SummaryState,
|
| 22 |
+
task: TodoItem,
|
| 23 |
+
context: str,
|
| 24 |
+
config: Configuration,
|
| 25 |
+
) -> str:
|
| 26 |
+
"""Generate a task-specific summary using the LLM."""
|
| 27 |
+
|
| 28 |
+
prompt = _build_prompt(state, task, context)
|
| 29 |
+
|
| 30 |
+
messages = [
|
| 31 |
+
SystemMessage(content=task_summarizer_instructions.strip()),
|
| 32 |
+
HumanMessage(content=prompt),
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
response = llm.invoke(messages)
|
| 36 |
+
summary_text = response.content.strip()
|
| 37 |
+
|
| 38 |
+
if config.strip_thinking_tokens:
|
| 39 |
+
summary_text = strip_thinking_tokens(summary_text)
|
| 40 |
+
|
| 41 |
+
summary_text = strip_tool_calls(summary_text).strip()
|
| 42 |
+
|
| 43 |
+
return summary_text or "No information available"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def stream_task_summary(
|
| 47 |
+
llm: ChatOpenAI,
|
| 48 |
+
state: SummaryState,
|
| 49 |
+
task: TodoItem,
|
| 50 |
+
context: str,
|
| 51 |
+
config: Configuration,
|
| 52 |
+
) -> Tuple[Iterator[str], Callable[[], str]]:
|
| 53 |
+
"""Stream the summary text for a task while collecting full output."""
|
| 54 |
+
|
| 55 |
+
prompt = _build_prompt(state, task, context)
|
| 56 |
+
remove_thinking = config.strip_thinking_tokens
|
| 57 |
+
raw_buffer = ""
|
| 58 |
+
visible_output = ""
|
| 59 |
+
emit_index = 0
|
| 60 |
+
|
| 61 |
+
messages = [
|
| 62 |
+
SystemMessage(content=task_summarizer_instructions.strip()),
|
| 63 |
+
HumanMessage(content=prompt),
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
def flush_visible() -> Iterator[str]:
|
| 67 |
+
nonlocal emit_index, raw_buffer
|
| 68 |
+
while True:
|
| 69 |
+
start = raw_buffer.find("<think>", emit_index)
|
| 70 |
+
if start == -1:
|
| 71 |
+
if emit_index < len(raw_buffer):
|
| 72 |
+
segment = raw_buffer[emit_index:]
|
| 73 |
+
emit_index = len(raw_buffer)
|
| 74 |
+
if segment:
|
| 75 |
+
yield segment
|
| 76 |
+
break
|
| 77 |
+
|
| 78 |
+
if start > emit_index:
|
| 79 |
+
segment = raw_buffer[emit_index:start]
|
| 80 |
+
emit_index = start
|
| 81 |
+
if segment:
|
| 82 |
+
yield segment
|
| 83 |
+
|
| 84 |
+
end = raw_buffer.find("</think>", start)
|
| 85 |
+
if end == -1:
|
| 86 |
+
break
|
| 87 |
+
emit_index = end + len("</think>")
|
| 88 |
+
|
| 89 |
+
def generator() -> Iterator[str]:
|
| 90 |
+
nonlocal raw_buffer, visible_output, emit_index
|
| 91 |
+
try:
|
| 92 |
+
for chunk in llm.stream(messages):
|
| 93 |
+
chunk_text = chunk.content
|
| 94 |
+
if not chunk_text:
|
| 95 |
+
continue
|
| 96 |
+
raw_buffer += chunk_text
|
| 97 |
+
if remove_thinking:
|
| 98 |
+
for segment in flush_visible():
|
| 99 |
+
visible_output += segment
|
| 100 |
+
if segment:
|
| 101 |
+
yield segment
|
| 102 |
+
else:
|
| 103 |
+
visible_output += chunk_text
|
| 104 |
+
if chunk_text:
|
| 105 |
+
yield chunk_text
|
| 106 |
+
finally:
|
| 107 |
+
if remove_thinking:
|
| 108 |
+
for segment in flush_visible():
|
| 109 |
+
visible_output += segment
|
| 110 |
+
if segment:
|
| 111 |
+
yield segment
|
| 112 |
+
|
| 113 |
+
def get_summary() -> str:
|
| 114 |
+
if remove_thinking:
|
| 115 |
+
cleaned = strip_thinking_tokens(visible_output)
|
| 116 |
+
else:
|
| 117 |
+
cleaned = visible_output
|
| 118 |
+
|
| 119 |
+
return strip_tool_calls(cleaned).strip()
|
| 120 |
+
|
| 121 |
+
return generator(), get_summary
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _build_prompt(state: SummaryState, task: TodoItem, context: str) -> str:
|
| 125 |
+
"""Construct the summarization prompt shared by both modes."""
|
| 126 |
+
|
| 127 |
+
return (
|
| 128 |
+
f"Task topic: {state.research_topic}\n"
|
| 129 |
+
f"Task name: {task.title}\n"
|
| 130 |
+
f"Task objective: {task.intent}\n"
|
| 131 |
+
f"Search query: {task.query}\n"
|
| 132 |
+
f"Task context:\n{context}\n"
|
| 133 |
+
f"{build_note_guidance(task)}\n"
|
| 134 |
+
"Please follow the above collaboration requirements to sync notes first, then return a user-facing Markdown summary (still following the task summary template)."
|
| 135 |
+
)
|
backend/src/services/text_processing.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utility helpers for normalizing agent generated text."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import re
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def strip_tool_calls(text: str) -> str:
|
| 9 |
+
"""Remove tool call markers from text."""
|
| 10 |
+
|
| 11 |
+
if not text:
|
| 12 |
+
return text
|
| 13 |
+
|
| 14 |
+
pattern = re.compile(r"\[TOOL_CALL:[^\]]+\]")
|
| 15 |
+
return pattern.sub("", text)
|
| 16 |
+
|
backend/src/services/tool_events.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utility for collecting and exposing tool call events."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import re
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from threading import Lock
|
| 10 |
+
from typing import Any, Callable, Optional
|
| 11 |
+
|
| 12 |
+
from models import SummaryState, TodoItem
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class ToolCallEvent:
|
| 19 |
+
"""Internal representation of a tool call event."""
|
| 20 |
+
|
| 21 |
+
id: int
|
| 22 |
+
agent: str
|
| 23 |
+
tool: str
|
| 24 |
+
raw_parameters: str
|
| 25 |
+
parsed_parameters: dict[str, Any]
|
| 26 |
+
result: str
|
| 27 |
+
task_id: Optional[int]
|
| 28 |
+
note_id: Optional[str]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ToolCallTracker:
|
| 32 |
+
"""Collects tool call events and converts them to SSE payloads."""
|
| 33 |
+
|
| 34 |
+
def __init__(self, notes_workspace: Optional[str]) -> None:
|
| 35 |
+
self._notes_workspace = notes_workspace
|
| 36 |
+
self._events: list[ToolCallEvent] = []
|
| 37 |
+
self._cursor = 0
|
| 38 |
+
self._lock = Lock()
|
| 39 |
+
self._event_sink: Optional[Callable[[dict[str, Any]], None]] = None
|
| 40 |
+
|
| 41 |
+
def record(self, payload: dict[str, Any]) -> None:
|
| 42 |
+
"""Record a tool call event for logging and frontend display."""
|
| 43 |
+
|
| 44 |
+
agent_name = str(payload.get("agent_name") or payload.get("agent") or "unknown")
|
| 45 |
+
tool_name = str(payload.get("tool_name") or payload.get("tool") or "unknown")
|
| 46 |
+
raw_parameters = str(payload.get("raw_parameters") or "")
|
| 47 |
+
parsed_parameters = payload.get("parsed_parameters") or payload.get("parameters") or {}
|
| 48 |
+
result_text = str(payload.get("result") or "")
|
| 49 |
+
|
| 50 |
+
if not isinstance(parsed_parameters, dict):
|
| 51 |
+
parsed_parameters = {}
|
| 52 |
+
|
| 53 |
+
task_id = self._infer_task_id(parsed_parameters)
|
| 54 |
+
note_id: Optional[str] = payload.get("note_id")
|
| 55 |
+
|
| 56 |
+
if tool_name == "note" and note_id is None:
|
| 57 |
+
note_id = parsed_parameters.get("note_id")
|
| 58 |
+
if note_id is None:
|
| 59 |
+
note_id = self._extract_note_id(result_text)
|
| 60 |
+
|
| 61 |
+
event = ToolCallEvent(
|
| 62 |
+
id=len(self._events) + 1,
|
| 63 |
+
agent=agent_name,
|
| 64 |
+
tool=tool_name,
|
| 65 |
+
raw_parameters=raw_parameters,
|
| 66 |
+
parsed_parameters=parsed_parameters,
|
| 67 |
+
result=result_text,
|
| 68 |
+
task_id=task_id,
|
| 69 |
+
note_id=note_id,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
with self._lock:
|
| 73 |
+
self._events.append(event)
|
| 74 |
+
|
| 75 |
+
logger.info(
|
| 76 |
+
"Tool call recorded: agent=%s tool=%s task_id=%s note_id=%s parsed_parameters=%s",
|
| 77 |
+
agent_name,
|
| 78 |
+
tool_name,
|
| 79 |
+
task_id,
|
| 80 |
+
note_id,
|
| 81 |
+
parsed_parameters,
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
sink = self._event_sink
|
| 85 |
+
if sink:
|
| 86 |
+
sink(self._build_payload(event, step=None))
|
| 87 |
+
|
| 88 |
+
# ------------------------------------------------------------------
|
| 89 |
+
# Draining helpers
|
| 90 |
+
# ------------------------------------------------------------------
|
| 91 |
+
def drain(self, state: Optional[SummaryState] = None, *, step: Optional[int] = None) -> list[dict[str, Any]]:
|
| 92 |
+
"""Extract unconsumed tool call events and sync task note_ids."""
|
| 93 |
+
|
| 94 |
+
with self._lock:
|
| 95 |
+
if self._cursor >= len(self._events):
|
| 96 |
+
return []
|
| 97 |
+
new_events = self._events[self._cursor :]
|
| 98 |
+
self._cursor = len(self._events)
|
| 99 |
+
|
| 100 |
+
if state and state.todo_items:
|
| 101 |
+
for event in new_events:
|
| 102 |
+
task_id = event.task_id
|
| 103 |
+
note_id = event.note_id
|
| 104 |
+
if task_id is None or not note_id:
|
| 105 |
+
continue
|
| 106 |
+
self._attach_note_to_task(state.todo_items, task_id, note_id)
|
| 107 |
+
|
| 108 |
+
payloads: list[dict[str, Any]] = []
|
| 109 |
+
for event in new_events:
|
| 110 |
+
payload = self._build_payload(event, step=step)
|
| 111 |
+
payloads.append(payload)
|
| 112 |
+
|
| 113 |
+
return payloads
|
| 114 |
+
|
| 115 |
+
def reset(self) -> None:
|
| 116 |
+
"""Clear recorded events."""
|
| 117 |
+
|
| 118 |
+
with self._lock:
|
| 119 |
+
self._events.clear()
|
| 120 |
+
self._cursor = 0
|
| 121 |
+
|
| 122 |
+
def as_dicts(self) -> list[dict[str, Any]]:
|
| 123 |
+
"""Expose a snapshot of raw events for backwards compatibility."""
|
| 124 |
+
|
| 125 |
+
with self._lock:
|
| 126 |
+
return [
|
| 127 |
+
{
|
| 128 |
+
"id": event.id,
|
| 129 |
+
"agent": event.agent,
|
| 130 |
+
"tool": event.tool,
|
| 131 |
+
"raw_parameters": event.raw_parameters,
|
| 132 |
+
"parsed_parameters": event.parsed_parameters,
|
| 133 |
+
"result": event.result,
|
| 134 |
+
"task_id": event.task_id,
|
| 135 |
+
"note_id": event.note_id,
|
| 136 |
+
}
|
| 137 |
+
for event in self._events
|
| 138 |
+
]
|
| 139 |
+
|
| 140 |
+
def set_event_sink(self, sink: Optional[Callable[[dict[str, Any]], None]]) -> None:
|
| 141 |
+
"""Register a callback for immediate tool event notifications."""
|
| 142 |
+
|
| 143 |
+
self._event_sink = sink
|
| 144 |
+
|
| 145 |
+
def _build_payload(self, event: ToolCallEvent, step: Optional[int]) -> dict[str, Any]:
|
| 146 |
+
payload = {
|
| 147 |
+
"type": "tool_call",
|
| 148 |
+
"event_id": event.id,
|
| 149 |
+
"agent": event.agent,
|
| 150 |
+
"tool": event.tool,
|
| 151 |
+
"parameters": event.parsed_parameters,
|
| 152 |
+
"result": event.result,
|
| 153 |
+
"task_id": event.task_id,
|
| 154 |
+
"note_id": event.note_id,
|
| 155 |
+
}
|
| 156 |
+
if event.note_id and self._notes_workspace:
|
| 157 |
+
note_path = Path(self._notes_workspace) / f"{event.note_id}.md"
|
| 158 |
+
payload["note_path"] = str(note_path)
|
| 159 |
+
if step is not None:
|
| 160 |
+
payload["step"] = step
|
| 161 |
+
return payload
|
| 162 |
+
|
| 163 |
+
# ------------------------------------------------------------------
|
| 164 |
+
# Internal helpers
|
| 165 |
+
# ------------------------------------------------------------------
|
| 166 |
+
def _attach_note_to_task(self, tasks: list[TodoItem], task_id: int, note_id: str) -> None:
|
| 167 |
+
"""Update matching TODO item with note metadata."""
|
| 168 |
+
|
| 169 |
+
for task in tasks:
|
| 170 |
+
if task.id != task_id:
|
| 171 |
+
continue
|
| 172 |
+
|
| 173 |
+
if task.note_id != note_id:
|
| 174 |
+
task.note_id = note_id
|
| 175 |
+
if self._notes_workspace:
|
| 176 |
+
task.note_path = str(Path(self._notes_workspace) / f"{note_id}.md")
|
| 177 |
+
elif task.note_path is None and self._notes_workspace:
|
| 178 |
+
task.note_path = str(Path(self._notes_workspace) / f"{note_id}.md")
|
| 179 |
+
break
|
| 180 |
+
|
| 181 |
+
def _infer_task_id(self, parameters: dict[str, Any]) -> Optional[int]:
|
| 182 |
+
"""Attempt to infer task_id from tool parameters."""
|
| 183 |
+
|
| 184 |
+
if not parameters:
|
| 185 |
+
return None
|
| 186 |
+
|
| 187 |
+
if "task_id" in parameters:
|
| 188 |
+
try:
|
| 189 |
+
return int(parameters["task_id"])
|
| 190 |
+
except (TypeError, ValueError):
|
| 191 |
+
pass
|
| 192 |
+
|
| 193 |
+
tags = parameters.get("tags")
|
| 194 |
+
if isinstance(tags, list):
|
| 195 |
+
for tag in tags:
|
| 196 |
+
match = re.search(r"task_(\d+)", str(tag))
|
| 197 |
+
if match:
|
| 198 |
+
return int(match.group(1))
|
| 199 |
+
|
| 200 |
+
title = parameters.get("title")
|
| 201 |
+
if isinstance(title, str):
|
| 202 |
+
match = re.search(r"Task\s*(\d+)", title)
|
| 203 |
+
if match:
|
| 204 |
+
return int(match.group(1))
|
| 205 |
+
|
| 206 |
+
return None
|
| 207 |
+
|
| 208 |
+
def _extract_note_id(self, response: str) -> Optional[str]:
|
| 209 |
+
if not response:
|
| 210 |
+
return None
|
| 211 |
+
|
| 212 |
+
match = re.search(r"ID:\s*([^\n]+)", response)
|
| 213 |
+
if match:
|
| 214 |
+
return match.group(1).strip()
|
| 215 |
+
return None
|
backend/src/utils.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utility helpers shared across deep researcher services."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Any, Dict, List, Union
|
| 7 |
+
|
| 8 |
+
CHARS_PER_TOKEN = 4
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def get_config_value(value: Any) -> str:
|
| 14 |
+
"""Return configuration value as plain string."""
|
| 15 |
+
|
| 16 |
+
return value if isinstance(value, str) else value.value
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def strip_thinking_tokens(text: str) -> str:
|
| 20 |
+
"""Remove ``<think>`` sections from model responses."""
|
| 21 |
+
|
| 22 |
+
while "<think>" in text and "</think>" in text:
|
| 23 |
+
start = text.find("<think>")
|
| 24 |
+
end = text.find("</think>") + len("</think>")
|
| 25 |
+
text = text[:start] + text[end:]
|
| 26 |
+
return text
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def deduplicate_and_format_sources(
|
| 30 |
+
search_response: Dict[str, Any] | List[Dict[str, Any]],
|
| 31 |
+
max_tokens_per_source: int,
|
| 32 |
+
*,
|
| 33 |
+
fetch_full_page: bool = False,
|
| 34 |
+
) -> str:
|
| 35 |
+
"""Format and deduplicate search results for downstream prompting."""
|
| 36 |
+
|
| 37 |
+
if isinstance(search_response, dict):
|
| 38 |
+
sources_list = search_response.get("results", [])
|
| 39 |
+
else:
|
| 40 |
+
sources_list = search_response
|
| 41 |
+
|
| 42 |
+
unique_sources: dict[str, Dict[str, Any]] = {}
|
| 43 |
+
for source in sources_list:
|
| 44 |
+
url = source.get("url")
|
| 45 |
+
if not url:
|
| 46 |
+
continue
|
| 47 |
+
if url not in unique_sources:
|
| 48 |
+
unique_sources[url] = source
|
| 49 |
+
|
| 50 |
+
formatted_parts: List[str] = []
|
| 51 |
+
for source in unique_sources.values():
|
| 52 |
+
title = source.get("title") or source.get("url", "")
|
| 53 |
+
content = source.get("content", "")
|
| 54 |
+
formatted_parts.append(f"Source: {title}\n\n")
|
| 55 |
+
formatted_parts.append(f"URL: {source.get('url', '')}\n\n")
|
| 56 |
+
formatted_parts.append(f"Content: {content}\n\n")
|
| 57 |
+
|
| 58 |
+
if fetch_full_page:
|
| 59 |
+
raw_content = source.get("raw_content")
|
| 60 |
+
if raw_content is None:
|
| 61 |
+
logger.debug("raw_content missing for %s", source.get("url", ""))
|
| 62 |
+
raw_content = ""
|
| 63 |
+
char_limit = max_tokens_per_source * CHARS_PER_TOKEN
|
| 64 |
+
if len(raw_content) > char_limit:
|
| 65 |
+
raw_content = f"{raw_content[:char_limit]}... [truncated]"
|
| 66 |
+
formatted_parts.append(
|
| 67 |
+
f"Full content limited to {max_tokens_per_source} tokens: {raw_content}\n\n"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
return "".join(formatted_parts).strip()
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def format_sources(search_results: Dict[str, Any] | None) -> str:
|
| 74 |
+
"""Return bullet list summarising search sources."""
|
| 75 |
+
|
| 76 |
+
if not search_results:
|
| 77 |
+
return ""
|
| 78 |
+
|
| 79 |
+
results = search_results.get("results", [])
|
| 80 |
+
return "\n".join(
|
| 81 |
+
f"* {item.get('title', item.get('url', ''))} : {item.get('url', '')}"
|
| 82 |
+
for item in results
|
| 83 |
+
if item.get("url")
|
| 84 |
+
)
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
dist
|
| 3 |
+
.DS_Store
|
frontend/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>HelloAgents Deep Research Assistant</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<div id="app"></div>
|
| 10 |
+
<script type="module" src="/src/main.ts"></script>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|
frontend/package-lock.json
ADDED
|
@@ -0,0 +1,1758 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "helloagents-deepresearch-frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "helloagents-deepresearch-frontend",
|
| 9 |
+
"version": "0.1.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"axios": "^1.7.9",
|
| 12 |
+
"vue": "^3.5.13"
|
| 13 |
+
},
|
| 14 |
+
"devDependencies": {
|
| 15 |
+
"@types/node": "^22.10.5",
|
| 16 |
+
"@vitejs/plugin-vue": "^5.2.1",
|
| 17 |
+
"typescript": "^5.7.3",
|
| 18 |
+
"vite": "^6.0.7",
|
| 19 |
+
"vue-tsc": "^2.2.0"
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
"node_modules/@babel/helper-string-parser": {
|
| 23 |
+
"version": "7.27.1",
|
| 24 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 25 |
+
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
| 26 |
+
"license": "MIT",
|
| 27 |
+
"engines": {
|
| 28 |
+
"node": ">=6.9.0"
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
"node_modules/@babel/helper-validator-identifier": {
|
| 32 |
+
"version": "7.27.1",
|
| 33 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
| 34 |
+
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
| 35 |
+
"license": "MIT",
|
| 36 |
+
"engines": {
|
| 37 |
+
"node": ">=6.9.0"
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"node_modules/@babel/parser": {
|
| 41 |
+
"version": "7.28.4",
|
| 42 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
|
| 43 |
+
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
| 44 |
+
"license": "MIT",
|
| 45 |
+
"dependencies": {
|
| 46 |
+
"@babel/types": "^7.28.4"
|
| 47 |
+
},
|
| 48 |
+
"bin": {
|
| 49 |
+
"parser": "bin/babel-parser.js"
|
| 50 |
+
},
|
| 51 |
+
"engines": {
|
| 52 |
+
"node": ">=6.0.0"
|
| 53 |
+
}
|
| 54 |
+
},
|
| 55 |
+
"node_modules/@babel/types": {
|
| 56 |
+
"version": "7.28.4",
|
| 57 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
|
| 58 |
+
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
|
| 59 |
+
"license": "MIT",
|
| 60 |
+
"dependencies": {
|
| 61 |
+
"@babel/helper-string-parser": "^7.27.1",
|
| 62 |
+
"@babel/helper-validator-identifier": "^7.27.1"
|
| 63 |
+
},
|
| 64 |
+
"engines": {
|
| 65 |
+
"node": ">=6.9.0"
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 69 |
+
"version": "0.25.11",
|
| 70 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
|
| 71 |
+
"integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==",
|
| 72 |
+
"cpu": [
|
| 73 |
+
"ppc64"
|
| 74 |
+
],
|
| 75 |
+
"dev": true,
|
| 76 |
+
"license": "MIT",
|
| 77 |
+
"optional": true,
|
| 78 |
+
"os": [
|
| 79 |
+
"aix"
|
| 80 |
+
],
|
| 81 |
+
"engines": {
|
| 82 |
+
"node": ">=18"
|
| 83 |
+
}
|
| 84 |
+
},
|
| 85 |
+
"node_modules/@esbuild/android-arm": {
|
| 86 |
+
"version": "0.25.11",
|
| 87 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz",
|
| 88 |
+
"integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==",
|
| 89 |
+
"cpu": [
|
| 90 |
+
"arm"
|
| 91 |
+
],
|
| 92 |
+
"dev": true,
|
| 93 |
+
"license": "MIT",
|
| 94 |
+
"optional": true,
|
| 95 |
+
"os": [
|
| 96 |
+
"android"
|
| 97 |
+
],
|
| 98 |
+
"engines": {
|
| 99 |
+
"node": ">=18"
|
| 100 |
+
}
|
| 101 |
+
},
|
| 102 |
+
"node_modules/@esbuild/android-arm64": {
|
| 103 |
+
"version": "0.25.11",
|
| 104 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz",
|
| 105 |
+
"integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==",
|
| 106 |
+
"cpu": [
|
| 107 |
+
"arm64"
|
| 108 |
+
],
|
| 109 |
+
"dev": true,
|
| 110 |
+
"license": "MIT",
|
| 111 |
+
"optional": true,
|
| 112 |
+
"os": [
|
| 113 |
+
"android"
|
| 114 |
+
],
|
| 115 |
+
"engines": {
|
| 116 |
+
"node": ">=18"
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
"node_modules/@esbuild/android-x64": {
|
| 120 |
+
"version": "0.25.11",
|
| 121 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz",
|
| 122 |
+
"integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==",
|
| 123 |
+
"cpu": [
|
| 124 |
+
"x64"
|
| 125 |
+
],
|
| 126 |
+
"dev": true,
|
| 127 |
+
"license": "MIT",
|
| 128 |
+
"optional": true,
|
| 129 |
+
"os": [
|
| 130 |
+
"android"
|
| 131 |
+
],
|
| 132 |
+
"engines": {
|
| 133 |
+
"node": ">=18"
|
| 134 |
+
}
|
| 135 |
+
},
|
| 136 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 137 |
+
"version": "0.25.11",
|
| 138 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz",
|
| 139 |
+
"integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==",
|
| 140 |
+
"cpu": [
|
| 141 |
+
"arm64"
|
| 142 |
+
],
|
| 143 |
+
"dev": true,
|
| 144 |
+
"license": "MIT",
|
| 145 |
+
"optional": true,
|
| 146 |
+
"os": [
|
| 147 |
+
"darwin"
|
| 148 |
+
],
|
| 149 |
+
"engines": {
|
| 150 |
+
"node": ">=18"
|
| 151 |
+
}
|
| 152 |
+
},
|
| 153 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 154 |
+
"version": "0.25.11",
|
| 155 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz",
|
| 156 |
+
"integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==",
|
| 157 |
+
"cpu": [
|
| 158 |
+
"x64"
|
| 159 |
+
],
|
| 160 |
+
"dev": true,
|
| 161 |
+
"license": "MIT",
|
| 162 |
+
"optional": true,
|
| 163 |
+
"os": [
|
| 164 |
+
"darwin"
|
| 165 |
+
],
|
| 166 |
+
"engines": {
|
| 167 |
+
"node": ">=18"
|
| 168 |
+
}
|
| 169 |
+
},
|
| 170 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 171 |
+
"version": "0.25.11",
|
| 172 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz",
|
| 173 |
+
"integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==",
|
| 174 |
+
"cpu": [
|
| 175 |
+
"arm64"
|
| 176 |
+
],
|
| 177 |
+
"dev": true,
|
| 178 |
+
"license": "MIT",
|
| 179 |
+
"optional": true,
|
| 180 |
+
"os": [
|
| 181 |
+
"freebsd"
|
| 182 |
+
],
|
| 183 |
+
"engines": {
|
| 184 |
+
"node": ">=18"
|
| 185 |
+
}
|
| 186 |
+
},
|
| 187 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 188 |
+
"version": "0.25.11",
|
| 189 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz",
|
| 190 |
+
"integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==",
|
| 191 |
+
"cpu": [
|
| 192 |
+
"x64"
|
| 193 |
+
],
|
| 194 |
+
"dev": true,
|
| 195 |
+
"license": "MIT",
|
| 196 |
+
"optional": true,
|
| 197 |
+
"os": [
|
| 198 |
+
"freebsd"
|
| 199 |
+
],
|
| 200 |
+
"engines": {
|
| 201 |
+
"node": ">=18"
|
| 202 |
+
}
|
| 203 |
+
},
|
| 204 |
+
"node_modules/@esbuild/linux-arm": {
|
| 205 |
+
"version": "0.25.11",
|
| 206 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz",
|
| 207 |
+
"integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==",
|
| 208 |
+
"cpu": [
|
| 209 |
+
"arm"
|
| 210 |
+
],
|
| 211 |
+
"dev": true,
|
| 212 |
+
"license": "MIT",
|
| 213 |
+
"optional": true,
|
| 214 |
+
"os": [
|
| 215 |
+
"linux"
|
| 216 |
+
],
|
| 217 |
+
"engines": {
|
| 218 |
+
"node": ">=18"
|
| 219 |
+
}
|
| 220 |
+
},
|
| 221 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 222 |
+
"version": "0.25.11",
|
| 223 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz",
|
| 224 |
+
"integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==",
|
| 225 |
+
"cpu": [
|
| 226 |
+
"arm64"
|
| 227 |
+
],
|
| 228 |
+
"dev": true,
|
| 229 |
+
"license": "MIT",
|
| 230 |
+
"optional": true,
|
| 231 |
+
"os": [
|
| 232 |
+
"linux"
|
| 233 |
+
],
|
| 234 |
+
"engines": {
|
| 235 |
+
"node": ">=18"
|
| 236 |
+
}
|
| 237 |
+
},
|
| 238 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 239 |
+
"version": "0.25.11",
|
| 240 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz",
|
| 241 |
+
"integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==",
|
| 242 |
+
"cpu": [
|
| 243 |
+
"ia32"
|
| 244 |
+
],
|
| 245 |
+
"dev": true,
|
| 246 |
+
"license": "MIT",
|
| 247 |
+
"optional": true,
|
| 248 |
+
"os": [
|
| 249 |
+
"linux"
|
| 250 |
+
],
|
| 251 |
+
"engines": {
|
| 252 |
+
"node": ">=18"
|
| 253 |
+
}
|
| 254 |
+
},
|
| 255 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 256 |
+
"version": "0.25.11",
|
| 257 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz",
|
| 258 |
+
"integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==",
|
| 259 |
+
"cpu": [
|
| 260 |
+
"loong64"
|
| 261 |
+
],
|
| 262 |
+
"dev": true,
|
| 263 |
+
"license": "MIT",
|
| 264 |
+
"optional": true,
|
| 265 |
+
"os": [
|
| 266 |
+
"linux"
|
| 267 |
+
],
|
| 268 |
+
"engines": {
|
| 269 |
+
"node": ">=18"
|
| 270 |
+
}
|
| 271 |
+
},
|
| 272 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 273 |
+
"version": "0.25.11",
|
| 274 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz",
|
| 275 |
+
"integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==",
|
| 276 |
+
"cpu": [
|
| 277 |
+
"mips64el"
|
| 278 |
+
],
|
| 279 |
+
"dev": true,
|
| 280 |
+
"license": "MIT",
|
| 281 |
+
"optional": true,
|
| 282 |
+
"os": [
|
| 283 |
+
"linux"
|
| 284 |
+
],
|
| 285 |
+
"engines": {
|
| 286 |
+
"node": ">=18"
|
| 287 |
+
}
|
| 288 |
+
},
|
| 289 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 290 |
+
"version": "0.25.11",
|
| 291 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz",
|
| 292 |
+
"integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==",
|
| 293 |
+
"cpu": [
|
| 294 |
+
"ppc64"
|
| 295 |
+
],
|
| 296 |
+
"dev": true,
|
| 297 |
+
"license": "MIT",
|
| 298 |
+
"optional": true,
|
| 299 |
+
"os": [
|
| 300 |
+
"linux"
|
| 301 |
+
],
|
| 302 |
+
"engines": {
|
| 303 |
+
"node": ">=18"
|
| 304 |
+
}
|
| 305 |
+
},
|
| 306 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 307 |
+
"version": "0.25.11",
|
| 308 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz",
|
| 309 |
+
"integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==",
|
| 310 |
+
"cpu": [
|
| 311 |
+
"riscv64"
|
| 312 |
+
],
|
| 313 |
+
"dev": true,
|
| 314 |
+
"license": "MIT",
|
| 315 |
+
"optional": true,
|
| 316 |
+
"os": [
|
| 317 |
+
"linux"
|
| 318 |
+
],
|
| 319 |
+
"engines": {
|
| 320 |
+
"node": ">=18"
|
| 321 |
+
}
|
| 322 |
+
},
|
| 323 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 324 |
+
"version": "0.25.11",
|
| 325 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz",
|
| 326 |
+
"integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==",
|
| 327 |
+
"cpu": [
|
| 328 |
+
"s390x"
|
| 329 |
+
],
|
| 330 |
+
"dev": true,
|
| 331 |
+
"license": "MIT",
|
| 332 |
+
"optional": true,
|
| 333 |
+
"os": [
|
| 334 |
+
"linux"
|
| 335 |
+
],
|
| 336 |
+
"engines": {
|
| 337 |
+
"node": ">=18"
|
| 338 |
+
}
|
| 339 |
+
},
|
| 340 |
+
"node_modules/@esbuild/linux-x64": {
|
| 341 |
+
"version": "0.25.11",
|
| 342 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz",
|
| 343 |
+
"integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==",
|
| 344 |
+
"cpu": [
|
| 345 |
+
"x64"
|
| 346 |
+
],
|
| 347 |
+
"dev": true,
|
| 348 |
+
"license": "MIT",
|
| 349 |
+
"optional": true,
|
| 350 |
+
"os": [
|
| 351 |
+
"linux"
|
| 352 |
+
],
|
| 353 |
+
"engines": {
|
| 354 |
+
"node": ">=18"
|
| 355 |
+
}
|
| 356 |
+
},
|
| 357 |
+
"node_modules/@esbuild/netbsd-arm64": {
|
| 358 |
+
"version": "0.25.11",
|
| 359 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz",
|
| 360 |
+
"integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==",
|
| 361 |
+
"cpu": [
|
| 362 |
+
"arm64"
|
| 363 |
+
],
|
| 364 |
+
"dev": true,
|
| 365 |
+
"license": "MIT",
|
| 366 |
+
"optional": true,
|
| 367 |
+
"os": [
|
| 368 |
+
"netbsd"
|
| 369 |
+
],
|
| 370 |
+
"engines": {
|
| 371 |
+
"node": ">=18"
|
| 372 |
+
}
|
| 373 |
+
},
|
| 374 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 375 |
+
"version": "0.25.11",
|
| 376 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz",
|
| 377 |
+
"integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==",
|
| 378 |
+
"cpu": [
|
| 379 |
+
"x64"
|
| 380 |
+
],
|
| 381 |
+
"dev": true,
|
| 382 |
+
"license": "MIT",
|
| 383 |
+
"optional": true,
|
| 384 |
+
"os": [
|
| 385 |
+
"netbsd"
|
| 386 |
+
],
|
| 387 |
+
"engines": {
|
| 388 |
+
"node": ">=18"
|
| 389 |
+
}
|
| 390 |
+
},
|
| 391 |
+
"node_modules/@esbuild/openbsd-arm64": {
|
| 392 |
+
"version": "0.25.11",
|
| 393 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz",
|
| 394 |
+
"integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==",
|
| 395 |
+
"cpu": [
|
| 396 |
+
"arm64"
|
| 397 |
+
],
|
| 398 |
+
"dev": true,
|
| 399 |
+
"license": "MIT",
|
| 400 |
+
"optional": true,
|
| 401 |
+
"os": [
|
| 402 |
+
"openbsd"
|
| 403 |
+
],
|
| 404 |
+
"engines": {
|
| 405 |
+
"node": ">=18"
|
| 406 |
+
}
|
| 407 |
+
},
|
| 408 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 409 |
+
"version": "0.25.11",
|
| 410 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz",
|
| 411 |
+
"integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==",
|
| 412 |
+
"cpu": [
|
| 413 |
+
"x64"
|
| 414 |
+
],
|
| 415 |
+
"dev": true,
|
| 416 |
+
"license": "MIT",
|
| 417 |
+
"optional": true,
|
| 418 |
+
"os": [
|
| 419 |
+
"openbsd"
|
| 420 |
+
],
|
| 421 |
+
"engines": {
|
| 422 |
+
"node": ">=18"
|
| 423 |
+
}
|
| 424 |
+
},
|
| 425 |
+
"node_modules/@esbuild/openharmony-arm64": {
|
| 426 |
+
"version": "0.25.11",
|
| 427 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz",
|
| 428 |
+
"integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==",
|
| 429 |
+
"cpu": [
|
| 430 |
+
"arm64"
|
| 431 |
+
],
|
| 432 |
+
"dev": true,
|
| 433 |
+
"license": "MIT",
|
| 434 |
+
"optional": true,
|
| 435 |
+
"os": [
|
| 436 |
+
"openharmony"
|
| 437 |
+
],
|
| 438 |
+
"engines": {
|
| 439 |
+
"node": ">=18"
|
| 440 |
+
}
|
| 441 |
+
},
|
| 442 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 443 |
+
"version": "0.25.11",
|
| 444 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz",
|
| 445 |
+
"integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==",
|
| 446 |
+
"cpu": [
|
| 447 |
+
"x64"
|
| 448 |
+
],
|
| 449 |
+
"dev": true,
|
| 450 |
+
"license": "MIT",
|
| 451 |
+
"optional": true,
|
| 452 |
+
"os": [
|
| 453 |
+
"sunos"
|
| 454 |
+
],
|
| 455 |
+
"engines": {
|
| 456 |
+
"node": ">=18"
|
| 457 |
+
}
|
| 458 |
+
},
|
| 459 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 460 |
+
"version": "0.25.11",
|
| 461 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz",
|
| 462 |
+
"integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==",
|
| 463 |
+
"cpu": [
|
| 464 |
+
"arm64"
|
| 465 |
+
],
|
| 466 |
+
"dev": true,
|
| 467 |
+
"license": "MIT",
|
| 468 |
+
"optional": true,
|
| 469 |
+
"os": [
|
| 470 |
+
"win32"
|
| 471 |
+
],
|
| 472 |
+
"engines": {
|
| 473 |
+
"node": ">=18"
|
| 474 |
+
}
|
| 475 |
+
},
|
| 476 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 477 |
+
"version": "0.25.11",
|
| 478 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz",
|
| 479 |
+
"integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==",
|
| 480 |
+
"cpu": [
|
| 481 |
+
"ia32"
|
| 482 |
+
],
|
| 483 |
+
"dev": true,
|
| 484 |
+
"license": "MIT",
|
| 485 |
+
"optional": true,
|
| 486 |
+
"os": [
|
| 487 |
+
"win32"
|
| 488 |
+
],
|
| 489 |
+
"engines": {
|
| 490 |
+
"node": ">=18"
|
| 491 |
+
}
|
| 492 |
+
},
|
| 493 |
+
"node_modules/@esbuild/win32-x64": {
|
| 494 |
+
"version": "0.25.11",
|
| 495 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz",
|
| 496 |
+
"integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==",
|
| 497 |
+
"cpu": [
|
| 498 |
+
"x64"
|
| 499 |
+
],
|
| 500 |
+
"dev": true,
|
| 501 |
+
"license": "MIT",
|
| 502 |
+
"optional": true,
|
| 503 |
+
"os": [
|
| 504 |
+
"win32"
|
| 505 |
+
],
|
| 506 |
+
"engines": {
|
| 507 |
+
"node": ">=18"
|
| 508 |
+
}
|
| 509 |
+
},
|
| 510 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 511 |
+
"version": "1.5.5",
|
| 512 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 513 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 514 |
+
"license": "MIT"
|
| 515 |
+
},
|
| 516 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 517 |
+
"version": "4.52.5",
|
| 518 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
|
| 519 |
+
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
|
| 520 |
+
"cpu": [
|
| 521 |
+
"arm"
|
| 522 |
+
],
|
| 523 |
+
"dev": true,
|
| 524 |
+
"license": "MIT",
|
| 525 |
+
"optional": true,
|
| 526 |
+
"os": [
|
| 527 |
+
"android"
|
| 528 |
+
]
|
| 529 |
+
},
|
| 530 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 531 |
+
"version": "4.52.5",
|
| 532 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
|
| 533 |
+
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
|
| 534 |
+
"cpu": [
|
| 535 |
+
"arm64"
|
| 536 |
+
],
|
| 537 |
+
"dev": true,
|
| 538 |
+
"license": "MIT",
|
| 539 |
+
"optional": true,
|
| 540 |
+
"os": [
|
| 541 |
+
"android"
|
| 542 |
+
]
|
| 543 |
+
},
|
| 544 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 545 |
+
"version": "4.52.5",
|
| 546 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
|
| 547 |
+
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
|
| 548 |
+
"cpu": [
|
| 549 |
+
"arm64"
|
| 550 |
+
],
|
| 551 |
+
"dev": true,
|
| 552 |
+
"license": "MIT",
|
| 553 |
+
"optional": true,
|
| 554 |
+
"os": [
|
| 555 |
+
"darwin"
|
| 556 |
+
]
|
| 557 |
+
},
|
| 558 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 559 |
+
"version": "4.52.5",
|
| 560 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
|
| 561 |
+
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
|
| 562 |
+
"cpu": [
|
| 563 |
+
"x64"
|
| 564 |
+
],
|
| 565 |
+
"dev": true,
|
| 566 |
+
"license": "MIT",
|
| 567 |
+
"optional": true,
|
| 568 |
+
"os": [
|
| 569 |
+
"darwin"
|
| 570 |
+
]
|
| 571 |
+
},
|
| 572 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 573 |
+
"version": "4.52.5",
|
| 574 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
|
| 575 |
+
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
|
| 576 |
+
"cpu": [
|
| 577 |
+
"arm64"
|
| 578 |
+
],
|
| 579 |
+
"dev": true,
|
| 580 |
+
"license": "MIT",
|
| 581 |
+
"optional": true,
|
| 582 |
+
"os": [
|
| 583 |
+
"freebsd"
|
| 584 |
+
]
|
| 585 |
+
},
|
| 586 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 587 |
+
"version": "4.52.5",
|
| 588 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
|
| 589 |
+
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
|
| 590 |
+
"cpu": [
|
| 591 |
+
"x64"
|
| 592 |
+
],
|
| 593 |
+
"dev": true,
|
| 594 |
+
"license": "MIT",
|
| 595 |
+
"optional": true,
|
| 596 |
+
"os": [
|
| 597 |
+
"freebsd"
|
| 598 |
+
]
|
| 599 |
+
},
|
| 600 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 601 |
+
"version": "4.52.5",
|
| 602 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
|
| 603 |
+
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
|
| 604 |
+
"cpu": [
|
| 605 |
+
"arm"
|
| 606 |
+
],
|
| 607 |
+
"dev": true,
|
| 608 |
+
"license": "MIT",
|
| 609 |
+
"optional": true,
|
| 610 |
+
"os": [
|
| 611 |
+
"linux"
|
| 612 |
+
]
|
| 613 |
+
},
|
| 614 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 615 |
+
"version": "4.52.5",
|
| 616 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
|
| 617 |
+
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
|
| 618 |
+
"cpu": [
|
| 619 |
+
"arm"
|
| 620 |
+
],
|
| 621 |
+
"dev": true,
|
| 622 |
+
"license": "MIT",
|
| 623 |
+
"optional": true,
|
| 624 |
+
"os": [
|
| 625 |
+
"linux"
|
| 626 |
+
]
|
| 627 |
+
},
|
| 628 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 629 |
+
"version": "4.52.5",
|
| 630 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
|
| 631 |
+
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
|
| 632 |
+
"cpu": [
|
| 633 |
+
"arm64"
|
| 634 |
+
],
|
| 635 |
+
"dev": true,
|
| 636 |
+
"license": "MIT",
|
| 637 |
+
"optional": true,
|
| 638 |
+
"os": [
|
| 639 |
+
"linux"
|
| 640 |
+
]
|
| 641 |
+
},
|
| 642 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 643 |
+
"version": "4.52.5",
|
| 644 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
|
| 645 |
+
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
|
| 646 |
+
"cpu": [
|
| 647 |
+
"arm64"
|
| 648 |
+
],
|
| 649 |
+
"dev": true,
|
| 650 |
+
"license": "MIT",
|
| 651 |
+
"optional": true,
|
| 652 |
+
"os": [
|
| 653 |
+
"linux"
|
| 654 |
+
]
|
| 655 |
+
},
|
| 656 |
+
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 657 |
+
"version": "4.52.5",
|
| 658 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
|
| 659 |
+
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
|
| 660 |
+
"cpu": [
|
| 661 |
+
"loong64"
|
| 662 |
+
],
|
| 663 |
+
"dev": true,
|
| 664 |
+
"license": "MIT",
|
| 665 |
+
"optional": true,
|
| 666 |
+
"os": [
|
| 667 |
+
"linux"
|
| 668 |
+
]
|
| 669 |
+
},
|
| 670 |
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 671 |
+
"version": "4.52.5",
|
| 672 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
|
| 673 |
+
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
|
| 674 |
+
"cpu": [
|
| 675 |
+
"ppc64"
|
| 676 |
+
],
|
| 677 |
+
"dev": true,
|
| 678 |
+
"license": "MIT",
|
| 679 |
+
"optional": true,
|
| 680 |
+
"os": [
|
| 681 |
+
"linux"
|
| 682 |
+
]
|
| 683 |
+
},
|
| 684 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 685 |
+
"version": "4.52.5",
|
| 686 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
|
| 687 |
+
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
|
| 688 |
+
"cpu": [
|
| 689 |
+
"riscv64"
|
| 690 |
+
],
|
| 691 |
+
"dev": true,
|
| 692 |
+
"license": "MIT",
|
| 693 |
+
"optional": true,
|
| 694 |
+
"os": [
|
| 695 |
+
"linux"
|
| 696 |
+
]
|
| 697 |
+
},
|
| 698 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 699 |
+
"version": "4.52.5",
|
| 700 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
|
| 701 |
+
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
|
| 702 |
+
"cpu": [
|
| 703 |
+
"riscv64"
|
| 704 |
+
],
|
| 705 |
+
"dev": true,
|
| 706 |
+
"license": "MIT",
|
| 707 |
+
"optional": true,
|
| 708 |
+
"os": [
|
| 709 |
+
"linux"
|
| 710 |
+
]
|
| 711 |
+
},
|
| 712 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 713 |
+
"version": "4.52.5",
|
| 714 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
|
| 715 |
+
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
|
| 716 |
+
"cpu": [
|
| 717 |
+
"s390x"
|
| 718 |
+
],
|
| 719 |
+
"dev": true,
|
| 720 |
+
"license": "MIT",
|
| 721 |
+
"optional": true,
|
| 722 |
+
"os": [
|
| 723 |
+
"linux"
|
| 724 |
+
]
|
| 725 |
+
},
|
| 726 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 727 |
+
"version": "4.52.5",
|
| 728 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
|
| 729 |
+
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
|
| 730 |
+
"cpu": [
|
| 731 |
+
"x64"
|
| 732 |
+
],
|
| 733 |
+
"dev": true,
|
| 734 |
+
"license": "MIT",
|
| 735 |
+
"optional": true,
|
| 736 |
+
"os": [
|
| 737 |
+
"linux"
|
| 738 |
+
]
|
| 739 |
+
},
|
| 740 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 741 |
+
"version": "4.52.5",
|
| 742 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
|
| 743 |
+
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
|
| 744 |
+
"cpu": [
|
| 745 |
+
"x64"
|
| 746 |
+
],
|
| 747 |
+
"dev": true,
|
| 748 |
+
"license": "MIT",
|
| 749 |
+
"optional": true,
|
| 750 |
+
"os": [
|
| 751 |
+
"linux"
|
| 752 |
+
]
|
| 753 |
+
},
|
| 754 |
+
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 755 |
+
"version": "4.52.5",
|
| 756 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
|
| 757 |
+
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
|
| 758 |
+
"cpu": [
|
| 759 |
+
"arm64"
|
| 760 |
+
],
|
| 761 |
+
"dev": true,
|
| 762 |
+
"license": "MIT",
|
| 763 |
+
"optional": true,
|
| 764 |
+
"os": [
|
| 765 |
+
"openharmony"
|
| 766 |
+
]
|
| 767 |
+
},
|
| 768 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 769 |
+
"version": "4.52.5",
|
| 770 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
|
| 771 |
+
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
|
| 772 |
+
"cpu": [
|
| 773 |
+
"arm64"
|
| 774 |
+
],
|
| 775 |
+
"dev": true,
|
| 776 |
+
"license": "MIT",
|
| 777 |
+
"optional": true,
|
| 778 |
+
"os": [
|
| 779 |
+
"win32"
|
| 780 |
+
]
|
| 781 |
+
},
|
| 782 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 783 |
+
"version": "4.52.5",
|
| 784 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
|
| 785 |
+
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
|
| 786 |
+
"cpu": [
|
| 787 |
+
"ia32"
|
| 788 |
+
],
|
| 789 |
+
"dev": true,
|
| 790 |
+
"license": "MIT",
|
| 791 |
+
"optional": true,
|
| 792 |
+
"os": [
|
| 793 |
+
"win32"
|
| 794 |
+
]
|
| 795 |
+
},
|
| 796 |
+
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 797 |
+
"version": "4.52.5",
|
| 798 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
|
| 799 |
+
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
|
| 800 |
+
"cpu": [
|
| 801 |
+
"x64"
|
| 802 |
+
],
|
| 803 |
+
"dev": true,
|
| 804 |
+
"license": "MIT",
|
| 805 |
+
"optional": true,
|
| 806 |
+
"os": [
|
| 807 |
+
"win32"
|
| 808 |
+
]
|
| 809 |
+
},
|
| 810 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 811 |
+
"version": "4.52.5",
|
| 812 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
|
| 813 |
+
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
|
| 814 |
+
"cpu": [
|
| 815 |
+
"x64"
|
| 816 |
+
],
|
| 817 |
+
"dev": true,
|
| 818 |
+
"license": "MIT",
|
| 819 |
+
"optional": true,
|
| 820 |
+
"os": [
|
| 821 |
+
"win32"
|
| 822 |
+
]
|
| 823 |
+
},
|
| 824 |
+
"node_modules/@types/estree": {
|
| 825 |
+
"version": "1.0.8",
|
| 826 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 827 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 828 |
+
"dev": true,
|
| 829 |
+
"license": "MIT"
|
| 830 |
+
},
|
| 831 |
+
"node_modules/@types/node": {
|
| 832 |
+
"version": "22.18.12",
|
| 833 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz",
|
| 834 |
+
"integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==",
|
| 835 |
+
"dev": true,
|
| 836 |
+
"license": "MIT",
|
| 837 |
+
"peer": true,
|
| 838 |
+
"dependencies": {
|
| 839 |
+
"undici-types": "~6.21.0"
|
| 840 |
+
}
|
| 841 |
+
},
|
| 842 |
+
"node_modules/@vitejs/plugin-vue": {
|
| 843 |
+
"version": "5.2.4",
|
| 844 |
+
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
| 845 |
+
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
|
| 846 |
+
"dev": true,
|
| 847 |
+
"license": "MIT",
|
| 848 |
+
"engines": {
|
| 849 |
+
"node": "^18.0.0 || >=20.0.0"
|
| 850 |
+
},
|
| 851 |
+
"peerDependencies": {
|
| 852 |
+
"vite": "^5.0.0 || ^6.0.0",
|
| 853 |
+
"vue": "^3.2.25"
|
| 854 |
+
}
|
| 855 |
+
},
|
| 856 |
+
"node_modules/@volar/language-core": {
|
| 857 |
+
"version": "2.4.15",
|
| 858 |
+
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
|
| 859 |
+
"integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
|
| 860 |
+
"dev": true,
|
| 861 |
+
"license": "MIT",
|
| 862 |
+
"dependencies": {
|
| 863 |
+
"@volar/source-map": "2.4.15"
|
| 864 |
+
}
|
| 865 |
+
},
|
| 866 |
+
"node_modules/@volar/source-map": {
|
| 867 |
+
"version": "2.4.15",
|
| 868 |
+
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
|
| 869 |
+
"integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
|
| 870 |
+
"dev": true,
|
| 871 |
+
"license": "MIT"
|
| 872 |
+
},
|
| 873 |
+
"node_modules/@volar/typescript": {
|
| 874 |
+
"version": "2.4.15",
|
| 875 |
+
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
|
| 876 |
+
"integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
|
| 877 |
+
"dev": true,
|
| 878 |
+
"license": "MIT",
|
| 879 |
+
"dependencies": {
|
| 880 |
+
"@volar/language-core": "2.4.15",
|
| 881 |
+
"path-browserify": "^1.0.1",
|
| 882 |
+
"vscode-uri": "^3.0.8"
|
| 883 |
+
}
|
| 884 |
+
},
|
| 885 |
+
"node_modules/@vue/compiler-core": {
|
| 886 |
+
"version": "3.5.22",
|
| 887 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
|
| 888 |
+
"integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
|
| 889 |
+
"license": "MIT",
|
| 890 |
+
"dependencies": {
|
| 891 |
+
"@babel/parser": "^7.28.4",
|
| 892 |
+
"@vue/shared": "3.5.22",
|
| 893 |
+
"entities": "^4.5.0",
|
| 894 |
+
"estree-walker": "^2.0.2",
|
| 895 |
+
"source-map-js": "^1.2.1"
|
| 896 |
+
}
|
| 897 |
+
},
|
| 898 |
+
"node_modules/@vue/compiler-dom": {
|
| 899 |
+
"version": "3.5.22",
|
| 900 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
|
| 901 |
+
"integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
|
| 902 |
+
"license": "MIT",
|
| 903 |
+
"dependencies": {
|
| 904 |
+
"@vue/compiler-core": "3.5.22",
|
| 905 |
+
"@vue/shared": "3.5.22"
|
| 906 |
+
}
|
| 907 |
+
},
|
| 908 |
+
"node_modules/@vue/compiler-sfc": {
|
| 909 |
+
"version": "3.5.22",
|
| 910 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
|
| 911 |
+
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
|
| 912 |
+
"license": "MIT",
|
| 913 |
+
"dependencies": {
|
| 914 |
+
"@babel/parser": "^7.28.4",
|
| 915 |
+
"@vue/compiler-core": "3.5.22",
|
| 916 |
+
"@vue/compiler-dom": "3.5.22",
|
| 917 |
+
"@vue/compiler-ssr": "3.5.22",
|
| 918 |
+
"@vue/shared": "3.5.22",
|
| 919 |
+
"estree-walker": "^2.0.2",
|
| 920 |
+
"magic-string": "^0.30.19",
|
| 921 |
+
"postcss": "^8.5.6",
|
| 922 |
+
"source-map-js": "^1.2.1"
|
| 923 |
+
}
|
| 924 |
+
},
|
| 925 |
+
"node_modules/@vue/compiler-ssr": {
|
| 926 |
+
"version": "3.5.22",
|
| 927 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
|
| 928 |
+
"integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
|
| 929 |
+
"license": "MIT",
|
| 930 |
+
"dependencies": {
|
| 931 |
+
"@vue/compiler-dom": "3.5.22",
|
| 932 |
+
"@vue/shared": "3.5.22"
|
| 933 |
+
}
|
| 934 |
+
},
|
| 935 |
+
"node_modules/@vue/compiler-vue2": {
|
| 936 |
+
"version": "2.7.16",
|
| 937 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
|
| 938 |
+
"integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
|
| 939 |
+
"dev": true,
|
| 940 |
+
"license": "MIT",
|
| 941 |
+
"dependencies": {
|
| 942 |
+
"de-indent": "^1.0.2",
|
| 943 |
+
"he": "^1.2.0"
|
| 944 |
+
}
|
| 945 |
+
},
|
| 946 |
+
"node_modules/@vue/language-core": {
|
| 947 |
+
"version": "2.2.12",
|
| 948 |
+
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
|
| 949 |
+
"integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
|
| 950 |
+
"dev": true,
|
| 951 |
+
"license": "MIT",
|
| 952 |
+
"dependencies": {
|
| 953 |
+
"@volar/language-core": "2.4.15",
|
| 954 |
+
"@vue/compiler-dom": "^3.5.0",
|
| 955 |
+
"@vue/compiler-vue2": "^2.7.16",
|
| 956 |
+
"@vue/shared": "^3.5.0",
|
| 957 |
+
"alien-signals": "^1.0.3",
|
| 958 |
+
"minimatch": "^9.0.3",
|
| 959 |
+
"muggle-string": "^0.4.1",
|
| 960 |
+
"path-browserify": "^1.0.1"
|
| 961 |
+
},
|
| 962 |
+
"peerDependencies": {
|
| 963 |
+
"typescript": "*"
|
| 964 |
+
},
|
| 965 |
+
"peerDependenciesMeta": {
|
| 966 |
+
"typescript": {
|
| 967 |
+
"optional": true
|
| 968 |
+
}
|
| 969 |
+
}
|
| 970 |
+
},
|
| 971 |
+
"node_modules/@vue/reactivity": {
|
| 972 |
+
"version": "3.5.22",
|
| 973 |
+
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz",
|
| 974 |
+
"integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
|
| 975 |
+
"license": "MIT",
|
| 976 |
+
"dependencies": {
|
| 977 |
+
"@vue/shared": "3.5.22"
|
| 978 |
+
}
|
| 979 |
+
},
|
| 980 |
+
"node_modules/@vue/runtime-core": {
|
| 981 |
+
"version": "3.5.22",
|
| 982 |
+
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
|
| 983 |
+
"integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
|
| 984 |
+
"license": "MIT",
|
| 985 |
+
"dependencies": {
|
| 986 |
+
"@vue/reactivity": "3.5.22",
|
| 987 |
+
"@vue/shared": "3.5.22"
|
| 988 |
+
}
|
| 989 |
+
},
|
| 990 |
+
"node_modules/@vue/runtime-dom": {
|
| 991 |
+
"version": "3.5.22",
|
| 992 |
+
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
|
| 993 |
+
"integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
|
| 994 |
+
"license": "MIT",
|
| 995 |
+
"dependencies": {
|
| 996 |
+
"@vue/reactivity": "3.5.22",
|
| 997 |
+
"@vue/runtime-core": "3.5.22",
|
| 998 |
+
"@vue/shared": "3.5.22",
|
| 999 |
+
"csstype": "^3.1.3"
|
| 1000 |
+
}
|
| 1001 |
+
},
|
| 1002 |
+
"node_modules/@vue/server-renderer": {
|
| 1003 |
+
"version": "3.5.22",
|
| 1004 |
+
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
|
| 1005 |
+
"integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
|
| 1006 |
+
"license": "MIT",
|
| 1007 |
+
"dependencies": {
|
| 1008 |
+
"@vue/compiler-ssr": "3.5.22",
|
| 1009 |
+
"@vue/shared": "3.5.22"
|
| 1010 |
+
},
|
| 1011 |
+
"peerDependencies": {
|
| 1012 |
+
"vue": "3.5.22"
|
| 1013 |
+
}
|
| 1014 |
+
},
|
| 1015 |
+
"node_modules/@vue/shared": {
|
| 1016 |
+
"version": "3.5.22",
|
| 1017 |
+
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz",
|
| 1018 |
+
"integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
|
| 1019 |
+
"license": "MIT"
|
| 1020 |
+
},
|
| 1021 |
+
"node_modules/alien-signals": {
|
| 1022 |
+
"version": "1.0.13",
|
| 1023 |
+
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
|
| 1024 |
+
"integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
|
| 1025 |
+
"dev": true,
|
| 1026 |
+
"license": "MIT"
|
| 1027 |
+
},
|
| 1028 |
+
"node_modules/asynckit": {
|
| 1029 |
+
"version": "0.4.0",
|
| 1030 |
+
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
| 1031 |
+
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
| 1032 |
+
"license": "MIT"
|
| 1033 |
+
},
|
| 1034 |
+
"node_modules/axios": {
|
| 1035 |
+
"version": "1.12.2",
|
| 1036 |
+
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
| 1037 |
+
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
| 1038 |
+
"license": "MIT",
|
| 1039 |
+
"dependencies": {
|
| 1040 |
+
"follow-redirects": "^1.15.6",
|
| 1041 |
+
"form-data": "^4.0.4",
|
| 1042 |
+
"proxy-from-env": "^1.1.0"
|
| 1043 |
+
}
|
| 1044 |
+
},
|
| 1045 |
+
"node_modules/balanced-match": {
|
| 1046 |
+
"version": "1.0.2",
|
| 1047 |
+
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
| 1048 |
+
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
| 1049 |
+
"dev": true,
|
| 1050 |
+
"license": "MIT"
|
| 1051 |
+
},
|
| 1052 |
+
"node_modules/brace-expansion": {
|
| 1053 |
+
"version": "2.0.2",
|
| 1054 |
+
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
| 1055 |
+
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
| 1056 |
+
"dev": true,
|
| 1057 |
+
"license": "MIT",
|
| 1058 |
+
"dependencies": {
|
| 1059 |
+
"balanced-match": "^1.0.0"
|
| 1060 |
+
}
|
| 1061 |
+
},
|
| 1062 |
+
"node_modules/call-bind-apply-helpers": {
|
| 1063 |
+
"version": "1.0.2",
|
| 1064 |
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
| 1065 |
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
| 1066 |
+
"license": "MIT",
|
| 1067 |
+
"dependencies": {
|
| 1068 |
+
"es-errors": "^1.3.0",
|
| 1069 |
+
"function-bind": "^1.1.2"
|
| 1070 |
+
},
|
| 1071 |
+
"engines": {
|
| 1072 |
+
"node": ">= 0.4"
|
| 1073 |
+
}
|
| 1074 |
+
},
|
| 1075 |
+
"node_modules/combined-stream": {
|
| 1076 |
+
"version": "1.0.8",
|
| 1077 |
+
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
| 1078 |
+
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
| 1079 |
+
"license": "MIT",
|
| 1080 |
+
"dependencies": {
|
| 1081 |
+
"delayed-stream": "~1.0.0"
|
| 1082 |
+
},
|
| 1083 |
+
"engines": {
|
| 1084 |
+
"node": ">= 0.8"
|
| 1085 |
+
}
|
| 1086 |
+
},
|
| 1087 |
+
"node_modules/csstype": {
|
| 1088 |
+
"version": "3.1.3",
|
| 1089 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
| 1090 |
+
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
| 1091 |
+
"license": "MIT"
|
| 1092 |
+
},
|
| 1093 |
+
"node_modules/de-indent": {
|
| 1094 |
+
"version": "1.0.2",
|
| 1095 |
+
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
| 1096 |
+
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
|
| 1097 |
+
"dev": true,
|
| 1098 |
+
"license": "MIT"
|
| 1099 |
+
},
|
| 1100 |
+
"node_modules/delayed-stream": {
|
| 1101 |
+
"version": "1.0.0",
|
| 1102 |
+
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
| 1103 |
+
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
| 1104 |
+
"license": "MIT",
|
| 1105 |
+
"engines": {
|
| 1106 |
+
"node": ">=0.4.0"
|
| 1107 |
+
}
|
| 1108 |
+
},
|
| 1109 |
+
"node_modules/dunder-proto": {
|
| 1110 |
+
"version": "1.0.1",
|
| 1111 |
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
| 1112 |
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
| 1113 |
+
"license": "MIT",
|
| 1114 |
+
"dependencies": {
|
| 1115 |
+
"call-bind-apply-helpers": "^1.0.1",
|
| 1116 |
+
"es-errors": "^1.3.0",
|
| 1117 |
+
"gopd": "^1.2.0"
|
| 1118 |
+
},
|
| 1119 |
+
"engines": {
|
| 1120 |
+
"node": ">= 0.4"
|
| 1121 |
+
}
|
| 1122 |
+
},
|
| 1123 |
+
"node_modules/entities": {
|
| 1124 |
+
"version": "4.5.0",
|
| 1125 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
| 1126 |
+
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
| 1127 |
+
"license": "BSD-2-Clause",
|
| 1128 |
+
"engines": {
|
| 1129 |
+
"node": ">=0.12"
|
| 1130 |
+
},
|
| 1131 |
+
"funding": {
|
| 1132 |
+
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 1133 |
+
}
|
| 1134 |
+
},
|
| 1135 |
+
"node_modules/es-define-property": {
|
| 1136 |
+
"version": "1.0.1",
|
| 1137 |
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
| 1138 |
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
| 1139 |
+
"license": "MIT",
|
| 1140 |
+
"engines": {
|
| 1141 |
+
"node": ">= 0.4"
|
| 1142 |
+
}
|
| 1143 |
+
},
|
| 1144 |
+
"node_modules/es-errors": {
|
| 1145 |
+
"version": "1.3.0",
|
| 1146 |
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
| 1147 |
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
| 1148 |
+
"license": "MIT",
|
| 1149 |
+
"engines": {
|
| 1150 |
+
"node": ">= 0.4"
|
| 1151 |
+
}
|
| 1152 |
+
},
|
| 1153 |
+
"node_modules/es-object-atoms": {
|
| 1154 |
+
"version": "1.1.1",
|
| 1155 |
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
| 1156 |
+
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
| 1157 |
+
"license": "MIT",
|
| 1158 |
+
"dependencies": {
|
| 1159 |
+
"es-errors": "^1.3.0"
|
| 1160 |
+
},
|
| 1161 |
+
"engines": {
|
| 1162 |
+
"node": ">= 0.4"
|
| 1163 |
+
}
|
| 1164 |
+
},
|
| 1165 |
+
"node_modules/es-set-tostringtag": {
|
| 1166 |
+
"version": "2.1.0",
|
| 1167 |
+
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
| 1168 |
+
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
| 1169 |
+
"license": "MIT",
|
| 1170 |
+
"dependencies": {
|
| 1171 |
+
"es-errors": "^1.3.0",
|
| 1172 |
+
"get-intrinsic": "^1.2.6",
|
| 1173 |
+
"has-tostringtag": "^1.0.2",
|
| 1174 |
+
"hasown": "^2.0.2"
|
| 1175 |
+
},
|
| 1176 |
+
"engines": {
|
| 1177 |
+
"node": ">= 0.4"
|
| 1178 |
+
}
|
| 1179 |
+
},
|
| 1180 |
+
"node_modules/esbuild": {
|
| 1181 |
+
"version": "0.25.11",
|
| 1182 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
|
| 1183 |
+
"integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==",
|
| 1184 |
+
"dev": true,
|
| 1185 |
+
"hasInstallScript": true,
|
| 1186 |
+
"license": "MIT",
|
| 1187 |
+
"bin": {
|
| 1188 |
+
"esbuild": "bin/esbuild"
|
| 1189 |
+
},
|
| 1190 |
+
"engines": {
|
| 1191 |
+
"node": ">=18"
|
| 1192 |
+
},
|
| 1193 |
+
"optionalDependencies": {
|
| 1194 |
+
"@esbuild/aix-ppc64": "0.25.11",
|
| 1195 |
+
"@esbuild/android-arm": "0.25.11",
|
| 1196 |
+
"@esbuild/android-arm64": "0.25.11",
|
| 1197 |
+
"@esbuild/android-x64": "0.25.11",
|
| 1198 |
+
"@esbuild/darwin-arm64": "0.25.11",
|
| 1199 |
+
"@esbuild/darwin-x64": "0.25.11",
|
| 1200 |
+
"@esbuild/freebsd-arm64": "0.25.11",
|
| 1201 |
+
"@esbuild/freebsd-x64": "0.25.11",
|
| 1202 |
+
"@esbuild/linux-arm": "0.25.11",
|
| 1203 |
+
"@esbuild/linux-arm64": "0.25.11",
|
| 1204 |
+
"@esbuild/linux-ia32": "0.25.11",
|
| 1205 |
+
"@esbuild/linux-loong64": "0.25.11",
|
| 1206 |
+
"@esbuild/linux-mips64el": "0.25.11",
|
| 1207 |
+
"@esbuild/linux-ppc64": "0.25.11",
|
| 1208 |
+
"@esbuild/linux-riscv64": "0.25.11",
|
| 1209 |
+
"@esbuild/linux-s390x": "0.25.11",
|
| 1210 |
+
"@esbuild/linux-x64": "0.25.11",
|
| 1211 |
+
"@esbuild/netbsd-arm64": "0.25.11",
|
| 1212 |
+
"@esbuild/netbsd-x64": "0.25.11",
|
| 1213 |
+
"@esbuild/openbsd-arm64": "0.25.11",
|
| 1214 |
+
"@esbuild/openbsd-x64": "0.25.11",
|
| 1215 |
+
"@esbuild/openharmony-arm64": "0.25.11",
|
| 1216 |
+
"@esbuild/sunos-x64": "0.25.11",
|
| 1217 |
+
"@esbuild/win32-arm64": "0.25.11",
|
| 1218 |
+
"@esbuild/win32-ia32": "0.25.11",
|
| 1219 |
+
"@esbuild/win32-x64": "0.25.11"
|
| 1220 |
+
}
|
| 1221 |
+
},
|
| 1222 |
+
"node_modules/estree-walker": {
|
| 1223 |
+
"version": "2.0.2",
|
| 1224 |
+
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
| 1225 |
+
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
| 1226 |
+
"license": "MIT"
|
| 1227 |
+
},
|
| 1228 |
+
"node_modules/fdir": {
|
| 1229 |
+
"version": "6.5.0",
|
| 1230 |
+
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
| 1231 |
+
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
| 1232 |
+
"dev": true,
|
| 1233 |
+
"license": "MIT",
|
| 1234 |
+
"engines": {
|
| 1235 |
+
"node": ">=12.0.0"
|
| 1236 |
+
},
|
| 1237 |
+
"peerDependencies": {
|
| 1238 |
+
"picomatch": "^3 || ^4"
|
| 1239 |
+
},
|
| 1240 |
+
"peerDependenciesMeta": {
|
| 1241 |
+
"picomatch": {
|
| 1242 |
+
"optional": true
|
| 1243 |
+
}
|
| 1244 |
+
}
|
| 1245 |
+
},
|
| 1246 |
+
"node_modules/follow-redirects": {
|
| 1247 |
+
"version": "1.15.11",
|
| 1248 |
+
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
| 1249 |
+
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
| 1250 |
+
"funding": [
|
| 1251 |
+
{
|
| 1252 |
+
"type": "individual",
|
| 1253 |
+
"url": "https://github.com/sponsors/RubenVerborgh"
|
| 1254 |
+
}
|
| 1255 |
+
],
|
| 1256 |
+
"license": "MIT",
|
| 1257 |
+
"engines": {
|
| 1258 |
+
"node": ">=4.0"
|
| 1259 |
+
},
|
| 1260 |
+
"peerDependenciesMeta": {
|
| 1261 |
+
"debug": {
|
| 1262 |
+
"optional": true
|
| 1263 |
+
}
|
| 1264 |
+
}
|
| 1265 |
+
},
|
| 1266 |
+
"node_modules/form-data": {
|
| 1267 |
+
"version": "4.0.4",
|
| 1268 |
+
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
| 1269 |
+
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
| 1270 |
+
"license": "MIT",
|
| 1271 |
+
"dependencies": {
|
| 1272 |
+
"asynckit": "^0.4.0",
|
| 1273 |
+
"combined-stream": "^1.0.8",
|
| 1274 |
+
"es-set-tostringtag": "^2.1.0",
|
| 1275 |
+
"hasown": "^2.0.2",
|
| 1276 |
+
"mime-types": "^2.1.12"
|
| 1277 |
+
},
|
| 1278 |
+
"engines": {
|
| 1279 |
+
"node": ">= 6"
|
| 1280 |
+
}
|
| 1281 |
+
},
|
| 1282 |
+
"node_modules/fsevents": {
|
| 1283 |
+
"version": "2.3.3",
|
| 1284 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1285 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1286 |
+
"dev": true,
|
| 1287 |
+
"hasInstallScript": true,
|
| 1288 |
+
"license": "MIT",
|
| 1289 |
+
"optional": true,
|
| 1290 |
+
"os": [
|
| 1291 |
+
"darwin"
|
| 1292 |
+
],
|
| 1293 |
+
"engines": {
|
| 1294 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1295 |
+
}
|
| 1296 |
+
},
|
| 1297 |
+
"node_modules/function-bind": {
|
| 1298 |
+
"version": "1.1.2",
|
| 1299 |
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
| 1300 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 1301 |
+
"license": "MIT",
|
| 1302 |
+
"funding": {
|
| 1303 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1304 |
+
}
|
| 1305 |
+
},
|
| 1306 |
+
"node_modules/get-intrinsic": {
|
| 1307 |
+
"version": "1.3.0",
|
| 1308 |
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
| 1309 |
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
| 1310 |
+
"license": "MIT",
|
| 1311 |
+
"dependencies": {
|
| 1312 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 1313 |
+
"es-define-property": "^1.0.1",
|
| 1314 |
+
"es-errors": "^1.3.0",
|
| 1315 |
+
"es-object-atoms": "^1.1.1",
|
| 1316 |
+
"function-bind": "^1.1.2",
|
| 1317 |
+
"get-proto": "^1.0.1",
|
| 1318 |
+
"gopd": "^1.2.0",
|
| 1319 |
+
"has-symbols": "^1.1.0",
|
| 1320 |
+
"hasown": "^2.0.2",
|
| 1321 |
+
"math-intrinsics": "^1.1.0"
|
| 1322 |
+
},
|
| 1323 |
+
"engines": {
|
| 1324 |
+
"node": ">= 0.4"
|
| 1325 |
+
},
|
| 1326 |
+
"funding": {
|
| 1327 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1328 |
+
}
|
| 1329 |
+
},
|
| 1330 |
+
"node_modules/get-proto": {
|
| 1331 |
+
"version": "1.0.1",
|
| 1332 |
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
| 1333 |
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
| 1334 |
+
"license": "MIT",
|
| 1335 |
+
"dependencies": {
|
| 1336 |
+
"dunder-proto": "^1.0.1",
|
| 1337 |
+
"es-object-atoms": "^1.0.0"
|
| 1338 |
+
},
|
| 1339 |
+
"engines": {
|
| 1340 |
+
"node": ">= 0.4"
|
| 1341 |
+
}
|
| 1342 |
+
},
|
| 1343 |
+
"node_modules/gopd": {
|
| 1344 |
+
"version": "1.2.0",
|
| 1345 |
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
| 1346 |
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
| 1347 |
+
"license": "MIT",
|
| 1348 |
+
"engines": {
|
| 1349 |
+
"node": ">= 0.4"
|
| 1350 |
+
},
|
| 1351 |
+
"funding": {
|
| 1352 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1353 |
+
}
|
| 1354 |
+
},
|
| 1355 |
+
"node_modules/has-symbols": {
|
| 1356 |
+
"version": "1.1.0",
|
| 1357 |
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
| 1358 |
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
| 1359 |
+
"license": "MIT",
|
| 1360 |
+
"engines": {
|
| 1361 |
+
"node": ">= 0.4"
|
| 1362 |
+
},
|
| 1363 |
+
"funding": {
|
| 1364 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1365 |
+
}
|
| 1366 |
+
},
|
| 1367 |
+
"node_modules/has-tostringtag": {
|
| 1368 |
+
"version": "1.0.2",
|
| 1369 |
+
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
| 1370 |
+
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
| 1371 |
+
"license": "MIT",
|
| 1372 |
+
"dependencies": {
|
| 1373 |
+
"has-symbols": "^1.0.3"
|
| 1374 |
+
},
|
| 1375 |
+
"engines": {
|
| 1376 |
+
"node": ">= 0.4"
|
| 1377 |
+
},
|
| 1378 |
+
"funding": {
|
| 1379 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1380 |
+
}
|
| 1381 |
+
},
|
| 1382 |
+
"node_modules/hasown": {
|
| 1383 |
+
"version": "2.0.2",
|
| 1384 |
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
| 1385 |
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
| 1386 |
+
"license": "MIT",
|
| 1387 |
+
"dependencies": {
|
| 1388 |
+
"function-bind": "^1.1.2"
|
| 1389 |
+
},
|
| 1390 |
+
"engines": {
|
| 1391 |
+
"node": ">= 0.4"
|
| 1392 |
+
}
|
| 1393 |
+
},
|
| 1394 |
+
"node_modules/he": {
|
| 1395 |
+
"version": "1.2.0",
|
| 1396 |
+
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
| 1397 |
+
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
| 1398 |
+
"dev": true,
|
| 1399 |
+
"license": "MIT",
|
| 1400 |
+
"bin": {
|
| 1401 |
+
"he": "bin/he"
|
| 1402 |
+
}
|
| 1403 |
+
},
|
| 1404 |
+
"node_modules/magic-string": {
|
| 1405 |
+
"version": "0.30.19",
|
| 1406 |
+
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
| 1407 |
+
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
|
| 1408 |
+
"license": "MIT",
|
| 1409 |
+
"dependencies": {
|
| 1410 |
+
"@jridgewell/sourcemap-codec": "^1.5.5"
|
| 1411 |
+
}
|
| 1412 |
+
},
|
| 1413 |
+
"node_modules/math-intrinsics": {
|
| 1414 |
+
"version": "1.1.0",
|
| 1415 |
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
| 1416 |
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
| 1417 |
+
"license": "MIT",
|
| 1418 |
+
"engines": {
|
| 1419 |
+
"node": ">= 0.4"
|
| 1420 |
+
}
|
| 1421 |
+
},
|
| 1422 |
+
"node_modules/mime-db": {
|
| 1423 |
+
"version": "1.52.0",
|
| 1424 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
| 1425 |
+
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
| 1426 |
+
"license": "MIT",
|
| 1427 |
+
"engines": {
|
| 1428 |
+
"node": ">= 0.6"
|
| 1429 |
+
}
|
| 1430 |
+
},
|
| 1431 |
+
"node_modules/mime-types": {
|
| 1432 |
+
"version": "2.1.35",
|
| 1433 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
| 1434 |
+
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
| 1435 |
+
"license": "MIT",
|
| 1436 |
+
"dependencies": {
|
| 1437 |
+
"mime-db": "1.52.0"
|
| 1438 |
+
},
|
| 1439 |
+
"engines": {
|
| 1440 |
+
"node": ">= 0.6"
|
| 1441 |
+
}
|
| 1442 |
+
},
|
| 1443 |
+
"node_modules/minimatch": {
|
| 1444 |
+
"version": "9.0.5",
|
| 1445 |
+
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
| 1446 |
+
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
| 1447 |
+
"dev": true,
|
| 1448 |
+
"license": "ISC",
|
| 1449 |
+
"dependencies": {
|
| 1450 |
+
"brace-expansion": "^2.0.1"
|
| 1451 |
+
},
|
| 1452 |
+
"engines": {
|
| 1453 |
+
"node": ">=16 || 14 >=14.17"
|
| 1454 |
+
},
|
| 1455 |
+
"funding": {
|
| 1456 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 1457 |
+
}
|
| 1458 |
+
},
|
| 1459 |
+
"node_modules/muggle-string": {
|
| 1460 |
+
"version": "0.4.1",
|
| 1461 |
+
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
| 1462 |
+
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
| 1463 |
+
"dev": true,
|
| 1464 |
+
"license": "MIT"
|
| 1465 |
+
},
|
| 1466 |
+
"node_modules/nanoid": {
|
| 1467 |
+
"version": "3.3.11",
|
| 1468 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
| 1469 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 1470 |
+
"funding": [
|
| 1471 |
+
{
|
| 1472 |
+
"type": "github",
|
| 1473 |
+
"url": "https://github.com/sponsors/ai"
|
| 1474 |
+
}
|
| 1475 |
+
],
|
| 1476 |
+
"license": "MIT",
|
| 1477 |
+
"bin": {
|
| 1478 |
+
"nanoid": "bin/nanoid.cjs"
|
| 1479 |
+
},
|
| 1480 |
+
"engines": {
|
| 1481 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 1482 |
+
}
|
| 1483 |
+
},
|
| 1484 |
+
"node_modules/path-browserify": {
|
| 1485 |
+
"version": "1.0.1",
|
| 1486 |
+
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
| 1487 |
+
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
| 1488 |
+
"dev": true,
|
| 1489 |
+
"license": "MIT"
|
| 1490 |
+
},
|
| 1491 |
+
"node_modules/picocolors": {
|
| 1492 |
+
"version": "1.1.1",
|
| 1493 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 1494 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 1495 |
+
"license": "ISC"
|
| 1496 |
+
},
|
| 1497 |
+
"node_modules/picomatch": {
|
| 1498 |
+
"version": "4.0.3",
|
| 1499 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
| 1500 |
+
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 1501 |
+
"dev": true,
|
| 1502 |
+
"license": "MIT",
|
| 1503 |
+
"peer": true,
|
| 1504 |
+
"engines": {
|
| 1505 |
+
"node": ">=12"
|
| 1506 |
+
},
|
| 1507 |
+
"funding": {
|
| 1508 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 1509 |
+
}
|
| 1510 |
+
},
|
| 1511 |
+
"node_modules/postcss": {
|
| 1512 |
+
"version": "8.5.6",
|
| 1513 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
| 1514 |
+
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
| 1515 |
+
"funding": [
|
| 1516 |
+
{
|
| 1517 |
+
"type": "opencollective",
|
| 1518 |
+
"url": "https://opencollective.com/postcss/"
|
| 1519 |
+
},
|
| 1520 |
+
{
|
| 1521 |
+
"type": "tidelift",
|
| 1522 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 1523 |
+
},
|
| 1524 |
+
{
|
| 1525 |
+
"type": "github",
|
| 1526 |
+
"url": "https://github.com/sponsors/ai"
|
| 1527 |
+
}
|
| 1528 |
+
],
|
| 1529 |
+
"license": "MIT",
|
| 1530 |
+
"dependencies": {
|
| 1531 |
+
"nanoid": "^3.3.11",
|
| 1532 |
+
"picocolors": "^1.1.1",
|
| 1533 |
+
"source-map-js": "^1.2.1"
|
| 1534 |
+
},
|
| 1535 |
+
"engines": {
|
| 1536 |
+
"node": "^10 || ^12 || >=14"
|
| 1537 |
+
}
|
| 1538 |
+
},
|
| 1539 |
+
"node_modules/proxy-from-env": {
|
| 1540 |
+
"version": "1.1.0",
|
| 1541 |
+
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
| 1542 |
+
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
| 1543 |
+
"license": "MIT"
|
| 1544 |
+
},
|
| 1545 |
+
"node_modules/rollup": {
|
| 1546 |
+
"version": "4.52.5",
|
| 1547 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
| 1548 |
+
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
|
| 1549 |
+
"dev": true,
|
| 1550 |
+
"license": "MIT",
|
| 1551 |
+
"dependencies": {
|
| 1552 |
+
"@types/estree": "1.0.8"
|
| 1553 |
+
},
|
| 1554 |
+
"bin": {
|
| 1555 |
+
"rollup": "dist/bin/rollup"
|
| 1556 |
+
},
|
| 1557 |
+
"engines": {
|
| 1558 |
+
"node": ">=18.0.0",
|
| 1559 |
+
"npm": ">=8.0.0"
|
| 1560 |
+
},
|
| 1561 |
+
"optionalDependencies": {
|
| 1562 |
+
"@rollup/rollup-android-arm-eabi": "4.52.5",
|
| 1563 |
+
"@rollup/rollup-android-arm64": "4.52.5",
|
| 1564 |
+
"@rollup/rollup-darwin-arm64": "4.52.5",
|
| 1565 |
+
"@rollup/rollup-darwin-x64": "4.52.5",
|
| 1566 |
+
"@rollup/rollup-freebsd-arm64": "4.52.5",
|
| 1567 |
+
"@rollup/rollup-freebsd-x64": "4.52.5",
|
| 1568 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
|
| 1569 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.52.5",
|
| 1570 |
+
"@rollup/rollup-linux-arm64-gnu": "4.52.5",
|
| 1571 |
+
"@rollup/rollup-linux-arm64-musl": "4.52.5",
|
| 1572 |
+
"@rollup/rollup-linux-loong64-gnu": "4.52.5",
|
| 1573 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.52.5",
|
| 1574 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.52.5",
|
| 1575 |
+
"@rollup/rollup-linux-riscv64-musl": "4.52.5",
|
| 1576 |
+
"@rollup/rollup-linux-s390x-gnu": "4.52.5",
|
| 1577 |
+
"@rollup/rollup-linux-x64-gnu": "4.52.5",
|
| 1578 |
+
"@rollup/rollup-linux-x64-musl": "4.52.5",
|
| 1579 |
+
"@rollup/rollup-openharmony-arm64": "4.52.5",
|
| 1580 |
+
"@rollup/rollup-win32-arm64-msvc": "4.52.5",
|
| 1581 |
+
"@rollup/rollup-win32-ia32-msvc": "4.52.5",
|
| 1582 |
+
"@rollup/rollup-win32-x64-gnu": "4.52.5",
|
| 1583 |
+
"@rollup/rollup-win32-x64-msvc": "4.52.5",
|
| 1584 |
+
"fsevents": "~2.3.2"
|
| 1585 |
+
}
|
| 1586 |
+
},
|
| 1587 |
+
"node_modules/source-map-js": {
|
| 1588 |
+
"version": "1.2.1",
|
| 1589 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 1590 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 1591 |
+
"license": "BSD-3-Clause",
|
| 1592 |
+
"engines": {
|
| 1593 |
+
"node": ">=0.10.0"
|
| 1594 |
+
}
|
| 1595 |
+
},
|
| 1596 |
+
"node_modules/tinyglobby": {
|
| 1597 |
+
"version": "0.2.15",
|
| 1598 |
+
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
| 1599 |
+
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
| 1600 |
+
"dev": true,
|
| 1601 |
+
"license": "MIT",
|
| 1602 |
+
"dependencies": {
|
| 1603 |
+
"fdir": "^6.5.0",
|
| 1604 |
+
"picomatch": "^4.0.3"
|
| 1605 |
+
},
|
| 1606 |
+
"engines": {
|
| 1607 |
+
"node": ">=12.0.0"
|
| 1608 |
+
},
|
| 1609 |
+
"funding": {
|
| 1610 |
+
"url": "https://github.com/sponsors/SuperchupuDev"
|
| 1611 |
+
}
|
| 1612 |
+
},
|
| 1613 |
+
"node_modules/typescript": {
|
| 1614 |
+
"version": "5.9.3",
|
| 1615 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
| 1616 |
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
| 1617 |
+
"devOptional": true,
|
| 1618 |
+
"license": "Apache-2.0",
|
| 1619 |
+
"peer": true,
|
| 1620 |
+
"bin": {
|
| 1621 |
+
"tsc": "bin/tsc",
|
| 1622 |
+
"tsserver": "bin/tsserver"
|
| 1623 |
+
},
|
| 1624 |
+
"engines": {
|
| 1625 |
+
"node": ">=14.17"
|
| 1626 |
+
}
|
| 1627 |
+
},
|
| 1628 |
+
"node_modules/undici-types": {
|
| 1629 |
+
"version": "6.21.0",
|
| 1630 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
| 1631 |
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
| 1632 |
+
"dev": true,
|
| 1633 |
+
"license": "MIT"
|
| 1634 |
+
},
|
| 1635 |
+
"node_modules/vite": {
|
| 1636 |
+
"version": "6.4.1",
|
| 1637 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
| 1638 |
+
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
| 1639 |
+
"dev": true,
|
| 1640 |
+
"license": "MIT",
|
| 1641 |
+
"peer": true,
|
| 1642 |
+
"dependencies": {
|
| 1643 |
+
"esbuild": "^0.25.0",
|
| 1644 |
+
"fdir": "^6.4.4",
|
| 1645 |
+
"picomatch": "^4.0.2",
|
| 1646 |
+
"postcss": "^8.5.3",
|
| 1647 |
+
"rollup": "^4.34.9",
|
| 1648 |
+
"tinyglobby": "^0.2.13"
|
| 1649 |
+
},
|
| 1650 |
+
"bin": {
|
| 1651 |
+
"vite": "bin/vite.js"
|
| 1652 |
+
},
|
| 1653 |
+
"engines": {
|
| 1654 |
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
| 1655 |
+
},
|
| 1656 |
+
"funding": {
|
| 1657 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 1658 |
+
},
|
| 1659 |
+
"optionalDependencies": {
|
| 1660 |
+
"fsevents": "~2.3.3"
|
| 1661 |
+
},
|
| 1662 |
+
"peerDependencies": {
|
| 1663 |
+
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
| 1664 |
+
"jiti": ">=1.21.0",
|
| 1665 |
+
"less": "*",
|
| 1666 |
+
"lightningcss": "^1.21.0",
|
| 1667 |
+
"sass": "*",
|
| 1668 |
+
"sass-embedded": "*",
|
| 1669 |
+
"stylus": "*",
|
| 1670 |
+
"sugarss": "*",
|
| 1671 |
+
"terser": "^5.16.0",
|
| 1672 |
+
"tsx": "^4.8.1",
|
| 1673 |
+
"yaml": "^2.4.2"
|
| 1674 |
+
},
|
| 1675 |
+
"peerDependenciesMeta": {
|
| 1676 |
+
"@types/node": {
|
| 1677 |
+
"optional": true
|
| 1678 |
+
},
|
| 1679 |
+
"jiti": {
|
| 1680 |
+
"optional": true
|
| 1681 |
+
},
|
| 1682 |
+
"less": {
|
| 1683 |
+
"optional": true
|
| 1684 |
+
},
|
| 1685 |
+
"lightningcss": {
|
| 1686 |
+
"optional": true
|
| 1687 |
+
},
|
| 1688 |
+
"sass": {
|
| 1689 |
+
"optional": true
|
| 1690 |
+
},
|
| 1691 |
+
"sass-embedded": {
|
| 1692 |
+
"optional": true
|
| 1693 |
+
},
|
| 1694 |
+
"stylus": {
|
| 1695 |
+
"optional": true
|
| 1696 |
+
},
|
| 1697 |
+
"sugarss": {
|
| 1698 |
+
"optional": true
|
| 1699 |
+
},
|
| 1700 |
+
"terser": {
|
| 1701 |
+
"optional": true
|
| 1702 |
+
},
|
| 1703 |
+
"tsx": {
|
| 1704 |
+
"optional": true
|
| 1705 |
+
},
|
| 1706 |
+
"yaml": {
|
| 1707 |
+
"optional": true
|
| 1708 |
+
}
|
| 1709 |
+
}
|
| 1710 |
+
},
|
| 1711 |
+
"node_modules/vscode-uri": {
|
| 1712 |
+
"version": "3.1.0",
|
| 1713 |
+
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
| 1714 |
+
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
|
| 1715 |
+
"dev": true,
|
| 1716 |
+
"license": "MIT"
|
| 1717 |
+
},
|
| 1718 |
+
"node_modules/vue": {
|
| 1719 |
+
"version": "3.5.22",
|
| 1720 |
+
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
| 1721 |
+
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
| 1722 |
+
"license": "MIT",
|
| 1723 |
+
"peer": true,
|
| 1724 |
+
"dependencies": {
|
| 1725 |
+
"@vue/compiler-dom": "3.5.22",
|
| 1726 |
+
"@vue/compiler-sfc": "3.5.22",
|
| 1727 |
+
"@vue/runtime-dom": "3.5.22",
|
| 1728 |
+
"@vue/server-renderer": "3.5.22",
|
| 1729 |
+
"@vue/shared": "3.5.22"
|
| 1730 |
+
},
|
| 1731 |
+
"peerDependencies": {
|
| 1732 |
+
"typescript": "*"
|
| 1733 |
+
},
|
| 1734 |
+
"peerDependenciesMeta": {
|
| 1735 |
+
"typescript": {
|
| 1736 |
+
"optional": true
|
| 1737 |
+
}
|
| 1738 |
+
}
|
| 1739 |
+
},
|
| 1740 |
+
"node_modules/vue-tsc": {
|
| 1741 |
+
"version": "2.2.12",
|
| 1742 |
+
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
|
| 1743 |
+
"integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
|
| 1744 |
+
"dev": true,
|
| 1745 |
+
"license": "MIT",
|
| 1746 |
+
"dependencies": {
|
| 1747 |
+
"@volar/typescript": "2.4.15",
|
| 1748 |
+
"@vue/language-core": "2.2.12"
|
| 1749 |
+
},
|
| 1750 |
+
"bin": {
|
| 1751 |
+
"vue-tsc": "bin/vue-tsc.js"
|
| 1752 |
+
},
|
| 1753 |
+
"peerDependencies": {
|
| 1754 |
+
"typescript": ">=5.0.0"
|
| 1755 |
+
}
|
| 1756 |
+
}
|
| 1757 |
+
}
|
| 1758 |
+
}
|
frontend/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "helloagents-deepresearch-frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.1.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vue-tsc --noEmit && vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"axios": "^1.7.9",
|
| 13 |
+
"vue": "^3.5.13"
|
| 14 |
+
},
|
| 15 |
+
"devDependencies": {
|
| 16 |
+
"@types/node": "^22.10.5",
|
| 17 |
+
"@vitejs/plugin-vue": "^5.2.1",
|
| 18 |
+
"typescript": "^5.7.3",
|
| 19 |
+
"vite": "^6.0.7",
|
| 20 |
+
"vue-tsc": "^2.2.0"
|
| 21 |
+
}
|
| 22 |
+
}
|
frontend/src/App.vue
ADDED
|
@@ -0,0 +1,2304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<main class="app-shell" :class="{ expanded: isExpanded }">
|
| 3 |
+
<div class="aurora" aria-hidden="true">
|
| 4 |
+
<span></span>
|
| 5 |
+
<span></span>
|
| 6 |
+
<span></span>
|
| 7 |
+
</div>
|
| 8 |
+
|
| 9 |
+
<!-- Initial state: centered input card -->
|
| 10 |
+
<div v-if="!isExpanded" class="layout layout-centered">
|
| 11 |
+
<section class="panel panel-form panel-centered">
|
| 12 |
+
<header class="panel-head">
|
| 13 |
+
<div class="logo">
|
| 14 |
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
| 15 |
+
<path
|
| 16 |
+
d="M12 2.5c-.7 0-1.4.2-2 .6L4.6 7C3.6 7.6 3 8.7 3 9.9v4.2c0 1.2.6 2.3 1.6 2.9l5.4 3.9c1.2.8 2.8.8 4 0l5.4-3.9c1-.7 1.6-1.7 1.6-2.9V9.9c0-1.2-.6-2.3-1.6-2.9L14 3.1a3.6 3.6 0 0 0-2-.6Z"
|
| 17 |
+
/>
|
| 18 |
+
</svg>
|
| 19 |
+
</div>
|
| 20 |
+
<div>
|
| 21 |
+
<h1>Deep Research Assistant</h1>
|
| 22 |
+
<p>Combining multi-round intelligent search and summarization, presenting insights and citations in real-time.</p>
|
| 23 |
+
</div>
|
| 24 |
+
</header>
|
| 25 |
+
|
| 26 |
+
<form class="form" @submit.prevent="handleSubmit">
|
| 27 |
+
<label class="field">
|
| 28 |
+
<span>Research Topic</span>
|
| 29 |
+
<textarea
|
| 30 |
+
v-model="form.topic"
|
| 31 |
+
placeholder="e.g., Explore key breakthroughs in multimodal models in 2025"
|
| 32 |
+
rows="4"
|
| 33 |
+
required
|
| 34 |
+
></textarea>
|
| 35 |
+
</label>
|
| 36 |
+
|
| 37 |
+
<section class="options">
|
| 38 |
+
<label class="field option">
|
| 39 |
+
<span>Search Engine</span>
|
| 40 |
+
<select v-model="form.searchApi">
|
| 41 |
+
<option value="">Use backend default</option>
|
| 42 |
+
<option
|
| 43 |
+
v-for="option in searchOptions"
|
| 44 |
+
:key="option"
|
| 45 |
+
:value="option"
|
| 46 |
+
>
|
| 47 |
+
{{ option }}
|
| 48 |
+
</option>
|
| 49 |
+
</select>
|
| 50 |
+
</label>
|
| 51 |
+
</section>
|
| 52 |
+
|
| 53 |
+
<div class="form-actions">
|
| 54 |
+
<button class="submit" type="submit" :disabled="loading">
|
| 55 |
+
<span class="submit-label">
|
| 56 |
+
<svg
|
| 57 |
+
v-if="loading"
|
| 58 |
+
class="spinner"
|
| 59 |
+
viewBox="0 0 24 24"
|
| 60 |
+
aria-hidden="true"
|
| 61 |
+
>
|
| 62 |
+
<circle cx="12" cy="12" r="9" stroke-width="3" />
|
| 63 |
+
</svg>
|
| 64 |
+
{{ loading ? "Research in progress..." : "Start Research" }}
|
| 65 |
+
</span>
|
| 66 |
+
</button>
|
| 67 |
+
<button
|
| 68 |
+
v-if="loading"
|
| 69 |
+
type="button"
|
| 70 |
+
class="secondary-btn"
|
| 71 |
+
@click="cancelResearch"
|
| 72 |
+
>
|
| 73 |
+
Cancel Research
|
| 74 |
+
</button>
|
| 75 |
+
</div>
|
| 76 |
+
</form>
|
| 77 |
+
|
| 78 |
+
<p v-if="error" class="error-chip">
|
| 79 |
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
| 80 |
+
<path
|
| 81 |
+
d="M10 3.2c-.3 0-.6.2-.8.5L3.4 15c-.4.7.1 1.6.8 1.6h11.6c.7 0 1.2-.9.8-1.6L10.8 3.7c-.2-.3-.5-.5-.8-.5Zm0 4.3c.4 0 .7.3.7.7v4c0 .4-.3.7-.7.7s-.7-.3-.7-.7V8.2c0-.4.3-.7.7-.7Zm0 6.6a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"
|
| 82 |
+
/>
|
| 83 |
+
</svg>
|
| 84 |
+
{{ error }}
|
| 85 |
+
</p>
|
| 86 |
+
<p v-else-if="loading" class="hint muted">
|
| 87 |
+
Collecting clues and evidence, real-time progress shown on the right.
|
| 88 |
+
</p>
|
| 89 |
+
</section>
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<!-- Fullscreen state: left-right split layout -->
|
| 93 |
+
<div v-else class="layout layout-fullscreen">
|
| 94 |
+
<!-- Left side: Research info -->
|
| 95 |
+
<aside class="sidebar">
|
| 96 |
+
<div class="sidebar-header">
|
| 97 |
+
<button class="back-btn" @click="goBack" :disabled="loading">
|
| 98 |
+
<svg viewBox="0 0 24 24" width="20" height="20">
|
| 99 |
+
<path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
| 100 |
+
</svg>
|
| 101 |
+
Back
|
| 102 |
+
</button>
|
| 103 |
+
<h2>🔍 Deep Research Assistant</h2>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<div class="research-info">
|
| 107 |
+
<div class="info-item">
|
| 108 |
+
<label>Research Topic</label>
|
| 109 |
+
<p class="topic-display">{{ form.topic }}</p>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<div class="info-item" v-if="form.searchApi">
|
| 113 |
+
<label>Search Engine</label>
|
| 114 |
+
<p>{{ form.searchApi }}</p>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<div class="info-item" v-if="totalTasks > 0">
|
| 118 |
+
<label>Research Progress</label>
|
| 119 |
+
<div class="progress-bar">
|
| 120 |
+
<div class="progress-fill" :style="{ width: `${(completedTasks / totalTasks) * 100}%` }"></div>
|
| 121 |
+
</div>
|
| 122 |
+
<p class="progress-text">{{ completedTasks }} / {{ totalTasks }} tasks completed</p>
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
|
| 126 |
+
<div class="sidebar-actions">
|
| 127 |
+
<button class="new-research-btn" @click="startNewResearch">
|
| 128 |
+
<svg viewBox="0 0 24 24" width="18" height="18">
|
| 129 |
+
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
|
| 130 |
+
</svg>
|
| 131 |
+
Start New Research
|
| 132 |
+
</button>
|
| 133 |
+
</div>
|
| 134 |
+
</aside>
|
| 135 |
+
|
| 136 |
+
<!-- Right side: Research results -->
|
| 137 |
+
<section
|
| 138 |
+
class="panel panel-result"
|
| 139 |
+
v-if="todoTasks.length || reportMarkdown || progressLogs.length"
|
| 140 |
+
>
|
| 141 |
+
<header class="status-bar">
|
| 142 |
+
<div class="status-main">
|
| 143 |
+
<div class="status-chip" :class="{ active: loading }">
|
| 144 |
+
<span class="dot"></span>
|
| 145 |
+
{{ loading ? "Research in progress" : "Research completed" }}
|
| 146 |
+
</div>
|
| 147 |
+
<span class="status-meta">
|
| 148 |
+
Task progress: {{ completedTasks }} / {{ totalTasks || todoTasks.length || 1 }}
|
| 149 |
+
· {{ progressLogs.length }} phase records
|
| 150 |
+
</span>
|
| 151 |
+
</div>
|
| 152 |
+
<div class="status-controls">
|
| 153 |
+
<button class="secondary-btn" @click="logsCollapsed = !logsCollapsed">
|
| 154 |
+
{{ logsCollapsed ? "Expand process" : "Collapse process" }}
|
| 155 |
+
</button>
|
| 156 |
+
</div>
|
| 157 |
+
</header>
|
| 158 |
+
|
| 159 |
+
<div class="timeline-wrapper" v-show="!logsCollapsed && progressLogs.length">
|
| 160 |
+
<transition-group name="timeline" tag="ul" class="timeline">
|
| 161 |
+
<li v-for="(log, index) in progressLogs" :key="`${log}-${index}`">
|
| 162 |
+
<span class="timeline-node"></span>
|
| 163 |
+
<p>{{ log }}</p>
|
| 164 |
+
</li>
|
| 165 |
+
</transition-group>
|
| 166 |
+
</div>
|
| 167 |
+
|
| 168 |
+
<div class="tasks-section" v-if="todoTasks.length">
|
| 169 |
+
<aside class="tasks-list">
|
| 170 |
+
<h3>Task List</h3>
|
| 171 |
+
<ul>
|
| 172 |
+
<li
|
| 173 |
+
v-for="task in todoTasks"
|
| 174 |
+
:key="task.id"
|
| 175 |
+
:class="['task-item', { active: task.id === activeTaskId, completed: task.status === 'completed' }]"
|
| 176 |
+
>
|
| 177 |
+
<button
|
| 178 |
+
type="button"
|
| 179 |
+
class="task-button"
|
| 180 |
+
@click="activeTaskId = task.id"
|
| 181 |
+
>
|
| 182 |
+
<span class="task-title">{{ task.title }}</span>
|
| 183 |
+
<span class="task-status" :class="task.status">
|
| 184 |
+
{{ formatTaskStatus(task.status) }}
|
| 185 |
+
</span>
|
| 186 |
+
</button>
|
| 187 |
+
<p class="task-intent">{{ task.intent }}</p>
|
| 188 |
+
</li>
|
| 189 |
+
</ul>
|
| 190 |
+
</aside>
|
| 191 |
+
|
| 192 |
+
<article class="task-detail" v-if="currentTask">
|
| 193 |
+
<header class="task-header">
|
| 194 |
+
<div>
|
| 195 |
+
<h3>{{ currentTaskTitle || "Current Task" }}</h3>
|
| 196 |
+
<p class="muted" v-if="currentTaskIntent">
|
| 197 |
+
{{ currentTaskIntent }}
|
| 198 |
+
</p>
|
| 199 |
+
</div>
|
| 200 |
+
<div class="task-chip-group">
|
| 201 |
+
<span class="task-label">Query: {{ currentTaskQuery || "" }}</span>
|
| 202 |
+
<span
|
| 203 |
+
v-if="currentTaskNoteId"
|
| 204 |
+
class="task-label note-chip"
|
| 205 |
+
:title="currentTaskNoteId"
|
| 206 |
+
>
|
| 207 |
+
Note: {{ currentTaskNoteId }}
|
| 208 |
+
</span>
|
| 209 |
+
<span
|
| 210 |
+
v-if="currentTaskNotePath"
|
| 211 |
+
class="task-label note-chip path-chip"
|
| 212 |
+
:title="currentTaskNotePath"
|
| 213 |
+
>
|
| 214 |
+
<span class="path-label">Path:</span>
|
| 215 |
+
<span class="path-text">{{ currentTaskNotePath }}</span>
|
| 216 |
+
<button
|
| 217 |
+
class="chip-action"
|
| 218 |
+
type="button"
|
| 219 |
+
@click="copyNotePath(currentTaskNotePath)"
|
| 220 |
+
>
|
| 221 |
+
Copy
|
| 222 |
+
</button>
|
| 223 |
+
</span>
|
| 224 |
+
</div>
|
| 225 |
+
</header>
|
| 226 |
+
|
| 227 |
+
<section v-if="currentTask && currentTask.notices.length" class="task-notices">
|
| 228 |
+
<h4>System Notices</h4>
|
| 229 |
+
<ul>
|
| 230 |
+
<li v-for="(notice, idx) in currentTask.notices" :key="`${notice}-${idx}`">
|
| 231 |
+
{{ notice }}
|
| 232 |
+
</li>
|
| 233 |
+
</ul>
|
| 234 |
+
</section>
|
| 235 |
+
|
| 236 |
+
<section
|
| 237 |
+
class="sources-block"
|
| 238 |
+
:class="{ 'block-highlight': sourcesHighlight }"
|
| 239 |
+
>
|
| 240 |
+
<h3>Latest Sources</h3>
|
| 241 |
+
<template v-if="currentTaskSources.length">
|
| 242 |
+
<ul class="sources-list">
|
| 243 |
+
<li
|
| 244 |
+
v-for="(item, index) in currentTaskSources"
|
| 245 |
+
:key="`${item.title}-${index}`"
|
| 246 |
+
class="source-item"
|
| 247 |
+
>
|
| 248 |
+
<a
|
| 249 |
+
class="source-link"
|
| 250 |
+
:href="item.url || '#'"
|
| 251 |
+
target="_blank"
|
| 252 |
+
rel="noopener noreferrer"
|
| 253 |
+
>
|
| 254 |
+
{{ item.title || item.url || `Source ${index + 1}` }}
|
| 255 |
+
</a>
|
| 256 |
+
<div v-if="item.snippet || item.raw" class="source-tooltip">
|
| 257 |
+
<p v-if="item.snippet">{{ item.snippet }}</p>
|
| 258 |
+
<p v-if="item.raw" class="muted-text">{{ item.raw }}</p>
|
| 259 |
+
</div>
|
| 260 |
+
</li>
|
| 261 |
+
</ul>
|
| 262 |
+
</template>
|
| 263 |
+
<p v-else class="muted">No sources available</p>
|
| 264 |
+
</section>
|
| 265 |
+
|
| 266 |
+
<section
|
| 267 |
+
class="summary-block"
|
| 268 |
+
:class="{ 'block-highlight': summaryHighlight }"
|
| 269 |
+
>
|
| 270 |
+
<h3>Task Summary</h3>
|
| 271 |
+
<pre class="block-pre">{{ currentTaskSummary || "No information available" }}</pre>
|
| 272 |
+
</section>
|
| 273 |
+
|
| 274 |
+
<section
|
| 275 |
+
class="tools-block"
|
| 276 |
+
:class="{ 'block-highlight': toolHighlight }"
|
| 277 |
+
v-if="currentTaskToolCalls.length"
|
| 278 |
+
>
|
| 279 |
+
<h3>Tool Call Records</h3>
|
| 280 |
+
<ul class="tool-list">
|
| 281 |
+
<li
|
| 282 |
+
v-for="entry in currentTaskToolCalls"
|
| 283 |
+
:key="`${entry.eventId}-${entry.timestamp}`"
|
| 284 |
+
class="tool-entry"
|
| 285 |
+
>
|
| 286 |
+
<div class="tool-entry-header">
|
| 287 |
+
<span class="tool-entry-title">
|
| 288 |
+
#{{ entry.eventId }} {{ entry.agent }} → {{ entry.tool }}
|
| 289 |
+
</span>
|
| 290 |
+
<span
|
| 291 |
+
v-if="entry.noteId"
|
| 292 |
+
class="tool-entry-note"
|
| 293 |
+
>
|
| 294 |
+
Note: {{ entry.noteId }}
|
| 295 |
+
</span>
|
| 296 |
+
</div>
|
| 297 |
+
<p v-if="entry.notePath" class="tool-entry-path">
|
| 298 |
+
Note path:
|
| 299 |
+
<button
|
| 300 |
+
class="link-btn"
|
| 301 |
+
type="button"
|
| 302 |
+
@click="copyNotePath(entry.notePath)"
|
| 303 |
+
>
|
| 304 |
+
Copy
|
| 305 |
+
</button>
|
| 306 |
+
<span class="path-text">{{ entry.notePath }}</span>
|
| 307 |
+
</p>
|
| 308 |
+
<p class="tool-subtitle">Parameters</p>
|
| 309 |
+
<pre class="tool-pre">{{ formatToolParameters(entry.parameters) }}</pre>
|
| 310 |
+
<template v-if="entry.result">
|
| 311 |
+
<p class="tool-subtitle">Execution Result</p>
|
| 312 |
+
<pre class="tool-pre">{{ formatToolResult(entry.result) }}</pre>
|
| 313 |
+
</template>
|
| 314 |
+
</li>
|
| 315 |
+
</ul>
|
| 316 |
+
</section>
|
| 317 |
+
</article>
|
| 318 |
+
|
| 319 |
+
<article class="task-detail" v-else>
|
| 320 |
+
<p class="muted">Waiting for task planning or execution results.</p>
|
| 321 |
+
</article>
|
| 322 |
+
</div>
|
| 323 |
+
|
| 324 |
+
<div
|
| 325 |
+
v-if="reportMarkdown"
|
| 326 |
+
class="report-block"
|
| 327 |
+
:class="{ 'block-highlight': reportHighlight }"
|
| 328 |
+
>
|
| 329 |
+
<h3>Final Report</h3>
|
| 330 |
+
<pre class="block-pre">{{ reportMarkdown }}</pre>
|
| 331 |
+
</div>
|
| 332 |
+
</section>
|
| 333 |
+
|
| 334 |
+
</div>
|
| 335 |
+
</main>
|
| 336 |
+
</template>
|
| 337 |
+
|
| 338 |
+
<script lang="ts" setup>
|
| 339 |
+
import { computed, onBeforeUnmount, reactive, ref } from "vue";
|
| 340 |
+
|
| 341 |
+
import {
|
| 342 |
+
runResearchStream,
|
| 343 |
+
type ResearchStreamEvent
|
| 344 |
+
} from "./services/api";
|
| 345 |
+
|
| 346 |
+
interface SourceItem {
|
| 347 |
+
title: string;
|
| 348 |
+
url: string;
|
| 349 |
+
snippet: string;
|
| 350 |
+
raw: string;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
interface ToolCallLog {
|
| 354 |
+
eventId: number;
|
| 355 |
+
agent: string;
|
| 356 |
+
tool: string;
|
| 357 |
+
parameters: Record<string, unknown>;
|
| 358 |
+
result: string;
|
| 359 |
+
noteId: string | null;
|
| 360 |
+
notePath: string | null;
|
| 361 |
+
timestamp: number;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
interface TodoTaskView {
|
| 365 |
+
id: number;
|
| 366 |
+
title: string;
|
| 367 |
+
intent: string;
|
| 368 |
+
query: string;
|
| 369 |
+
status: string;
|
| 370 |
+
summary: string;
|
| 371 |
+
sourcesSummary: string;
|
| 372 |
+
sourceItems: SourceItem[];
|
| 373 |
+
notices: string[];
|
| 374 |
+
noteId: string | null;
|
| 375 |
+
notePath: string | null;
|
| 376 |
+
toolCalls: ToolCallLog[];
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
const form = reactive({
|
| 380 |
+
topic: "",
|
| 381 |
+
searchApi: ""
|
| 382 |
+
});
|
| 383 |
+
|
| 384 |
+
const loading = ref(false);
|
| 385 |
+
const error = ref("");
|
| 386 |
+
const progressLogs = ref<string[]>([]);
|
| 387 |
+
const logsCollapsed = ref(false);
|
| 388 |
+
const isExpanded = ref(false);
|
| 389 |
+
|
| 390 |
+
const todoTasks = ref<TodoTaskView[]>([]);
|
| 391 |
+
const activeTaskId = ref<number | null>(null);
|
| 392 |
+
const reportMarkdown = ref("");
|
| 393 |
+
|
| 394 |
+
const summaryHighlight = ref(false);
|
| 395 |
+
const sourcesHighlight = ref(false);
|
| 396 |
+
const reportHighlight = ref(false);
|
| 397 |
+
const toolHighlight = ref(false);
|
| 398 |
+
|
| 399 |
+
let currentController: AbortController | null = null;
|
| 400 |
+
|
| 401 |
+
const searchOptions = [
|
| 402 |
+
"advanced",
|
| 403 |
+
"duckduckgo",
|
| 404 |
+
"tavily",
|
| 405 |
+
"perplexity",
|
| 406 |
+
"searxng"
|
| 407 |
+
];
|
| 408 |
+
|
| 409 |
+
const TASK_STATUS_LABEL: Record<string, string> = {
|
| 410 |
+
pending: "Pending",
|
| 411 |
+
in_progress: "In Progress",
|
| 412 |
+
completed: "Completed",
|
| 413 |
+
skipped: "Skipped"
|
| 414 |
+
};
|
| 415 |
+
|
| 416 |
+
function formatTaskStatus(status: string): string {
|
| 417 |
+
return TASK_STATUS_LABEL[status] ?? status;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
const totalTasks = computed(() => todoTasks.value.length);
|
| 421 |
+
const completedTasks = computed(() =>
|
| 422 |
+
todoTasks.value.filter((task) => task.status === "completed").length
|
| 423 |
+
);
|
| 424 |
+
|
| 425 |
+
const currentTask = computed(() => {
|
| 426 |
+
if (activeTaskId.value !== null) {
|
| 427 |
+
return todoTasks.value.find((task) => task.id === activeTaskId.value) ?? null;
|
| 428 |
+
}
|
| 429 |
+
return todoTasks.value[0] ?? null;
|
| 430 |
+
});
|
| 431 |
+
|
| 432 |
+
const currentTaskSources = computed(() => currentTask.value?.sourceItems ?? []);
|
| 433 |
+
const currentTaskSummary = computed(() => currentTask.value?.summary ?? "");
|
| 434 |
+
const currentTaskTitle = computed(() => currentTask.value?.title ?? "");
|
| 435 |
+
const currentTaskIntent = computed(() => currentTask.value?.intent ?? "");
|
| 436 |
+
const currentTaskQuery = computed(() => currentTask.value?.query ?? "");
|
| 437 |
+
const currentTaskNoteId = computed(() => currentTask.value?.noteId ?? "");
|
| 438 |
+
const currentTaskNotePath = computed(() => currentTask.value?.notePath ?? "");
|
| 439 |
+
const currentTaskToolCalls = computed(
|
| 440 |
+
() => currentTask.value?.toolCalls ?? []
|
| 441 |
+
);
|
| 442 |
+
|
| 443 |
+
const pulse = (flag: typeof summaryHighlight) => {
|
| 444 |
+
flag.value = false;
|
| 445 |
+
requestAnimationFrame(() => {
|
| 446 |
+
flag.value = true;
|
| 447 |
+
window.setTimeout(() => {
|
| 448 |
+
flag.value = false;
|
| 449 |
+
}, 1200);
|
| 450 |
+
});
|
| 451 |
+
};
|
| 452 |
+
|
| 453 |
+
function parseSources(raw: string): SourceItem[] {
|
| 454 |
+
if (!raw) {
|
| 455 |
+
return [];
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
const items: SourceItem[] = [];
|
| 459 |
+
const lines = raw.split("\n");
|
| 460 |
+
|
| 461 |
+
let current: SourceItem | null = null;
|
| 462 |
+
const truncate = (value: string, max = 360) => {
|
| 463 |
+
const trimmed = value.trim();
|
| 464 |
+
return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed;
|
| 465 |
+
};
|
| 466 |
+
|
| 467 |
+
const flush = () => {
|
| 468 |
+
if (!current) {
|
| 469 |
+
return;
|
| 470 |
+
}
|
| 471 |
+
const normalized: SourceItem = {
|
| 472 |
+
title: current.title?.trim() || "",
|
| 473 |
+
url: current.url?.trim() || "",
|
| 474 |
+
snippet: current.snippet ? truncate(current.snippet) : "",
|
| 475 |
+
raw: current.raw ? truncate(current.raw, 420) : ""
|
| 476 |
+
};
|
| 477 |
+
|
| 478 |
+
if (
|
| 479 |
+
normalized.title ||
|
| 480 |
+
normalized.url ||
|
| 481 |
+
normalized.snippet ||
|
| 482 |
+
normalized.raw
|
| 483 |
+
) {
|
| 484 |
+
if (!normalized.title && normalized.url) {
|
| 485 |
+
normalized.title = normalized.url;
|
| 486 |
+
}
|
| 487 |
+
items.push(normalized);
|
| 488 |
+
}
|
| 489 |
+
current = null;
|
| 490 |
+
};
|
| 491 |
+
|
| 492 |
+
const ensureCurrent = () => {
|
| 493 |
+
if (!current) {
|
| 494 |
+
current = { title: "", url: "", snippet: "", raw: "" };
|
| 495 |
+
}
|
| 496 |
+
};
|
| 497 |
+
|
| 498 |
+
for (const line of lines) {
|
| 499 |
+
const trimmed = line.trim();
|
| 500 |
+
if (!trimmed) {
|
| 501 |
+
continue;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
if (/^\*/.test(trimmed) && trimmed.includes(" : ")) {
|
| 505 |
+
flush();
|
| 506 |
+
const withoutBullet = trimmed.replace(/^\*\s*/, "");
|
| 507 |
+
const [titlePart, urlPart] = withoutBullet.split(" : ");
|
| 508 |
+
current = {
|
| 509 |
+
title: titlePart?.trim() || "",
|
| 510 |
+
url: urlPart?.trim() || "",
|
| 511 |
+
snippet: "",
|
| 512 |
+
raw: ""
|
| 513 |
+
};
|
| 514 |
+
continue;
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
if (/^(Source|Info Source)\s*:/.test(trimmed)) {
|
| 518 |
+
flush();
|
| 519 |
+
const [, titlePart = ""] = trimmed.split(/:\s*(.+)/);
|
| 520 |
+
current = {
|
| 521 |
+
title: titlePart.trim(),
|
| 522 |
+
url: "",
|
| 523 |
+
snippet: "",
|
| 524 |
+
raw: ""
|
| 525 |
+
};
|
| 526 |
+
continue;
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
if (/^URL\s*:/.test(trimmed)) {
|
| 530 |
+
ensureCurrent();
|
| 531 |
+
const [, urlPart = ""] = trimmed.split(/:\s*(.+)/);
|
| 532 |
+
current!.url = urlPart.trim();
|
| 533 |
+
continue;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
if (
|
| 537 |
+
/^(Most relevant content from source|Info Content)\s*:/.test(trimmed)
|
| 538 |
+
) {
|
| 539 |
+
ensureCurrent();
|
| 540 |
+
const [, contentPart = ""] = trimmed.split(/:\s*(.+)/);
|
| 541 |
+
current!.snippet = contentPart.trim();
|
| 542 |
+
continue;
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
if (
|
| 546 |
+
/^(Full source content limited to|Info content limited to)\s*:/.test(trimmed)
|
| 547 |
+
) {
|
| 548 |
+
ensureCurrent();
|
| 549 |
+
const [, rawPart = ""] = trimmed.split(/:\s*(.+)/);
|
| 550 |
+
current!.raw = rawPart.trim();
|
| 551 |
+
continue;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
if (/^https?:\/\//.test(trimmed)) {
|
| 555 |
+
ensureCurrent();
|
| 556 |
+
if (!current!.url) {
|
| 557 |
+
current!.url = trimmed;
|
| 558 |
+
continue;
|
| 559 |
+
}
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
ensureCurrent();
|
| 563 |
+
current!.raw = current!.raw ? `${current!.raw}\n${trimmed}` : trimmed;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
flush();
|
| 567 |
+
return items;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
function extractOptionalString(value: unknown): string | null {
|
| 571 |
+
if (typeof value !== "string") {
|
| 572 |
+
return null;
|
| 573 |
+
}
|
| 574 |
+
const trimmed = value.trim();
|
| 575 |
+
return trimmed ? trimmed : null;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
function ensureRecord(value: unknown): Record<string, unknown> {
|
| 579 |
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
| 580 |
+
return value as Record<string, unknown>;
|
| 581 |
+
}
|
| 582 |
+
return {};
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
function applyNoteMetadata(
|
| 586 |
+
task: TodoTaskView,
|
| 587 |
+
payload: Record<string, unknown>
|
| 588 |
+
): void {
|
| 589 |
+
const noteId = extractOptionalString(payload.note_id);
|
| 590 |
+
if (noteId) {
|
| 591 |
+
task.noteId = noteId;
|
| 592 |
+
}
|
| 593 |
+
const notePath = extractOptionalString(payload.note_path);
|
| 594 |
+
if (notePath) {
|
| 595 |
+
task.notePath = notePath;
|
| 596 |
+
}
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
function formatToolParameters(parameters: Record<string, unknown>): string {
|
| 600 |
+
try {
|
| 601 |
+
return JSON.stringify(parameters, null, 2);
|
| 602 |
+
} catch (error) {
|
| 603 |
+
console.warn("Unable to format tool parameters", error, parameters);
|
| 604 |
+
return Object.entries(parameters)
|
| 605 |
+
.map(([key, value]) => `${key}: ${String(value)}`)
|
| 606 |
+
.join("\n");
|
| 607 |
+
}
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
function formatToolResult(result: string): string {
|
| 611 |
+
const trimmed = result.trim();
|
| 612 |
+
const limit = 900;
|
| 613 |
+
if (trimmed.length > limit) {
|
| 614 |
+
return `${trimmed.slice(0, limit)}…`;
|
| 615 |
+
}
|
| 616 |
+
return trimmed;
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
async function copyNotePath(path: string | null | undefined) {
|
| 620 |
+
if (!path) {
|
| 621 |
+
return;
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
try {
|
| 625 |
+
await navigator.clipboard.writeText(path);
|
| 626 |
+
progressLogs.value.push(`Note path copied: ${path}`);
|
| 627 |
+
} catch (error) {
|
| 628 |
+
console.warn("Unable to copy to clipboard directly", error);
|
| 629 |
+
window.prompt("Copy the following note path", path);
|
| 630 |
+
progressLogs.value.push("Please copy the note path manually");
|
| 631 |
+
}
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
function resetWorkflowState() {
|
| 635 |
+
todoTasks.value = [];
|
| 636 |
+
activeTaskId.value = null;
|
| 637 |
+
reportMarkdown.value = "";
|
| 638 |
+
progressLogs.value = [];
|
| 639 |
+
summaryHighlight.value = false;
|
| 640 |
+
sourcesHighlight.value = false;
|
| 641 |
+
reportHighlight.value = false;
|
| 642 |
+
toolHighlight.value = false;
|
| 643 |
+
logsCollapsed.value = false;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
function findTask(taskId: unknown): TodoTaskView | undefined {
|
| 647 |
+
const numeric =
|
| 648 |
+
typeof taskId === "number"
|
| 649 |
+
? taskId
|
| 650 |
+
: typeof taskId === "string"
|
| 651 |
+
? Number(taskId)
|
| 652 |
+
: NaN;
|
| 653 |
+
if (Number.isNaN(numeric)) {
|
| 654 |
+
return undefined;
|
| 655 |
+
}
|
| 656 |
+
return todoTasks.value.find((task) => task.id === numeric);
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
function upsertTaskMetadata(task: TodoTaskView, payload: Record<string, unknown>) {
|
| 660 |
+
if (typeof payload.title === "string" && payload.title.trim()) {
|
| 661 |
+
task.title = payload.title.trim();
|
| 662 |
+
}
|
| 663 |
+
if (typeof payload.intent === "string" && payload.intent.trim()) {
|
| 664 |
+
task.intent = payload.intent.trim();
|
| 665 |
+
}
|
| 666 |
+
if (typeof payload.query === "string" && payload.query.trim()) {
|
| 667 |
+
task.query = payload.query.trim();
|
| 668 |
+
}
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
const handleSubmit = async () => {
|
| 672 |
+
if (!form.topic.trim()) {
|
| 673 |
+
error.value = "Please enter a research topic";
|
| 674 |
+
return;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
if (currentController) {
|
| 678 |
+
currentController.abort();
|
| 679 |
+
currentController = null;
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
loading.value = true;
|
| 683 |
+
error.value = "";
|
| 684 |
+
isExpanded.value = true;
|
| 685 |
+
resetWorkflowState();
|
| 686 |
+
|
| 687 |
+
const controller = new AbortController();
|
| 688 |
+
currentController = controller;
|
| 689 |
+
|
| 690 |
+
const payload = {
|
| 691 |
+
topic: form.topic.trim(),
|
| 692 |
+
search_api: form.searchApi || undefined
|
| 693 |
+
};
|
| 694 |
+
|
| 695 |
+
try {
|
| 696 |
+
await runResearchStream(
|
| 697 |
+
payload,
|
| 698 |
+
(event: ResearchStreamEvent) => {
|
| 699 |
+
if (event.type === "status") {
|
| 700 |
+
const message =
|
| 701 |
+
typeof event.message === "string" && event.message.trim()
|
| 702 |
+
? event.message
|
| 703 |
+
: "Workflow status update";
|
| 704 |
+
progressLogs.value.push(message);
|
| 705 |
+
|
| 706 |
+
const payload = event as Record<string, unknown>;
|
| 707 |
+
const task = findTask(payload.task_id);
|
| 708 |
+
if (task && message) {
|
| 709 |
+
task.notices.push(message);
|
| 710 |
+
applyNoteMetadata(task, payload);
|
| 711 |
+
}
|
| 712 |
+
return;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
if (event.type === "todo_list") {
|
| 716 |
+
const tasks = Array.isArray(event.tasks)
|
| 717 |
+
? (event.tasks as Record<string, unknown>[])
|
| 718 |
+
: [];
|
| 719 |
+
|
| 720 |
+
todoTasks.value = tasks.map((item, index) => {
|
| 721 |
+
const rawId =
|
| 722 |
+
typeof item.id === "number"
|
| 723 |
+
? item.id
|
| 724 |
+
: typeof item.id === "string"
|
| 725 |
+
? Number(item.id)
|
| 726 |
+
: index + 1;
|
| 727 |
+
const id = Number.isFinite(rawId) ? Number(rawId) : index + 1;
|
| 728 |
+
const noteId =
|
| 729 |
+
typeof item.note_id === "string" && item.note_id.trim()
|
| 730 |
+
? item.note_id.trim()
|
| 731 |
+
: null;
|
| 732 |
+
const notePath =
|
| 733 |
+
typeof item.note_path === "string" && item.note_path.trim()
|
| 734 |
+
? item.note_path.trim()
|
| 735 |
+
: null;
|
| 736 |
+
|
| 737 |
+
return {
|
| 738 |
+
id,
|
| 739 |
+
title:
|
| 740 |
+
typeof item.title === "string" && item.title.trim()
|
| 741 |
+
? item.title.trim()
|
| 742 |
+
: `Task ${id}`,
|
| 743 |
+
intent:
|
| 744 |
+
typeof item.intent === "string" && item.intent.trim()
|
| 745 |
+
? item.intent.trim()
|
| 746 |
+
: "Explore key information related to the topic",
|
| 747 |
+
query:
|
| 748 |
+
typeof item.query === "string" && item.query.trim()
|
| 749 |
+
? item.query.trim()
|
| 750 |
+
: form.topic.trim(),
|
| 751 |
+
status:
|
| 752 |
+
typeof item.status === "string" && item.status.trim()
|
| 753 |
+
? item.status.trim()
|
| 754 |
+
: "pending",
|
| 755 |
+
summary: "",
|
| 756 |
+
sourcesSummary: "",
|
| 757 |
+
sourceItems: [],
|
| 758 |
+
notices: [],
|
| 759 |
+
noteId,
|
| 760 |
+
notePath,
|
| 761 |
+
toolCalls: []
|
| 762 |
+
} as TodoTaskView;
|
| 763 |
+
});
|
| 764 |
+
|
| 765 |
+
if (todoTasks.value.length) {
|
| 766 |
+
activeTaskId.value = todoTasks.value[0].id;
|
| 767 |
+
progressLogs.value.push("Task list generated");
|
| 768 |
+
} else {
|
| 769 |
+
progressLogs.value.push("No task list generated, continuing with default task");
|
| 770 |
+
}
|
| 771 |
+
return;
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
if (event.type === "task_status") {
|
| 775 |
+
const payload = event as Record<string, unknown>;
|
| 776 |
+
const task = findTask(event.task_id);
|
| 777 |
+
if (!task) {
|
| 778 |
+
return;
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
upsertTaskMetadata(task, payload);
|
| 782 |
+
applyNoteMetadata(task, payload);
|
| 783 |
+
const status =
|
| 784 |
+
typeof event.status === "string" && event.status.trim()
|
| 785 |
+
? event.status.trim()
|
| 786 |
+
: task.status;
|
| 787 |
+
task.status = status;
|
| 788 |
+
|
| 789 |
+
if (status === "in_progress") {
|
| 790 |
+
task.summary = "";
|
| 791 |
+
task.sourcesSummary = "";
|
| 792 |
+
task.sourceItems = [];
|
| 793 |
+
task.notices = [];
|
| 794 |
+
activeTaskId.value = task.id;
|
| 795 |
+
progressLogs.value.push(`Starting task: ${task.title}`);
|
| 796 |
+
} else if (status === "completed") {
|
| 797 |
+
if (typeof event.summary === "string" && event.summary.trim()) {
|
| 798 |
+
task.summary = event.summary.trim();
|
| 799 |
+
}
|
| 800 |
+
if (
|
| 801 |
+
typeof event.sources_summary === "string" &&
|
| 802 |
+
event.sources_summary.trim()
|
| 803 |
+
) {
|
| 804 |
+
task.sourcesSummary = event.sources_summary.trim();
|
| 805 |
+
task.sourceItems = parseSources(task.sourcesSummary);
|
| 806 |
+
}
|
| 807 |
+
progressLogs.value.push(`Completed task: ${task.title}`);
|
| 808 |
+
if (activeTaskId.value === task.id) {
|
| 809 |
+
pulse(summaryHighlight);
|
| 810 |
+
pulse(sourcesHighlight);
|
| 811 |
+
}
|
| 812 |
+
} else if (status === "skipped") {
|
| 813 |
+
progressLogs.value.push(`Task skipped: ${task.title}`);
|
| 814 |
+
}
|
| 815 |
+
return;
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
if (event.type === "sources") {
|
| 819 |
+
const payload = event as Record<string, unknown>;
|
| 820 |
+
const task = findTask(event.task_id);
|
| 821 |
+
if (!task) {
|
| 822 |
+
return;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
const textCandidates = [
|
| 826 |
+
payload.latest_sources,
|
| 827 |
+
payload.sources_summary,
|
| 828 |
+
payload.raw_context
|
| 829 |
+
];
|
| 830 |
+
const latestText = textCandidates
|
| 831 |
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
| 832 |
+
.find((value) => value);
|
| 833 |
+
|
| 834 |
+
if (latestText) {
|
| 835 |
+
task.sourcesSummary = latestText;
|
| 836 |
+
task.sourceItems = parseSources(latestText);
|
| 837 |
+
if (activeTaskId.value === task.id) {
|
| 838 |
+
pulse(sourcesHighlight);
|
| 839 |
+
}
|
| 840 |
+
progressLogs.value.push(`Updated task sources: ${task.title}`);
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
if (typeof payload.backend === "string") {
|
| 844 |
+
progressLogs.value.push(
|
| 845 |
+
`Current search backend: ${payload.backend}`
|
| 846 |
+
);
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
applyNoteMetadata(task, payload);
|
| 850 |
+
|
| 851 |
+
return;
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
if (event.type === "task_summary_chunk") {
|
| 855 |
+
const payload = event as Record<string, unknown>;
|
| 856 |
+
const task = findTask(event.task_id);
|
| 857 |
+
if (!task) {
|
| 858 |
+
return;
|
| 859 |
+
}
|
| 860 |
+
const chunk =
|
| 861 |
+
typeof event.content === "string" ? event.content : "";
|
| 862 |
+
task.summary += chunk;
|
| 863 |
+
applyNoteMetadata(task, payload);
|
| 864 |
+
if (activeTaskId.value === task.id) {
|
| 865 |
+
pulse(summaryHighlight);
|
| 866 |
+
}
|
| 867 |
+
return;
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
if (event.type === "tool_call") {
|
| 871 |
+
const payload = event as Record<string, unknown>;
|
| 872 |
+
const eventId =
|
| 873 |
+
typeof payload.event_id === "number"
|
| 874 |
+
? payload.event_id
|
| 875 |
+
: Date.now();
|
| 876 |
+
const agent =
|
| 877 |
+
typeof payload.agent === "string" && payload.agent.trim()
|
| 878 |
+
? payload.agent.trim()
|
| 879 |
+
: "Agent";
|
| 880 |
+
const tool =
|
| 881 |
+
typeof payload.tool === "string" && payload.tool.trim()
|
| 882 |
+
? payload.tool.trim()
|
| 883 |
+
: "tool";
|
| 884 |
+
const parameters = ensureRecord(payload.parameters);
|
| 885 |
+
const result =
|
| 886 |
+
typeof payload.result === "string" ? payload.result : "";
|
| 887 |
+
const noteId = extractOptionalString(payload.note_id);
|
| 888 |
+
const notePath = extractOptionalString(payload.note_path);
|
| 889 |
+
|
| 890 |
+
const task = findTask(payload.task_id);
|
| 891 |
+
if (task) {
|
| 892 |
+
task.toolCalls.push({
|
| 893 |
+
eventId,
|
| 894 |
+
agent,
|
| 895 |
+
tool,
|
| 896 |
+
parameters,
|
| 897 |
+
result,
|
| 898 |
+
noteId,
|
| 899 |
+
notePath,
|
| 900 |
+
timestamp: Date.now()
|
| 901 |
+
});
|
| 902 |
+
if (noteId) {
|
| 903 |
+
task.noteId = noteId;
|
| 904 |
+
}
|
| 905 |
+
if (notePath) {
|
| 906 |
+
task.notePath = notePath;
|
| 907 |
+
}
|
| 908 |
+
const logSummary = noteId
|
| 909 |
+
? `${agent} called ${tool} (Task ${task.id}, Note ${noteId})`
|
| 910 |
+
: `${agent} called ${tool} (Task ${task.id})`;
|
| 911 |
+
progressLogs.value.push(logSummary);
|
| 912 |
+
if (activeTaskId.value === task.id) {
|
| 913 |
+
pulse(toolHighlight);
|
| 914 |
+
}
|
| 915 |
+
} else {
|
| 916 |
+
progressLogs.value.push(`${agent} called ${tool}`);
|
| 917 |
+
}
|
| 918 |
+
return;
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
if (event.type === "final_report") {
|
| 922 |
+
const report =
|
| 923 |
+
typeof event.report === "string" && event.report.trim()
|
| 924 |
+
? event.report.trim()
|
| 925 |
+
: "";
|
| 926 |
+
reportMarkdown.value = report || "Report generation failed, no valid content obtained";
|
| 927 |
+
pulse(reportHighlight);
|
| 928 |
+
progressLogs.value.push("Final report generated");
|
| 929 |
+
return;
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
if (event.type === "error") {
|
| 933 |
+
const detail =
|
| 934 |
+
typeof event.detail === "string" && event.detail.trim()
|
| 935 |
+
? event.detail
|
| 936 |
+
: "An error occurred during research";
|
| 937 |
+
error.value = detail;
|
| 938 |
+
progressLogs.value.push("Research failed, workflow stopped");
|
| 939 |
+
}
|
| 940 |
+
},
|
| 941 |
+
{ signal: controller.signal }
|
| 942 |
+
);
|
| 943 |
+
|
| 944 |
+
if (!reportMarkdown.value) {
|
| 945 |
+
reportMarkdown.value = "No report generated";
|
| 946 |
+
}
|
| 947 |
+
} catch (err) {
|
| 948 |
+
if (err instanceof DOMException && err.name === "AbortError") {
|
| 949 |
+
progressLogs.value.push("Current research task cancelled");
|
| 950 |
+
} else {
|
| 951 |
+
error.value = err instanceof Error ? err.message : "Request failed";
|
| 952 |
+
}
|
| 953 |
+
} finally {
|
| 954 |
+
loading.value = false;
|
| 955 |
+
if (currentController === controller) {
|
| 956 |
+
currentController = null;
|
| 957 |
+
}
|
| 958 |
+
}
|
| 959 |
+
};
|
| 960 |
+
|
| 961 |
+
const cancelResearch = () => {
|
| 962 |
+
if (!loading.value || !currentController) {
|
| 963 |
+
return;
|
| 964 |
+
}
|
| 965 |
+
progressLogs.value.push("Attempting to cancel current research task...");
|
| 966 |
+
currentController.abort();
|
| 967 |
+
};
|
| 968 |
+
|
| 969 |
+
const goBack = () => {
|
| 970 |
+
if (loading.value) {
|
| 971 |
+
return; // Cannot go back while research is in progress
|
| 972 |
+
}
|
| 973 |
+
isExpanded.value = false;
|
| 974 |
+
};
|
| 975 |
+
|
| 976 |
+
const startNewResearch = () => {
|
| 977 |
+
if (loading.value) {
|
| 978 |
+
cancelResearch();
|
| 979 |
+
}
|
| 980 |
+
resetWorkflowState();
|
| 981 |
+
isExpanded.value = false;
|
| 982 |
+
form.topic = "";
|
| 983 |
+
form.searchApi = "";
|
| 984 |
+
};
|
| 985 |
+
|
| 986 |
+
onBeforeUnmount(() => {
|
| 987 |
+
if (currentController) {
|
| 988 |
+
currentController.abort();
|
| 989 |
+
currentController = null;
|
| 990 |
+
}
|
| 991 |
+
});
|
| 992 |
+
</script>
|
| 993 |
+
|
| 994 |
+
|
| 995 |
+
<style scoped>
|
| 996 |
+
.app-shell {
|
| 997 |
+
position: relative;
|
| 998 |
+
min-height: 100vh;
|
| 999 |
+
padding: 72px 24px;
|
| 1000 |
+
display: flex;
|
| 1001 |
+
justify-content: center;
|
| 1002 |
+
align-items: center;
|
| 1003 |
+
background: radial-gradient(circle at 20% 20%, #f8fafc, #dbeafe 60%);
|
| 1004 |
+
color: #1f2937;
|
| 1005 |
+
overflow: hidden;
|
| 1006 |
+
box-sizing: border-box;
|
| 1007 |
+
transition: padding 0.4s ease;
|
| 1008 |
+
}
|
| 1009 |
+
|
| 1010 |
+
.app-shell.expanded {
|
| 1011 |
+
padding: 0;
|
| 1012 |
+
align-items: stretch;
|
| 1013 |
+
}
|
| 1014 |
+
|
| 1015 |
+
.aurora {
|
| 1016 |
+
position: absolute;
|
| 1017 |
+
inset: 0;
|
| 1018 |
+
pointer-events: none;
|
| 1019 |
+
opacity: 0.55;
|
| 1020 |
+
}
|
| 1021 |
+
|
| 1022 |
+
.aurora span {
|
| 1023 |
+
position: absolute;
|
| 1024 |
+
width: 45vw;
|
| 1025 |
+
height: 45vw;
|
| 1026 |
+
max-width: 520px;
|
| 1027 |
+
max-height: 520px;
|
| 1028 |
+
background: radial-gradient(circle, rgba(148, 197, 255, 0.35), transparent 60%);
|
| 1029 |
+
filter: blur(90px);
|
| 1030 |
+
animation: float 26s infinite linear;
|
| 1031 |
+
}
|
| 1032 |
+
|
| 1033 |
+
.aurora span:nth-child(1) {
|
| 1034 |
+
top: -20%;
|
| 1035 |
+
left: -18%;
|
| 1036 |
+
animation-delay: 0s;
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
.aurora span:nth-child(2) {
|
| 1040 |
+
bottom: -25%;
|
| 1041 |
+
right: -20%;
|
| 1042 |
+
background: radial-gradient(circle, rgba(166, 139, 255, 0.28), transparent 60%);
|
| 1043 |
+
animation-delay: -9s;
|
| 1044 |
+
}
|
| 1045 |
+
|
| 1046 |
+
.aurora span:nth-child(3) {
|
| 1047 |
+
top: 35%;
|
| 1048 |
+
left: 45%;
|
| 1049 |
+
background: radial-gradient(circle, rgba(164, 219, 216, 0.26), transparent 60%);
|
| 1050 |
+
animation-delay: -16s;
|
| 1051 |
+
}
|
| 1052 |
+
|
| 1053 |
+
.layout {
|
| 1054 |
+
position: relative;
|
| 1055 |
+
width: 100%;
|
| 1056 |
+
display: flex;
|
| 1057 |
+
gap: 24px;
|
| 1058 |
+
z-index: 1;
|
| 1059 |
+
transition: all 0.4s ease;
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
.layout-centered {
|
| 1063 |
+
max-width: 600px;
|
| 1064 |
+
justify-content: center;
|
| 1065 |
+
align-items: center;
|
| 1066 |
+
}
|
| 1067 |
+
|
| 1068 |
+
.layout-fullscreen {
|
| 1069 |
+
height: 100vh;
|
| 1070 |
+
max-width: 100%;
|
| 1071 |
+
gap: 0;
|
| 1072 |
+
align-items: stretch;
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
.panel {
|
| 1076 |
+
position: relative;
|
| 1077 |
+
flex: 1 1 360px;
|
| 1078 |
+
padding: 24px;
|
| 1079 |
+
border-radius: 20px;
|
| 1080 |
+
background: rgba(255, 255, 255, 0.95);
|
| 1081 |
+
border: 1px solid rgba(148, 163, 184, 0.18);
|
| 1082 |
+
box-shadow: 0 24px 48px rgba(15, 23, 42, 0.12);
|
| 1083 |
+
backdrop-filter: blur(8px);
|
| 1084 |
+
overflow: hidden;
|
| 1085 |
+
}
|
| 1086 |
+
|
| 1087 |
+
.panel-form {
|
| 1088 |
+
max-width: 420px;
|
| 1089 |
+
}
|
| 1090 |
+
|
| 1091 |
+
.panel-centered {
|
| 1092 |
+
width: 100%;
|
| 1093 |
+
max-width: 600px;
|
| 1094 |
+
padding: 40px;
|
| 1095 |
+
box-shadow: 0 32px 64px rgba(15, 23, 42, 0.15);
|
| 1096 |
+
transform: scale(1);
|
| 1097 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
.panel-centered:hover {
|
| 1101 |
+
transform: scale(1.02);
|
| 1102 |
+
box-shadow: 0 40px 80px rgba(15, 23, 42, 0.2);
|
| 1103 |
+
}
|
| 1104 |
+
|
| 1105 |
+
.panel-result {
|
| 1106 |
+
min-width: 360px;
|
| 1107 |
+
flex: 2 1 420px;
|
| 1108 |
+
}
|
| 1109 |
+
|
| 1110 |
+
.panel::before {
|
| 1111 |
+
content: "";
|
| 1112 |
+
position: absolute;
|
| 1113 |
+
inset: 0;
|
| 1114 |
+
background: linear-gradient(135deg, rgba(59, 130, 246, 0.12), rgba(125, 86, 255, 0.1));
|
| 1115 |
+
opacity: 0;
|
| 1116 |
+
transition: opacity 0.35s ease;
|
| 1117 |
+
z-index: 0;
|
| 1118 |
+
}
|
| 1119 |
+
|
| 1120 |
+
.panel:hover::before {
|
| 1121 |
+
opacity: 1;
|
| 1122 |
+
}
|
| 1123 |
+
|
| 1124 |
+
.panel > * {
|
| 1125 |
+
position: relative;
|
| 1126 |
+
z-index: 1;
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
+
.panel-form h1 {
|
| 1130 |
+
margin: 0;
|
| 1131 |
+
font-size: 26px;
|
| 1132 |
+
letter-spacing: 0.01em;
|
| 1133 |
+
}
|
| 1134 |
+
|
| 1135 |
+
.panel-form p {
|
| 1136 |
+
margin: 4px 0 0;
|
| 1137 |
+
color: #64748b;
|
| 1138 |
+
font-size: 13px;
|
| 1139 |
+
}
|
| 1140 |
+
|
| 1141 |
+
.panel-head {
|
| 1142 |
+
display: flex;
|
| 1143 |
+
align-items: center;
|
| 1144 |
+
gap: 16px;
|
| 1145 |
+
margin-bottom: 24px;
|
| 1146 |
+
}
|
| 1147 |
+
|
| 1148 |
+
.logo {
|
| 1149 |
+
width: 52px;
|
| 1150 |
+
height: 52px;
|
| 1151 |
+
display: grid;
|
| 1152 |
+
place-items: center;
|
| 1153 |
+
border-radius: 16px;
|
| 1154 |
+
background: linear-gradient(135deg, #2563eb, #7c3aed);
|
| 1155 |
+
box-shadow: 0 12px 28px rgba(59, 130, 246, 0.4);
|
| 1156 |
+
}
|
| 1157 |
+
|
| 1158 |
+
.logo svg {
|
| 1159 |
+
width: 28px;
|
| 1160 |
+
height: 28px;
|
| 1161 |
+
fill: #f8fafc;
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
+
.form {
|
| 1165 |
+
display: flex;
|
| 1166 |
+
flex-direction: column;
|
| 1167 |
+
gap: 18px;
|
| 1168 |
+
}
|
| 1169 |
+
|
| 1170 |
+
.field {
|
| 1171 |
+
display: flex;
|
| 1172 |
+
flex-direction: column;
|
| 1173 |
+
gap: 10px;
|
| 1174 |
+
}
|
| 1175 |
+
|
| 1176 |
+
.field span {
|
| 1177 |
+
font-weight: 600;
|
| 1178 |
+
color: #475569;
|
| 1179 |
+
}
|
| 1180 |
+
|
| 1181 |
+
textarea,
|
| 1182 |
+
input,
|
| 1183 |
+
select {
|
| 1184 |
+
padding: 14px 16px;
|
| 1185 |
+
border-radius: 16px;
|
| 1186 |
+
border: 1px solid rgba(148, 163, 184, 0.35);
|
| 1187 |
+
background: rgba(255, 255, 255, 0.92);
|
| 1188 |
+
color: #1f2937;
|
| 1189 |
+
font-size: 14px;
|
| 1190 |
+
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
|
| 1191 |
+
}
|
| 1192 |
+
|
| 1193 |
+
textarea:focus,
|
| 1194 |
+
input:focus,
|
| 1195 |
+
select:focus {
|
| 1196 |
+
outline: none;
|
| 1197 |
+
border-color: rgba(37, 99, 235, 0.65);
|
| 1198 |
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
| 1199 |
+
background: #ffffff;
|
| 1200 |
+
}
|
| 1201 |
+
|
| 1202 |
+
.options {
|
| 1203 |
+
display: flex;
|
| 1204 |
+
gap: 16px;
|
| 1205 |
+
flex-wrap: wrap;
|
| 1206 |
+
}
|
| 1207 |
+
|
| 1208 |
+
.option {
|
| 1209 |
+
flex: 1;
|
| 1210 |
+
min-width: 140px;
|
| 1211 |
+
}
|
| 1212 |
+
|
| 1213 |
+
.form-actions {
|
| 1214 |
+
display: flex;
|
| 1215 |
+
align-items: center;
|
| 1216 |
+
gap: 12px;
|
| 1217 |
+
flex-wrap: wrap;
|
| 1218 |
+
}
|
| 1219 |
+
|
| 1220 |
+
.submit {
|
| 1221 |
+
align-self: flex-start;
|
| 1222 |
+
padding: 12px 24px;
|
| 1223 |
+
border-radius: 16px;
|
| 1224 |
+
border: none;
|
| 1225 |
+
background: linear-gradient(135deg, #2563eb, #7c3aed);
|
| 1226 |
+
color: #ffffff;
|
| 1227 |
+
font-size: 15px;
|
| 1228 |
+
font-weight: 600;
|
| 1229 |
+
cursor: pointer;
|
| 1230 |
+
transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s;
|
| 1231 |
+
display: inline-flex;
|
| 1232 |
+
align-items: center;
|
| 1233 |
+
gap: 10px;
|
| 1234 |
+
position: relative;
|
| 1235 |
+
}
|
| 1236 |
+
|
| 1237 |
+
.submit-label {
|
| 1238 |
+
display: inline-flex;
|
| 1239 |
+
align-items: center;
|
| 1240 |
+
gap: 10px;
|
| 1241 |
+
}
|
| 1242 |
+
|
| 1243 |
+
.submit .spinner {
|
| 1244 |
+
width: 18px;
|
| 1245 |
+
height: 18px;
|
| 1246 |
+
fill: none;
|
| 1247 |
+
stroke: rgba(255, 255, 255, 0.85);
|
| 1248 |
+
stroke-linecap: round;
|
| 1249 |
+
animation: spin 1s linear infinite;
|
| 1250 |
+
}
|
| 1251 |
+
|
| 1252 |
+
.submit:disabled {
|
| 1253 |
+
opacity: 0.7;
|
| 1254 |
+
cursor: not-allowed;
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
.submit:not(:disabled):hover {
|
| 1258 |
+
transform: translateY(-2px);
|
| 1259 |
+
box-shadow: 0 12px 28px rgba(37, 99, 235, 0.28);
|
| 1260 |
+
}
|
| 1261 |
+
|
| 1262 |
+
.secondary-btn {
|
| 1263 |
+
padding: 10px 18px;
|
| 1264 |
+
border-radius: 14px;
|
| 1265 |
+
background: rgba(148, 163, 184, 0.12);
|
| 1266 |
+
border: 1px solid rgba(148, 163, 184, 0.28);
|
| 1267 |
+
color: #1f2937;
|
| 1268 |
+
font-size: 14px;
|
| 1269 |
+
font-weight: 500;
|
| 1270 |
+
cursor: pointer;
|
| 1271 |
+
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
| 1272 |
+
}
|
| 1273 |
+
|
| 1274 |
+
.secondary-btn:hover {
|
| 1275 |
+
background: rgba(148, 163, 184, 0.2);
|
| 1276 |
+
border-color: rgba(148, 163, 184, 0.35);
|
| 1277 |
+
color: #0f172a;
|
| 1278 |
+
}
|
| 1279 |
+
|
| 1280 |
+
.error-chip {
|
| 1281 |
+
margin-top: 16px;
|
| 1282 |
+
display: inline-flex;
|
| 1283 |
+
align-items: center;
|
| 1284 |
+
gap: 8px;
|
| 1285 |
+
padding: 10px 14px;
|
| 1286 |
+
background: rgba(248, 113, 113, 0.12);
|
| 1287 |
+
border: 1px solid rgba(248, 113, 113, 0.35);
|
| 1288 |
+
border-radius: 14px;
|
| 1289 |
+
color: #b91c1c;
|
| 1290 |
+
font-size: 14px;
|
| 1291 |
+
}
|
| 1292 |
+
|
| 1293 |
+
.error-chip svg {
|
| 1294 |
+
width: 18px;
|
| 1295 |
+
height: 18px;
|
| 1296 |
+
fill: currentColor;
|
| 1297 |
+
}
|
| 1298 |
+
|
| 1299 |
+
.panel-result {
|
| 1300 |
+
display: flex;
|
| 1301 |
+
flex-direction: column;
|
| 1302 |
+
gap: 18px;
|
| 1303 |
+
}
|
| 1304 |
+
|
| 1305 |
+
.status-bar {
|
| 1306 |
+
display: flex;
|
| 1307 |
+
align-items: center;
|
| 1308 |
+
justify-content: space-between;
|
| 1309 |
+
gap: 12px;
|
| 1310 |
+
flex-wrap: wrap;
|
| 1311 |
+
}
|
| 1312 |
+
|
| 1313 |
+
.status-main {
|
| 1314 |
+
display: flex;
|
| 1315 |
+
align-items: center;
|
| 1316 |
+
gap: 12px;
|
| 1317 |
+
flex-wrap: wrap;
|
| 1318 |
+
}
|
| 1319 |
+
|
| 1320 |
+
.status-controls {
|
| 1321 |
+
display: flex;
|
| 1322 |
+
gap: 8px;
|
| 1323 |
+
}
|
| 1324 |
+
|
| 1325 |
+
.status-chip {
|
| 1326 |
+
display: inline-flex;
|
| 1327 |
+
align-items: center;
|
| 1328 |
+
gap: 8px;
|
| 1329 |
+
background: rgba(191, 219, 254, 0.28);
|
| 1330 |
+
padding: 8px 14px;
|
| 1331 |
+
border-radius: 999px;
|
| 1332 |
+
font-size: 13px;
|
| 1333 |
+
color: #1f2937;
|
| 1334 |
+
border: 1px solid rgba(59, 130, 246, 0.35);
|
| 1335 |
+
transition: background 0.3s ease, color 0.3s ease;
|
| 1336 |
+
}
|
| 1337 |
+
|
| 1338 |
+
.status-chip.active {
|
| 1339 |
+
background: rgba(129, 140, 248, 0.2);
|
| 1340 |
+
border-color: rgba(129, 140, 248, 0.4);
|
| 1341 |
+
color: #1e293b;
|
| 1342 |
+
}
|
| 1343 |
+
|
| 1344 |
+
.status-chip .dot {
|
| 1345 |
+
width: 8px;
|
| 1346 |
+
height: 8px;
|
| 1347 |
+
border-radius: 999px;
|
| 1348 |
+
background: #2563eb;
|
| 1349 |
+
box-shadow: 0 0 12px rgba(37, 99, 235, 0.45);
|
| 1350 |
+
animation: pulse 1.8s ease-in-out infinite;
|
| 1351 |
+
}
|
| 1352 |
+
|
| 1353 |
+
.status-meta {
|
| 1354 |
+
color: #64748b;
|
| 1355 |
+
font-size: 13px;
|
| 1356 |
+
}
|
| 1357 |
+
|
| 1358 |
+
.timeline-wrapper {
|
| 1359 |
+
margin-top: 12px;
|
| 1360 |
+
max-height: 220px;
|
| 1361 |
+
overflow-y: auto;
|
| 1362 |
+
padding-right: 8px;
|
| 1363 |
+
scrollbar-width: thin;
|
| 1364 |
+
scrollbar-color: rgba(129, 140, 248, 0.45) rgba(226, 232, 240, 0.6);
|
| 1365 |
+
}
|
| 1366 |
+
|
| 1367 |
+
.timeline-wrapper::-webkit-scrollbar {
|
| 1368 |
+
width: 6px;
|
| 1369 |
+
}
|
| 1370 |
+
|
| 1371 |
+
.timeline-wrapper::-webkit-scrollbar-track {
|
| 1372 |
+
background: rgba(226, 232, 240, 0.6);
|
| 1373 |
+
border-radius: 999px;
|
| 1374 |
+
}
|
| 1375 |
+
|
| 1376 |
+
.timeline-wrapper::-webkit-scrollbar-thumb {
|
| 1377 |
+
background: linear-gradient(180deg, rgba(129, 140, 248, 0.8), rgba(59, 130, 246, 0.7));
|
| 1378 |
+
border-radius: 999px;
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
.timeline-wrapper::-webkit-scrollbar-thumb:hover {
|
| 1382 |
+
background: linear-gradient(180deg, rgba(99, 102, 241, 0.9), rgba(37, 99, 235, 0.8));
|
| 1383 |
+
}
|
| 1384 |
+
|
| 1385 |
+
.timeline {
|
| 1386 |
+
list-style: none;
|
| 1387 |
+
padding: 0;
|
| 1388 |
+
margin: 0;
|
| 1389 |
+
display: flex;
|
| 1390 |
+
flex-direction: column;
|
| 1391 |
+
gap: 14px;
|
| 1392 |
+
position: relative;
|
| 1393 |
+
padding-left: 12px;
|
| 1394 |
+
}
|
| 1395 |
+
|
| 1396 |
+
.timeline::before {
|
| 1397 |
+
content: "";
|
| 1398 |
+
position: absolute;
|
| 1399 |
+
top: 8px;
|
| 1400 |
+
bottom: 8px;
|
| 1401 |
+
left: 0;
|
| 1402 |
+
width: 2px;
|
| 1403 |
+
background: linear-gradient(180deg, rgba(59, 130, 246, 0.35), rgba(129, 140, 248, 0.15));
|
| 1404 |
+
}
|
| 1405 |
+
|
| 1406 |
+
.timeline li {
|
| 1407 |
+
position: relative;
|
| 1408 |
+
padding-left: 24px;
|
| 1409 |
+
color: #1e293b;
|
| 1410 |
+
font-size: 14px;
|
| 1411 |
+
line-height: 1.5;
|
| 1412 |
+
}
|
| 1413 |
+
|
| 1414 |
+
.timeline-node {
|
| 1415 |
+
position: absolute;
|
| 1416 |
+
left: -12px;
|
| 1417 |
+
top: 6px;
|
| 1418 |
+
width: 10px;
|
| 1419 |
+
height: 10px;
|
| 1420 |
+
border-radius: 999px;
|
| 1421 |
+
background: linear-gradient(135deg, #38bdf8, #7c3aed);
|
| 1422 |
+
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.22);
|
| 1423 |
+
}
|
| 1424 |
+
|
| 1425 |
+
.timeline-enter-active,
|
| 1426 |
+
.timeline-leave-active {
|
| 1427 |
+
transition: all 0.35s ease, opacity 0.35s ease;
|
| 1428 |
+
}
|
| 1429 |
+
|
| 1430 |
+
.timeline-enter-from,
|
| 1431 |
+
.timeline-leave-to {
|
| 1432 |
+
opacity: 0;
|
| 1433 |
+
transform: translateY(-6px);
|
| 1434 |
+
}
|
| 1435 |
+
|
| 1436 |
+
.tasks-section {
|
| 1437 |
+
display: grid;
|
| 1438 |
+
grid-template-columns: 280px 1fr;
|
| 1439 |
+
gap: 20px;
|
| 1440 |
+
align-items: start;
|
| 1441 |
+
}
|
| 1442 |
+
|
| 1443 |
+
@media (max-width: 960px) {
|
| 1444 |
+
.tasks-section {
|
| 1445 |
+
grid-template-columns: 1fr;
|
| 1446 |
+
}
|
| 1447 |
+
}
|
| 1448 |
+
|
| 1449 |
+
.tasks-list {
|
| 1450 |
+
background: rgba(255, 255, 255, 0.92);
|
| 1451 |
+
border: 1px solid rgba(148, 163, 184, 0.26);
|
| 1452 |
+
border-radius: 18px;
|
| 1453 |
+
padding: 18px;
|
| 1454 |
+
display: flex;
|
| 1455 |
+
flex-direction: column;
|
| 1456 |
+
gap: 16px;
|
| 1457 |
+
box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
|
| 1458 |
+
}
|
| 1459 |
+
|
| 1460 |
+
.tasks-list h3 {
|
| 1461 |
+
margin: 0;
|
| 1462 |
+
font-size: 16px;
|
| 1463 |
+
font-weight: 600;
|
| 1464 |
+
color: #1f2937;
|
| 1465 |
+
}
|
| 1466 |
+
|
| 1467 |
+
.tasks-list ul {
|
| 1468 |
+
list-style: none;
|
| 1469 |
+
margin: 0;
|
| 1470 |
+
padding: 0;
|
| 1471 |
+
display: flex;
|
| 1472 |
+
flex-direction: column;
|
| 1473 |
+
gap: 12px;
|
| 1474 |
+
}
|
| 1475 |
+
|
| 1476 |
+
.task-item {
|
| 1477 |
+
border-radius: 14px;
|
| 1478 |
+
border: 1px solid transparent;
|
| 1479 |
+
transition: border-color 0.2s ease, background 0.2s ease;
|
| 1480 |
+
}
|
| 1481 |
+
|
| 1482 |
+
.task-item.completed {
|
| 1483 |
+
border-color: rgba(56, 189, 248, 0.35);
|
| 1484 |
+
background: rgba(191, 219, 254, 0.28);
|
| 1485 |
+
}
|
| 1486 |
+
|
| 1487 |
+
.task-item.active {
|
| 1488 |
+
border-color: rgba(129, 140, 248, 0.5);
|
| 1489 |
+
background: rgba(224, 231, 255, 0.5);
|
| 1490 |
+
}
|
| 1491 |
+
|
| 1492 |
+
.task-button {
|
| 1493 |
+
width: 100%;
|
| 1494 |
+
display: flex;
|
| 1495 |
+
align-items: center;
|
| 1496 |
+
justify-content: space-between;
|
| 1497 |
+
gap: 12px;
|
| 1498 |
+
padding: 12px 14px 6px;
|
| 1499 |
+
background: transparent;
|
| 1500 |
+
border: none;
|
| 1501 |
+
color: inherit;
|
| 1502 |
+
cursor: pointer;
|
| 1503 |
+
text-align: left;
|
| 1504 |
+
}
|
| 1505 |
+
|
| 1506 |
+
.task-title {
|
| 1507 |
+
font-weight: 600;
|
| 1508 |
+
font-size: 14px;
|
| 1509 |
+
color: #1e293b;
|
| 1510 |
+
}
|
| 1511 |
+
|
| 1512 |
+
.task-status {
|
| 1513 |
+
display: inline-flex;
|
| 1514 |
+
align-items: center;
|
| 1515 |
+
justify-content: center;
|
| 1516 |
+
padding: 4px 10px;
|
| 1517 |
+
border-radius: 999px;
|
| 1518 |
+
font-size: 12px;
|
| 1519 |
+
font-weight: 500;
|
| 1520 |
+
color: #1f2937;
|
| 1521 |
+
background: rgba(148, 163, 184, 0.2);
|
| 1522 |
+
}
|
| 1523 |
+
|
| 1524 |
+
.task-status.pending {
|
| 1525 |
+
background: rgba(148, 163, 184, 0.18);
|
| 1526 |
+
color: #475569;
|
| 1527 |
+
}
|
| 1528 |
+
|
| 1529 |
+
.task-status.in_progress {
|
| 1530 |
+
background: rgba(129, 140, 248, 0.24);
|
| 1531 |
+
color: #312e81;
|
| 1532 |
+
}
|
| 1533 |
+
|
| 1534 |
+
.task-status.completed {
|
| 1535 |
+
background: rgba(34, 197, 94, 0.2);
|
| 1536 |
+
color: #15803d;
|
| 1537 |
+
}
|
| 1538 |
+
|
| 1539 |
+
.task-status.skipped {
|
| 1540 |
+
background: rgba(248, 113, 113, 0.18);
|
| 1541 |
+
color: #b91c1c;
|
| 1542 |
+
}
|
| 1543 |
+
|
| 1544 |
+
.task-intent {
|
| 1545 |
+
margin: 0;
|
| 1546 |
+
padding: 0 14px 12px 14px;
|
| 1547 |
+
font-size: 13px;
|
| 1548 |
+
color: #64748b;
|
| 1549 |
+
}
|
| 1550 |
+
|
| 1551 |
+
.task-detail {
|
| 1552 |
+
background: rgba(255, 255, 255, 0.94);
|
| 1553 |
+
border: 1px solid rgba(148, 163, 184, 0.26);
|
| 1554 |
+
border-radius: 18px;
|
| 1555 |
+
padding: 22px;
|
| 1556 |
+
display: flex;
|
| 1557 |
+
flex-direction: column;
|
| 1558 |
+
gap: 18px;
|
| 1559 |
+
box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.5);
|
| 1560 |
+
}
|
| 1561 |
+
|
| 1562 |
+
.task-header {
|
| 1563 |
+
display: flex;
|
| 1564 |
+
justify-content: space-between;
|
| 1565 |
+
align-items: flex-start;
|
| 1566 |
+
flex-wrap: wrap;
|
| 1567 |
+
gap: 12px;
|
| 1568 |
+
}
|
| 1569 |
+
|
| 1570 |
+
.task-chip-group {
|
| 1571 |
+
display: flex;
|
| 1572 |
+
align-items: center;
|
| 1573 |
+
gap: 8px;
|
| 1574 |
+
flex-wrap: wrap;
|
| 1575 |
+
}
|
| 1576 |
+
|
| 1577 |
+
.task-header h3 {
|
| 1578 |
+
margin: 0;
|
| 1579 |
+
font-size: 18px;
|
| 1580 |
+
font-weight: 600;
|
| 1581 |
+
color: #1f2937;
|
| 1582 |
+
}
|
| 1583 |
+
|
| 1584 |
+
.task-header .muted {
|
| 1585 |
+
margin: 6px 0 0;
|
| 1586 |
+
}
|
| 1587 |
+
|
| 1588 |
+
.task-label {
|
| 1589 |
+
padding: 6px 12px;
|
| 1590 |
+
border-radius: 999px;
|
| 1591 |
+
background: rgba(191, 219, 254, 0.32);
|
| 1592 |
+
border: 1px solid rgba(59, 130, 246, 0.35);
|
| 1593 |
+
font-size: 12px;
|
| 1594 |
+
color: #1e3a8a;
|
| 1595 |
+
}
|
| 1596 |
+
|
| 1597 |
+
.task-label.note-chip {
|
| 1598 |
+
background: rgba(34, 197, 94, 0.2);
|
| 1599 |
+
border-color: rgba(34, 197, 94, 0.35);
|
| 1600 |
+
color: #15803d;
|
| 1601 |
+
}
|
| 1602 |
+
|
| 1603 |
+
.task-label.path-chip {
|
| 1604 |
+
display: inline-flex;
|
| 1605 |
+
align-items: center;
|
| 1606 |
+
gap: 6px;
|
| 1607 |
+
max-width: 360px;
|
| 1608 |
+
background: rgba(56, 189, 248, 0.2);
|
| 1609 |
+
border-color: rgba(56, 189, 248, 0.35);
|
| 1610 |
+
color: #0369a1;
|
| 1611 |
+
overflow: hidden;
|
| 1612 |
+
text-overflow: ellipsis;
|
| 1613 |
+
white-space: nowrap;
|
| 1614 |
+
}
|
| 1615 |
+
|
| 1616 |
+
.path-label {
|
| 1617 |
+
font-weight: 500;
|
| 1618 |
+
}
|
| 1619 |
+
|
| 1620 |
+
.path-text {
|
| 1621 |
+
max-width: 220px;
|
| 1622 |
+
overflow: hidden;
|
| 1623 |
+
text-overflow: ellipsis;
|
| 1624 |
+
white-space: nowrap;
|
| 1625 |
+
}
|
| 1626 |
+
|
| 1627 |
+
.chip-action {
|
| 1628 |
+
border: none;
|
| 1629 |
+
background: rgba(56, 189, 248, 0.2);
|
| 1630 |
+
color: #0369a1;
|
| 1631 |
+
padding: 3px 8px;
|
| 1632 |
+
border-radius: 10px;
|
| 1633 |
+
font-size: 11px;
|
| 1634 |
+
cursor: pointer;
|
| 1635 |
+
transition: background 0.2s ease, color 0.2s ease;
|
| 1636 |
+
}
|
| 1637 |
+
|
| 1638 |
+
.chip-action:hover {
|
| 1639 |
+
background: rgba(14, 165, 233, 0.28);
|
| 1640 |
+
color: #0f172a;
|
| 1641 |
+
}
|
| 1642 |
+
|
| 1643 |
+
.task-notices {
|
| 1644 |
+
background: rgba(191, 219, 254, 0.28);
|
| 1645 |
+
border: 1px solid rgba(96, 165, 250, 0.35);
|
| 1646 |
+
border-radius: 16px;
|
| 1647 |
+
padding: 14px 18px;
|
| 1648 |
+
color: #1f2937;
|
| 1649 |
+
}
|
| 1650 |
+
|
| 1651 |
+
.task-notices h4 {
|
| 1652 |
+
margin: 0 0 8px;
|
| 1653 |
+
font-size: 14px;
|
| 1654 |
+
font-weight: 600;
|
| 1655 |
+
}
|
| 1656 |
+
|
| 1657 |
+
.task-notices ul {
|
| 1658 |
+
list-style: disc;
|
| 1659 |
+
margin: 0 0 0 18px;
|
| 1660 |
+
padding: 0;
|
| 1661 |
+
display: flex;
|
| 1662 |
+
flex-direction: column;
|
| 1663 |
+
gap: 6px;
|
| 1664 |
+
}
|
| 1665 |
+
|
| 1666 |
+
.task-notices li {
|
| 1667 |
+
font-size: 13px;
|
| 1668 |
+
}
|
| 1669 |
+
|
| 1670 |
+
.report-block {
|
| 1671 |
+
background: rgba(255, 255, 255, 0.94);
|
| 1672 |
+
border: 1px solid rgba(148, 163, 184, 0.26);
|
| 1673 |
+
border-radius: 18px;
|
| 1674 |
+
padding: 22px;
|
| 1675 |
+
display: flex;
|
| 1676 |
+
flex-direction: column;
|
| 1677 |
+
gap: 12px;
|
| 1678 |
+
}
|
| 1679 |
+
|
| 1680 |
+
.report-block h3 {
|
| 1681 |
+
margin: 0;
|
| 1682 |
+
font-size: 18px;
|
| 1683 |
+
font-weight: 600;
|
| 1684 |
+
color: #1f2937;
|
| 1685 |
+
}
|
| 1686 |
+
|
| 1687 |
+
.block-pre {
|
| 1688 |
+
font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular,
|
| 1689 |
+
Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 1690 |
+
font-size: 13px;
|
| 1691 |
+
line-height: 1.7;
|
| 1692 |
+
white-space: pre-wrap;
|
| 1693 |
+
word-break: break-word;
|
| 1694 |
+
color: #1f2937;
|
| 1695 |
+
background: rgba(248, 250, 252, 0.9);
|
| 1696 |
+
padding: 16px;
|
| 1697 |
+
border-radius: 14px;
|
| 1698 |
+
border: 1px solid rgba(148, 163, 184, 0.35);
|
| 1699 |
+
overflow: auto;
|
| 1700 |
+
max-height: 420px;
|
| 1701 |
+
scrollbar-width: thin;
|
| 1702 |
+
scrollbar-color: rgba(129, 140, 248, 0.6) rgba(226, 232, 240, 0.7);
|
| 1703 |
+
}
|
| 1704 |
+
|
| 1705 |
+
.block-pre::-webkit-scrollbar {
|
| 1706 |
+
width: 6px;
|
| 1707 |
+
}
|
| 1708 |
+
|
| 1709 |
+
.block-pre::-webkit-scrollbar-track {
|
| 1710 |
+
background: rgba(226, 232, 240, 0.7);
|
| 1711 |
+
border-radius: 999px;
|
| 1712 |
+
}
|
| 1713 |
+
|
| 1714 |
+
.block-pre::-webkit-scrollbar-thumb {
|
| 1715 |
+
background: linear-gradient(180deg, rgba(99, 102, 241, 0.75), rgba(59, 130, 246, 0.65));
|
| 1716 |
+
border-radius: 999px;
|
| 1717 |
+
}
|
| 1718 |
+
|
| 1719 |
+
.block-pre::-webkit-scrollbar-thumb:hover {
|
| 1720 |
+
background: linear-gradient(180deg, rgba(79, 70, 229, 0.8), rgba(37, 99, 235, 0.75));
|
| 1721 |
+
}
|
| 1722 |
+
|
| 1723 |
+
.summary-block .block-pre,
|
| 1724 |
+
.sources-block .block-pre {
|
| 1725 |
+
max-height: 360px;
|
| 1726 |
+
}
|
| 1727 |
+
|
| 1728 |
+
|
| 1729 |
+
.tools-block {
|
| 1730 |
+
position: relative;
|
| 1731 |
+
margin-top: 16px;
|
| 1732 |
+
padding: 20px;
|
| 1733 |
+
border-radius: 18px;
|
| 1734 |
+
background: rgba(255, 255, 255, 0.94);
|
| 1735 |
+
border: 1px solid rgba(148, 163, 184, 0.18);
|
| 1736 |
+
box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
|
| 1737 |
+
display: flex;
|
| 1738 |
+
flex-direction: column;
|
| 1739 |
+
gap: 12px;
|
| 1740 |
+
}
|
| 1741 |
+
|
| 1742 |
+
.tools-block h3 {
|
| 1743 |
+
margin: 0;
|
| 1744 |
+
font-size: 16px;
|
| 1745 |
+
font-weight: 600;
|
| 1746 |
+
color: #1f2937;
|
| 1747 |
+
letter-spacing: 0.02em;
|
| 1748 |
+
}
|
| 1749 |
+
|
| 1750 |
+
.tool-list {
|
| 1751 |
+
list-style: none;
|
| 1752 |
+
margin: 0;
|
| 1753 |
+
padding: 0;
|
| 1754 |
+
display: flex;
|
| 1755 |
+
flex-direction: column;
|
| 1756 |
+
gap: 12px;
|
| 1757 |
+
}
|
| 1758 |
+
|
| 1759 |
+
.tool-entry {
|
| 1760 |
+
background: rgba(248, 250, 252, 0.95);
|
| 1761 |
+
border: 1px solid rgba(148, 163, 184, 0.24);
|
| 1762 |
+
border-radius: 14px;
|
| 1763 |
+
padding: 14px;
|
| 1764 |
+
display: flex;
|
| 1765 |
+
flex-direction: column;
|
| 1766 |
+
gap: 10px;
|
| 1767 |
+
}
|
| 1768 |
+
|
| 1769 |
+
.tool-entry-header {
|
| 1770 |
+
display: flex;
|
| 1771 |
+
flex-wrap: wrap;
|
| 1772 |
+
gap: 8px;
|
| 1773 |
+
align-items: center;
|
| 1774 |
+
justify-content: space-between;
|
| 1775 |
+
}
|
| 1776 |
+
|
| 1777 |
+
.tool-entry-title {
|
| 1778 |
+
font-weight: 600;
|
| 1779 |
+
color: #1f2937;
|
| 1780 |
+
}
|
| 1781 |
+
|
| 1782 |
+
.tool-entry-note {
|
| 1783 |
+
font-size: 12px;
|
| 1784 |
+
color: #0f766e;
|
| 1785 |
+
}
|
| 1786 |
+
|
| 1787 |
+
.tool-entry-path {
|
| 1788 |
+
margin: 0;
|
| 1789 |
+
font-size: 12px;
|
| 1790 |
+
display: flex;
|
| 1791 |
+
align-items: center;
|
| 1792 |
+
gap: 6px;
|
| 1793 |
+
color: #2563eb;
|
| 1794 |
+
}
|
| 1795 |
+
|
| 1796 |
+
.tool-subtitle {
|
| 1797 |
+
margin: 0;
|
| 1798 |
+
font-size: 13px;
|
| 1799 |
+
color: #475569;
|
| 1800 |
+
font-weight: 500;
|
| 1801 |
+
}
|
| 1802 |
+
|
| 1803 |
+
.tool-pre {
|
| 1804 |
+
font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular,
|
| 1805 |
+
Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 1806 |
+
font-size: 12px;
|
| 1807 |
+
line-height: 1.6;
|
| 1808 |
+
white-space: pre-wrap;
|
| 1809 |
+
word-break: break-word;
|
| 1810 |
+
color: #1f2937;
|
| 1811 |
+
background: rgba(248, 250, 252, 0.9);
|
| 1812 |
+
padding: 12px;
|
| 1813 |
+
border-radius: 12px;
|
| 1814 |
+
border: 1px solid rgba(148, 163, 184, 0.28);
|
| 1815 |
+
overflow: auto;
|
| 1816 |
+
max-height: 260px;
|
| 1817 |
+
scrollbar-width: thin;
|
| 1818 |
+
scrollbar-color: rgba(129, 140, 248, 0.6) rgba(226, 232, 240, 0.7);
|
| 1819 |
+
}
|
| 1820 |
+
|
| 1821 |
+
.tool-pre::-webkit-scrollbar {
|
| 1822 |
+
width: 6px;
|
| 1823 |
+
}
|
| 1824 |
+
|
| 1825 |
+
.tool-pre::-webkit-scrollbar-track {
|
| 1826 |
+
background: rgba(226, 232, 240, 0.7);
|
| 1827 |
+
}
|
| 1828 |
+
|
| 1829 |
+
.tool-pre::-webkit-scrollbar-thumb {
|
| 1830 |
+
background: rgba(99, 102, 241, 0.7);
|
| 1831 |
+
border-radius: 10px;
|
| 1832 |
+
}
|
| 1833 |
+
|
| 1834 |
+
.link-btn {
|
| 1835 |
+
background: none;
|
| 1836 |
+
border: none;
|
| 1837 |
+
color: #0369a1;
|
| 1838 |
+
cursor: pointer;
|
| 1839 |
+
padding: 0 4px;
|
| 1840 |
+
font-size: 12px;
|
| 1841 |
+
border-radius: 8px;
|
| 1842 |
+
transition: color 0.2s ease, background 0.2s ease;
|
| 1843 |
+
}
|
| 1844 |
+
|
| 1845 |
+
.link-btn:hover {
|
| 1846 |
+
color: #0ea5e9;
|
| 1847 |
+
background: rgba(14, 165, 233, 0.16);
|
| 1848 |
+
}
|
| 1849 |
+
|
| 1850 |
+
|
| 1851 |
+
.sources-block,
|
| 1852 |
+
.summary-block {
|
| 1853 |
+
position: relative;
|
| 1854 |
+
margin-top: 16px;
|
| 1855 |
+
padding: 18px;
|
| 1856 |
+
border-radius: 18px;
|
| 1857 |
+
background: rgba(255, 255, 255, 0.94);
|
| 1858 |
+
border: 1px solid rgba(148, 163, 184, 0.18);
|
| 1859 |
+
box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
|
| 1860 |
+
}
|
| 1861 |
+
|
| 1862 |
+
.sources-history {
|
| 1863 |
+
margin-top: 16px;
|
| 1864 |
+
display: flex;
|
| 1865 |
+
flex-direction: column;
|
| 1866 |
+
gap: 10px;
|
| 1867 |
+
}
|
| 1868 |
+
|
| 1869 |
+
.sources-history h4 {
|
| 1870 |
+
margin: 0;
|
| 1871 |
+
color: #1f2937;
|
| 1872 |
+
font-size: 14px;
|
| 1873 |
+
letter-spacing: 0.01em;
|
| 1874 |
+
}
|
| 1875 |
+
|
| 1876 |
+
.history-list {
|
| 1877 |
+
display: flex;
|
| 1878 |
+
flex-direction: column;
|
| 1879 |
+
gap: 10px;
|
| 1880 |
+
}
|
| 1881 |
+
|
| 1882 |
+
.history-list details {
|
| 1883 |
+
background: rgba(248, 250, 252, 0.95);
|
| 1884 |
+
border: 1px solid rgba(148, 163, 184, 0.24);
|
| 1885 |
+
border-radius: 14px;
|
| 1886 |
+
padding: 12px 16px;
|
| 1887 |
+
color: #1f2937;
|
| 1888 |
+
transition: border-color 0.2s ease, background 0.2s ease;
|
| 1889 |
+
}
|
| 1890 |
+
|
| 1891 |
+
.history-list details[open] {
|
| 1892 |
+
background: rgba(224, 231, 255, 0.55);
|
| 1893 |
+
border-color: rgba(129, 140, 248, 0.4);
|
| 1894 |
+
}
|
| 1895 |
+
|
| 1896 |
+
.history-list summary {
|
| 1897 |
+
cursor: pointer;
|
| 1898 |
+
font-weight: 600;
|
| 1899 |
+
outline: none;
|
| 1900 |
+
list-style: none;
|
| 1901 |
+
display: flex;
|
| 1902 |
+
align-items: center;
|
| 1903 |
+
justify-content: space-between;
|
| 1904 |
+
}
|
| 1905 |
+
|
| 1906 |
+
.history-list summary::-webkit-details-marker {
|
| 1907 |
+
display: none;
|
| 1908 |
+
}
|
| 1909 |
+
|
| 1910 |
+
.history-list summary::after {
|
| 1911 |
+
content: "▾";
|
| 1912 |
+
margin-left: 6px;
|
| 1913 |
+
font-size: 12px;
|
| 1914 |
+
opacity: 0.7;
|
| 1915 |
+
transition: transform 0.2s ease;
|
| 1916 |
+
}
|
| 1917 |
+
|
| 1918 |
+
.history-list details[open] summary::after {
|
| 1919 |
+
transform: rotate(180deg);
|
| 1920 |
+
}
|
| 1921 |
+
|
| 1922 |
+
.block-highlight {
|
| 1923 |
+
animation: glow 1.2s ease;
|
| 1924 |
+
}
|
| 1925 |
+
|
| 1926 |
+
.sources-block h3,
|
| 1927 |
+
.summary-block h3 {
|
| 1928 |
+
margin: 0 0 14px;
|
| 1929 |
+
color: #1f2937;
|
| 1930 |
+
letter-spacing: 0.02em;
|
| 1931 |
+
}
|
| 1932 |
+
|
| 1933 |
+
.sources-list {
|
| 1934 |
+
list-style: none;
|
| 1935 |
+
margin: 0;
|
| 1936 |
+
padding: 0;
|
| 1937 |
+
display: flex;
|
| 1938 |
+
flex-direction: column;
|
| 1939 |
+
gap: 10px;
|
| 1940 |
+
}
|
| 1941 |
+
|
| 1942 |
+
.source-item {
|
| 1943 |
+
position: relative;
|
| 1944 |
+
display: inline-flex;
|
| 1945 |
+
flex-direction: column;
|
| 1946 |
+
gap: 6px;
|
| 1947 |
+
}
|
| 1948 |
+
|
| 1949 |
+
.source-link {
|
| 1950 |
+
color: #2563eb;
|
| 1951 |
+
text-decoration: none;
|
| 1952 |
+
font-weight: 600;
|
| 1953 |
+
letter-spacing: 0.01em;
|
| 1954 |
+
transition: color 0.2s ease;
|
| 1955 |
+
}
|
| 1956 |
+
|
| 1957 |
+
.source-link::after {
|
| 1958 |
+
content: " ↗";
|
| 1959 |
+
font-size: 12px;
|
| 1960 |
+
opacity: 0.6;
|
| 1961 |
+
}
|
| 1962 |
+
|
| 1963 |
+
.source-link:hover {
|
| 1964 |
+
color: #0f172a;
|
| 1965 |
+
}
|
| 1966 |
+
|
| 1967 |
+
.source-tooltip {
|
| 1968 |
+
display: none;
|
| 1969 |
+
position: absolute;
|
| 1970 |
+
bottom: calc(100% + 12px);
|
| 1971 |
+
left: 50%;
|
| 1972 |
+
transform: translateX(-50%);
|
| 1973 |
+
background: rgba(255, 255, 255, 0.98);
|
| 1974 |
+
color: #1f2937;
|
| 1975 |
+
padding: 14px 16px;
|
| 1976 |
+
border-radius: 16px;
|
| 1977 |
+
box-shadow: 0 18px 32px rgba(15, 23, 42, 0.18);
|
| 1978 |
+
width: min(420px, 90vw);
|
| 1979 |
+
z-index: 20;
|
| 1980 |
+
border: 1px solid rgba(148, 163, 184, 0.24);
|
| 1981 |
+
}
|
| 1982 |
+
|
| 1983 |
+
.source-tooltip::after {
|
| 1984 |
+
content: "";
|
| 1985 |
+
position: absolute;
|
| 1986 |
+
top: 100%;
|
| 1987 |
+
left: 50%;
|
| 1988 |
+
transform: translateX(-50%);
|
| 1989 |
+
border-width: 10px;
|
| 1990 |
+
border-style: solid;
|
| 1991 |
+
border-color: rgba(255, 255, 255, 0.98) transparent transparent transparent;
|
| 1992 |
+
}
|
| 1993 |
+
|
| 1994 |
+
.source-tooltip::before {
|
| 1995 |
+
content: "";
|
| 1996 |
+
position: absolute;
|
| 1997 |
+
bottom: -12px;
|
| 1998 |
+
left: 50%;
|
| 1999 |
+
transform: translateX(-50%);
|
| 2000 |
+
border-width: 12px 10px 0 10px;
|
| 2001 |
+
border-style: solid;
|
| 2002 |
+
border-color: rgba(255, 255, 255, 0.98) transparent transparent transparent;
|
| 2003 |
+
filter: drop-shadow(0 -2px 4px rgba(15, 23, 42, 0.12));
|
| 2004 |
+
}
|
| 2005 |
+
|
| 2006 |
+
.source-tooltip p {
|
| 2007 |
+
margin: 0 0 8px;
|
| 2008 |
+
font-size: 13px;
|
| 2009 |
+
line-height: 1.6;
|
| 2010 |
+
}
|
| 2011 |
+
|
| 2012 |
+
.source-tooltip p:last-child {
|
| 2013 |
+
margin-bottom: 0;
|
| 2014 |
+
}
|
| 2015 |
+
|
| 2016 |
+
.muted-text {
|
| 2017 |
+
color: #64748b;
|
| 2018 |
+
}
|
| 2019 |
+
|
| 2020 |
+
.source-item:hover .source-tooltip,
|
| 2021 |
+
.source-item:focus-within .source-tooltip {
|
| 2022 |
+
display: block;
|
| 2023 |
+
}
|
| 2024 |
+
|
| 2025 |
+
.hint.muted {
|
| 2026 |
+
color: #64748b;
|
| 2027 |
+
}
|
| 2028 |
+
|
| 2029 |
+
@keyframes float {
|
| 2030 |
+
0% {
|
| 2031 |
+
transform: translate3d(0, 0, 0) rotate(0deg);
|
| 2032 |
+
}
|
| 2033 |
+
50% {
|
| 2034 |
+
transform: translate3d(10%, 6%, 0) rotate(3deg);
|
| 2035 |
+
}
|
| 2036 |
+
100% {
|
| 2037 |
+
transform: translate3d(0, 0, 0) rotate(0deg);
|
| 2038 |
+
}
|
| 2039 |
+
}
|
| 2040 |
+
|
| 2041 |
+
@keyframes spin {
|
| 2042 |
+
to {
|
| 2043 |
+
transform: rotate(360deg);
|
| 2044 |
+
}
|
| 2045 |
+
}
|
| 2046 |
+
|
| 2047 |
+
@keyframes pulse {
|
| 2048 |
+
0%,
|
| 2049 |
+
100% {
|
| 2050 |
+
transform: scale(1);
|
| 2051 |
+
opacity: 1;
|
| 2052 |
+
}
|
| 2053 |
+
50% {
|
| 2054 |
+
transform: scale(1.3);
|
| 2055 |
+
opacity: 0.5;
|
| 2056 |
+
}
|
| 2057 |
+
}
|
| 2058 |
+
|
| 2059 |
+
@keyframes glow {
|
| 2060 |
+
0% {
|
| 2061 |
+
box-shadow: 0 0 0 rgba(59, 130, 246, 0.3);
|
| 2062 |
+
border-color: rgba(59, 130, 246, 0.5);
|
| 2063 |
+
}
|
| 2064 |
+
100% {
|
| 2065 |
+
box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.12);
|
| 2066 |
+
border-color: rgba(148, 163, 184, 0.2);
|
| 2067 |
+
}
|
| 2068 |
+
}
|
| 2069 |
+
|
| 2070 |
+
@media (max-width: 960px) {
|
| 2071 |
+
.app-shell {
|
| 2072 |
+
padding: 56px 16px;
|
| 2073 |
+
}
|
| 2074 |
+
|
| 2075 |
+
.layout {
|
| 2076 |
+
flex-direction: column;
|
| 2077 |
+
align-items: stretch;
|
| 2078 |
+
}
|
| 2079 |
+
|
| 2080 |
+
.panel {
|
| 2081 |
+
padding: 22px;
|
| 2082 |
+
}
|
| 2083 |
+
|
| 2084 |
+
.panel-form,
|
| 2085 |
+
.panel-result {
|
| 2086 |
+
max-width: none;
|
| 2087 |
+
}
|
| 2088 |
+
|
| 2089 |
+
.status-bar {
|
| 2090 |
+
flex-direction: column;
|
| 2091 |
+
align-items: flex-start;
|
| 2092 |
+
}
|
| 2093 |
+
|
| 2094 |
+
.status-main,
|
| 2095 |
+
.status-controls {
|
| 2096 |
+
width: 100%;
|
| 2097 |
+
}
|
| 2098 |
+
|
| 2099 |
+
.status-controls {
|
| 2100 |
+
justify-content: flex-start;
|
| 2101 |
+
}
|
| 2102 |
+
}
|
| 2103 |
+
|
| 2104 |
+
@media (max-width: 600px) {
|
| 2105 |
+
.options {
|
| 2106 |
+
flex-direction: column;
|
| 2107 |
+
}
|
| 2108 |
+
|
| 2109 |
+
.status-meta {
|
| 2110 |
+
font-size: 12px;
|
| 2111 |
+
}
|
| 2112 |
+
|
| 2113 |
+
.panel-head {
|
| 2114 |
+
flex-direction: column;
|
| 2115 |
+
align-items: flex-start;
|
| 2116 |
+
}
|
| 2117 |
+
|
| 2118 |
+
.panel-form h1 {
|
| 2119 |
+
font-size: 24px;
|
| 2120 |
+
}
|
| 2121 |
+
}
|
| 2122 |
+
|
| 2123 |
+
/* Sidebar styles */
|
| 2124 |
+
.sidebar {
|
| 2125 |
+
width: 400px;
|
| 2126 |
+
min-width: 400px;
|
| 2127 |
+
height: 100vh;
|
| 2128 |
+
background: rgba(255, 255, 255, 0.98);
|
| 2129 |
+
border-right: 1px solid rgba(148, 163, 184, 0.2);
|
| 2130 |
+
padding: 32px 24px;
|
| 2131 |
+
display: flex;
|
| 2132 |
+
flex-direction: column;
|
| 2133 |
+
gap: 24px;
|
| 2134 |
+
overflow-y: auto;
|
| 2135 |
+
box-shadow: 4px 0 24px rgba(15, 23, 42, 0.08);
|
| 2136 |
+
}
|
| 2137 |
+
|
| 2138 |
+
.sidebar-header {
|
| 2139 |
+
display: flex;
|
| 2140 |
+
flex-direction: column;
|
| 2141 |
+
gap: 16px;
|
| 2142 |
+
}
|
| 2143 |
+
|
| 2144 |
+
.sidebar-header h2 {
|
| 2145 |
+
font-size: 24px;
|
| 2146 |
+
font-weight: 700;
|
| 2147 |
+
margin: 0;
|
| 2148 |
+
color: #1f2937;
|
| 2149 |
+
}
|
| 2150 |
+
|
| 2151 |
+
.back-btn {
|
| 2152 |
+
display: flex;
|
| 2153 |
+
align-items: center;
|
| 2154 |
+
gap: 8px;
|
| 2155 |
+
padding: 10px 16px;
|
| 2156 |
+
background: transparent;
|
| 2157 |
+
border: 1px solid rgba(148, 163, 184, 0.3);
|
| 2158 |
+
border-radius: 12px;
|
| 2159 |
+
color: #64748b;
|
| 2160 |
+
font-size: 14px;
|
| 2161 |
+
font-weight: 500;
|
| 2162 |
+
cursor: pointer;
|
| 2163 |
+
transition: all 0.2s ease;
|
| 2164 |
+
width: fit-content;
|
| 2165 |
+
}
|
| 2166 |
+
|
| 2167 |
+
.back-btn:hover:not(:disabled) {
|
| 2168 |
+
background: rgba(59, 130, 246, 0.1);
|
| 2169 |
+
border-color: #3b82f6;
|
| 2170 |
+
color: #3b82f6;
|
| 2171 |
+
}
|
| 2172 |
+
|
| 2173 |
+
.back-btn:disabled {
|
| 2174 |
+
opacity: 0.5;
|
| 2175 |
+
cursor: not-allowed;
|
| 2176 |
+
}
|
| 2177 |
+
|
| 2178 |
+
.research-info {
|
| 2179 |
+
flex: 1;
|
| 2180 |
+
display: flex;
|
| 2181 |
+
flex-direction: column;
|
| 2182 |
+
gap: 20px;
|
| 2183 |
+
}
|
| 2184 |
+
|
| 2185 |
+
.info-item {
|
| 2186 |
+
display: flex;
|
| 2187 |
+
flex-direction: column;
|
| 2188 |
+
gap: 8px;
|
| 2189 |
+
}
|
| 2190 |
+
|
| 2191 |
+
.info-item label {
|
| 2192 |
+
font-size: 12px;
|
| 2193 |
+
font-weight: 600;
|
| 2194 |
+
text-transform: uppercase;
|
| 2195 |
+
letter-spacing: 0.5px;
|
| 2196 |
+
color: #64748b;
|
| 2197 |
+
}
|
| 2198 |
+
|
| 2199 |
+
.info-item p {
|
| 2200 |
+
margin: 0;
|
| 2201 |
+
font-size: 14px;
|
| 2202 |
+
color: #1f2937;
|
| 2203 |
+
line-height: 1.6;
|
| 2204 |
+
}
|
| 2205 |
+
|
| 2206 |
+
.topic-display {
|
| 2207 |
+
font-size: 16px !important;
|
| 2208 |
+
font-weight: 600;
|
| 2209 |
+
color: #0f172a !important;
|
| 2210 |
+
padding: 12px;
|
| 2211 |
+
background: rgba(59, 130, 246, 0.05);
|
| 2212 |
+
border-radius: 8px;
|
| 2213 |
+
border-left: 3px solid #3b82f6;
|
| 2214 |
+
}
|
| 2215 |
+
|
| 2216 |
+
.progress-bar {
|
| 2217 |
+
width: 100%;
|
| 2218 |
+
height: 8px;
|
| 2219 |
+
background: rgba(148, 163, 184, 0.2);
|
| 2220 |
+
border-radius: 4px;
|
| 2221 |
+
overflow: hidden;
|
| 2222 |
+
}
|
| 2223 |
+
|
| 2224 |
+
.progress-fill {
|
| 2225 |
+
height: 100%;
|
| 2226 |
+
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
| 2227 |
+
border-radius: 4px;
|
| 2228 |
+
transition: width 0.5s ease;
|
| 2229 |
+
}
|
| 2230 |
+
|
| 2231 |
+
.progress-text {
|
| 2232 |
+
font-size: 13px !important;
|
| 2233 |
+
color: #64748b !important;
|
| 2234 |
+
font-weight: 500;
|
| 2235 |
+
}
|
| 2236 |
+
|
| 2237 |
+
.sidebar-actions {
|
| 2238 |
+
display: flex;
|
| 2239 |
+
flex-direction: column;
|
| 2240 |
+
gap: 12px;
|
| 2241 |
+
padding-top: 16px;
|
| 2242 |
+
border-top: 1px solid rgba(148, 163, 184, 0.2);
|
| 2243 |
+
}
|
| 2244 |
+
|
| 2245 |
+
.new-research-btn {
|
| 2246 |
+
display: flex;
|
| 2247 |
+
align-items: center;
|
| 2248 |
+
justify-content: center;
|
| 2249 |
+
gap: 8px;
|
| 2250 |
+
padding: 14px 20px;
|
| 2251 |
+
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
| 2252 |
+
border: none;
|
| 2253 |
+
border-radius: 12px;
|
| 2254 |
+
color: white;
|
| 2255 |
+
font-size: 15px;
|
| 2256 |
+
font-weight: 600;
|
| 2257 |
+
cursor: pointer;
|
| 2258 |
+
transition: all 0.3s ease;
|
| 2259 |
+
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
| 2260 |
+
}
|
| 2261 |
+
|
| 2262 |
+
.new-research-btn:hover {
|
| 2263 |
+
transform: translateY(-2px);
|
| 2264 |
+
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
|
| 2265 |
+
}
|
| 2266 |
+
|
| 2267 |
+
.new-research-btn:active {
|
| 2268 |
+
transform: translateY(0);
|
| 2269 |
+
}
|
| 2270 |
+
|
| 2271 |
+
/* Fullscreen result panel styles */
|
| 2272 |
+
.layout-fullscreen .panel-result {
|
| 2273 |
+
flex: 1;
|
| 2274 |
+
height: 100vh;
|
| 2275 |
+
border-radius: 0;
|
| 2276 |
+
border: none;
|
| 2277 |
+
overflow-y: auto;
|
| 2278 |
+
max-width: none;
|
| 2279 |
+
}
|
| 2280 |
+
|
| 2281 |
+
@media (max-width: 1024px) {
|
| 2282 |
+
.sidebar {
|
| 2283 |
+
width: 320px;
|
| 2284 |
+
min-width: 320px;
|
| 2285 |
+
}
|
| 2286 |
+
}
|
| 2287 |
+
|
| 2288 |
+
@media (max-width: 768px) {
|
| 2289 |
+
.layout-fullscreen {
|
| 2290 |
+
flex-direction: column;
|
| 2291 |
+
}
|
| 2292 |
+
|
| 2293 |
+
.sidebar {
|
| 2294 |
+
width: 100%;
|
| 2295 |
+
min-width: 100%;
|
| 2296 |
+
height: auto;
|
| 2297 |
+
max-height: 40vh;
|
| 2298 |
+
}
|
| 2299 |
+
|
| 2300 |
+
.layout-fullscreen .panel-result {
|
| 2301 |
+
height: 60vh;
|
| 2302 |
+
}
|
| 2303 |
+
}
|
| 2304 |
+
</style>
|
frontend/src/env.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface ImportMetaEnv {
|
| 2 |
+
readonly VITE_API_BASE_URL?: string;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
interface ImportMeta {
|
| 6 |
+
readonly env: ImportMetaEnv;
|
| 7 |
+
}
|
frontend/src/main.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createApp } from "vue";
|
| 2 |
+
import App from "./App.vue";
|
| 3 |
+
|
| 4 |
+
import "./style.css";
|
| 5 |
+
|
| 6 |
+
createApp(App).mount("#app");
|
frontend/src/services/api.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const baseURL =
|
| 2 |
+
import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
|
| 3 |
+
|
| 4 |
+
export interface ResearchRequest {
|
| 5 |
+
topic: string;
|
| 6 |
+
search_api?: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export interface ResearchStreamEvent {
|
| 10 |
+
type: string;
|
| 11 |
+
[key: string]: unknown;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export interface StreamOptions {
|
| 15 |
+
signal?: AbortSignal;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export async function runResearchStream(
|
| 19 |
+
payload: ResearchRequest,
|
| 20 |
+
onEvent: (event: ResearchStreamEvent) => void,
|
| 21 |
+
options: StreamOptions = {}
|
| 22 |
+
): Promise<void> {
|
| 23 |
+
const response = await fetch(`${baseURL}/research/stream`, {
|
| 24 |
+
method: "POST",
|
| 25 |
+
headers: {
|
| 26 |
+
"Content-Type": "application/json",
|
| 27 |
+
Accept: "text/event-stream"
|
| 28 |
+
},
|
| 29 |
+
body: JSON.stringify(payload),
|
| 30 |
+
signal: options.signal
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
if (!response.ok) {
|
| 34 |
+
const errorText = await response.text().catch(() => "");
|
| 35 |
+
throw new Error(
|
| 36 |
+
errorText || `Research request failed, status code: ${response.status}`
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
const body = response.body;
|
| 41 |
+
if (!body) {
|
| 42 |
+
throw new Error("Browser does not support streaming responses, unable to get research progress");
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
const reader = body.getReader();
|
| 46 |
+
const decoder = new TextDecoder("utf-8");
|
| 47 |
+
let buffer = "";
|
| 48 |
+
|
| 49 |
+
while (true) {
|
| 50 |
+
const { value, done } = await reader.read();
|
| 51 |
+
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
| 52 |
+
|
| 53 |
+
let boundary = buffer.indexOf("\n\n");
|
| 54 |
+
while (boundary !== -1) {
|
| 55 |
+
const rawEvent = buffer.slice(0, boundary).trim();
|
| 56 |
+
buffer = buffer.slice(boundary + 2);
|
| 57 |
+
|
| 58 |
+
if (rawEvent.startsWith("data:")) {
|
| 59 |
+
const dataPayload = rawEvent.slice(5).trim();
|
| 60 |
+
if (dataPayload) {
|
| 61 |
+
try {
|
| 62 |
+
const event = JSON.parse(dataPayload) as ResearchStreamEvent;
|
| 63 |
+
onEvent(event);
|
| 64 |
+
|
| 65 |
+
if (event.type === "error" || event.type === "done") {
|
| 66 |
+
return;
|
| 67 |
+
}
|
| 68 |
+
} catch (error) {
|
| 69 |
+
console.error("Failed to parse streaming event:", error, dataPayload);
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
boundary = buffer.indexOf("\n\n");
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
if (done) {
|
| 78 |
+
// Handle possible trailing events
|
| 79 |
+
if (buffer.trim()) {
|
| 80 |
+
const rawEvent = buffer.trim();
|
| 81 |
+
if (rawEvent.startsWith("data:")) {
|
| 82 |
+
const dataPayload = rawEvent.slice(5).trim();
|
| 83 |
+
if (dataPayload) {
|
| 84 |
+
try {
|
| 85 |
+
const event = JSON.parse(dataPayload) as ResearchStreamEvent;
|
| 86 |
+
onEvent(event);
|
| 87 |
+
} catch (error) {
|
| 88 |
+
console.error("Failed to parse streaming event:", error, dataPayload);
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
break;
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
}
|
frontend/src/style.css
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
color: #1a1a1a;
|
| 3 |
+
background-color: #f6f6f6;
|
| 4 |
+
font-family: "Helvetica Neue", Arial, sans-serif;
|
| 5 |
+
line-height: 1.5;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
* {
|
| 9 |
+
box-sizing: border-box;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
body {
|
| 13 |
+
margin: 0;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
#app {
|
| 17 |
+
min-height: 100vh;
|
| 18 |
+
}
|
frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ESNext",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"module": "ESNext",
|
| 6 |
+
"moduleResolution": "Node",
|
| 7 |
+
"strict": true,
|
| 8 |
+
"jsx": "preserve",
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"resolveJsonModule": true,
|
| 11 |
+
"isolatedModules": true,
|
| 12 |
+
"lib": ["ESNext", "DOM"],
|
| 13 |
+
"skipLibCheck": true,
|
| 14 |
+
"types": ["vite/client"],
|
| 15 |
+
"baseUrl": "./",
|
| 16 |
+
"paths": {
|
| 17 |
+
"@/*": ["src/*"]
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
| 21 |
+
"references": [{ "path": "./tsconfig.node.json" }]
|
| 22 |
+
}
|
frontend/tsconfig.node.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"composite": true,
|
| 4 |
+
"module": "ESNext",
|
| 5 |
+
"moduleResolution": "Node",
|
| 6 |
+
"allowSyntheticDefaultImports": true
|
| 7 |
+
},
|
| 8 |
+
"include": ["vite.config.ts"]
|
| 9 |
+
}
|
frontend/vite.config.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from "vite";
|
| 2 |
+
import vue from "@vitejs/plugin-vue";
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [vue()],
|
| 6 |
+
server: {
|
| 7 |
+
port: 5174
|
| 8 |
+
}
|
| 9 |
+
});
|