Spaces:
Running
Running
NeonClary Cursor commited on
Commit ·
20fa64f
1
Parent(s): f61792d
Adopt FEAT cybersecurity branding, CSS, Canvas/Chat/Sidebar UI, HF Spaces deploy infra, SQLite shim wiring, last-week's advisor cap + thinking-indicator fixes
Browse files- Dockerfile +68 -35
- README.md +31 -2
- docker-compose.yml +29 -38
- multi_llm_chatbot_backend/app/api/routes/chat.py +47 -3
- multi_llm_chatbot_backend/app/api/routes/chat_sessions.py +7 -77
- multi_llm_chatbot_backend/app/config.py +34 -11
- multi_llm_chatbot_backend/app/core/canvas_database.py +30 -63
- multi_llm_chatbot_backend/app/core/database.py +36 -69
- multi_llm_chatbot_backend/app/core/improved_orchestrator.py +98 -36
- multi_llm_chatbot_backend/app/core/rag_manager.py +22 -4
- multi_llm_chatbot_backend/app/main.py +39 -19
- multi_llm_chatbot_backend/app/utils/chroma_client.py +22 -1
- multi_llm_chatbot_backend/requirements.txt +5 -2
- phd-advisor-frontend/package-lock.json +923 -70
- phd-advisor-frontend/package.json +6 -1
- phd-advisor-frontend/public/index.html +2 -2
- phd-advisor-frontend/public/manifest.json +2 -2
- phd-advisor-frontend/src/App.js +8 -13
- phd-advisor-frontend/src/components/AdvisorStatusDropdown.js +217 -103
- phd-advisor-frontend/src/components/CopyrightNotice.js +1 -1
- phd-advisor-frontend/src/components/MessageBubble.js +30 -26
- phd-advisor-frontend/src/components/Sidebar.js +246 -120
- phd-advisor-frontend/src/components/Signup.js +35 -23
- phd-advisor-frontend/src/components/ThinkingIndicator.js +24 -40
- phd-advisor-frontend/src/data/userGuide.js +37 -68
- phd-advisor-frontend/src/pages/CanvasPage.js +1006 -508
- phd-advisor-frontend/src/pages/ChatPage.js +168 -87
- phd-advisor-frontend/src/pages/HomePage.js +8 -21
- phd-advisor-frontend/src/styles/CanvasPage.css +0 -0
- phd-advisor-frontend/src/styles/ChatPage.css +11 -38
- phd-advisor-frontend/src/styles/Sidebar.css +1 -70
- phd-advisor-frontend/src/styles/components.css +58 -6
Dockerfile
CHANGED
|
@@ -1,45 +1,78 @@
|
|
| 1 |
-
# syntax=docker/dockerfile:1
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
|
|
|
| 6 |
|
| 7 |
-
ENV OVOS_CONFIG_BASE_FOLDER=neon
|
| 8 |
-
ENV OVOS_CONFIG_FILENAME=neon.yaml
|
| 9 |
-
ENV XDG_CONFIG_HOME=/config
|
| 10 |
-
|
| 11 |
-
RUN apt-get update && \
|
| 12 |
-
apt-get install -y --no-install-recommends \
|
| 13 |
-
python3 \
|
| 14 |
-
python3-pip \
|
| 15 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
-
|
| 17 |
-
# ---- Python dependencies (cached unless requirements.txt changes) ----------
|
| 18 |
-
WORKDIR /ccai/multi_llm_chatbot_backend
|
| 19 |
-
COPY multi_llm_chatbot_backend/requirements.txt ./
|
| 20 |
-
RUN --mount=type=cache,target=/root/.cache/pip \
|
| 21 |
-
pip install --break-system-packages -r requirements.txt
|
| 22 |
-
|
| 23 |
-
# ---- Node dependencies (cached unless package.json changes) ----------------
|
| 24 |
-
WORKDIR /ccai/phd-advisor-frontend
|
| 25 |
COPY phd-advisor-frontend/package.json phd-advisor-frontend/package-lock.json* ./
|
| 26 |
RUN --mount=type=cache,target=/root/.npm \
|
| 27 |
-
npm
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
#
|
| 30 |
-
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
-
# ----
|
| 34 |
-
FROM
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
RUN apt-get update && \
|
| 37 |
apt-get install -y --no-install-recommends ffmpeg && \
|
| 38 |
rm -rf /var/lib/apt/lists/*
|
| 39 |
-
WORKDIR /ccai/multi_llm_chatbot_backend
|
| 40 |
-
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
| 41 |
|
| 42 |
-
#
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1.7
|
| 2 |
+
# ---------------------------------------------------------------------------
|
| 3 |
+
# Cybersecurity Panel — single-container HuggingFace Spaces image.
|
| 4 |
+
#
|
| 5 |
+
# Mirrors the structural choices used by the working HF Spaces deployments
|
| 6 |
+
# CCAI-Vibe-Demo and CU-Student-AIProject-Helper:
|
| 7 |
+
# * multi-stage Node frontend build → Python+SPA bundle
|
| 8 |
+
# * non-root user uid 1000, port 7860 (HF Spaces convention)
|
| 9 |
+
# * persistence on the HF Storage Bucket mount at /data (SQLite shim)
|
| 10 |
+
# * REACT_APP_API_URL is set to the empty string at build time so the SPA
|
| 11 |
+
# issues relative URLs and shares the FastAPI origin
|
| 12 |
+
#
|
| 13 |
+
# Build context: repo root (this file).
|
| 14 |
+
# ---------------------------------------------------------------------------
|
| 15 |
|
| 16 |
+
# ---- 1. Frontend build (CRA) ----------------------------------------------
|
| 17 |
+
FROM node:20-bookworm AS frontend-build
|
| 18 |
+
WORKDIR /app/frontend
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
COPY phd-advisor-frontend/package.json phd-advisor-frontend/package-lock.json* ./
|
| 21 |
RUN --mount=type=cache,target=/root/.npm \
|
| 22 |
+
npm ci
|
| 23 |
+
|
| 24 |
+
COPY phd-advisor-frontend/ ./
|
| 25 |
|
| 26 |
+
# Empty REACT_APP_API_URL → CRA inlines '' so every fetch() hits the same
|
| 27 |
+
# origin as the SPA (the FastAPI server below).
|
| 28 |
+
ENV REACT_APP_API_URL=""
|
| 29 |
+
RUN npm run build
|
| 30 |
|
| 31 |
+
# ---- 2. Python runtime + SPA bundle ---------------------------------------
|
| 32 |
+
FROM python:3.12-slim-bookworm
|
| 33 |
+
|
| 34 |
+
# ffmpeg is required by the /api/voice/transcribe endpoint
|
| 35 |
+
# (browser WebM/Opus → WAV for Whisper). Kept on the runtime image only,
|
| 36 |
+
# not on the build image, so we don't pay the apt cost during incremental
|
| 37 |
+
# code rebuilds.
|
| 38 |
RUN apt-get update && \
|
| 39 |
apt-get install -y --no-install-recommends ffmpeg && \
|
| 40 |
rm -rf /var/lib/apt/lists/*
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
# HF Spaces runs the container as uid 1000 and expects the app to do the
|
| 43 |
+
# same; with a non-root user we cannot bind privileged ports, but :7860 is
|
| 44 |
+
# fine.
|
| 45 |
+
RUN useradd -m -u 1000 user
|
| 46 |
+
USER user
|
| 47 |
+
ENV HOME=/home/user \
|
| 48 |
+
PATH=/home/user/.local/bin:$PATH \
|
| 49 |
+
PYTHONUNBUFFERED=1 \
|
| 50 |
+
CORS_ORIGINS=* \
|
| 51 |
+
DATA_DIR=/data \
|
| 52 |
+
CONFIG_PATH=/home/user/app/cybersecurity_config.yaml
|
| 53 |
+
|
| 54 |
+
WORKDIR $HOME/app
|
| 55 |
+
|
| 56 |
+
# ---- Python deps (cached unless requirements.txt changes) -----------------
|
| 57 |
+
COPY --chown=user multi_llm_chatbot_backend/requirements.txt ./
|
| 58 |
+
RUN --mount=type=cache,target=/home/user/.cache/pip,uid=1000,gid=1000 \
|
| 59 |
+
pip install --no-cache-dir --user -r requirements.txt
|
| 60 |
+
|
| 61 |
+
# ---- Backend source -------------------------------------------------------
|
| 62 |
+
COPY --chown=user multi_llm_chatbot_backend/ ./
|
| 63 |
+
|
| 64 |
+
# ---- Top-level configuration files (config.yaml + persona definitions) ----
|
| 65 |
+
COPY --chown=user cybersecurity_config.yaml ./cybersecurity_config.yaml
|
| 66 |
+
COPY --chown=user phd_config.yaml ./phd_config.yaml
|
| 67 |
+
COPY --chown=user undergrad_config.yaml ./undergrad_config.yaml
|
| 68 |
+
COPY --chown=user personas/ ./personas/
|
| 69 |
+
|
| 70 |
+
# ---- Frontend bundle ------------------------------------------------------
|
| 71 |
+
# main.py mounts $HOME/app/static at "/" so the SPA is served same-origin
|
| 72 |
+
# with the API.
|
| 73 |
+
COPY --chown=user --from=frontend-build /app/frontend/build ./static
|
| 74 |
+
|
| 75 |
+
ENV PYTHONPATH=$HOME/app
|
| 76 |
+
|
| 77 |
+
EXPOSE 7860
|
| 78 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,6 +1,35 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
An AI-powered academic guidance system that provides personalized advice through specialized advisor personas. Get diverse perspectives on your PhD journey from multiple AI advisors, each bringing unique expertise in methodology, theory, and practical guidance.
|
| 4 |
|
| 5 |
## Features
|
| 6 |
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Cybersecurity Panel
|
| 3 |
+
emoji: 🛡️
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 7860
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Cybersecurity Panel
|
| 12 |
+
|
| 13 |
+
An AI-powered cybersecurity guidance system that provides expert advice through a panel of specialized advisor personas. Ask about threats, compliance, incident response, architecture, and career growth and get diverse perspectives from multiple AI advisors. Built on Neon AI's Collaborative Conversational AI (CCAI) framework.
|
| 14 |
+
|
| 15 |
+
## Hugging Face Spaces deployment
|
| 16 |
+
|
| 17 |
+
This Space ships as a single Docker image built from the repository root [`Dockerfile`](Dockerfile). The container does three things:
|
| 18 |
+
|
| 19 |
+
1. Builds the React frontend (CRA) at image-build time with `REACT_APP_API_URL=""` so every `fetch` issues a relative URL.
|
| 20 |
+
2. Serves the bundled SPA from FastAPI at `/`, with the API exposed on `/api/...`, `/auth/...`, etc., on the same `:7860` origin.
|
| 21 |
+
3. Persists user data (auth, profiles, chat sessions, onboarding, canvas state) in **SQLite via `aiosqlite`** at `${DATA_DIR}/cybersecurity_panel.db`. Mount a Hugging Face Storage Bucket at `/data` to make the database survive Space rebuilds — there is **no MongoDB**, **no Atlas**, no third-party data plane (the persistence pattern follows [`CU-Student-AIProject-Helper`](https://github.com/NeonClary/CU-Student-AIProject-Helper)).
|
| 22 |
+
|
| 23 |
+
### Required Space secrets
|
| 24 |
+
|
| 25 |
+
| Secret | Purpose |
|
| 26 |
+
|--------|---------|
|
| 27 |
+
| `JWT_SECRET_KEY` | Signs auth tokens. Set this to a long random string. |
|
| 28 |
+
| `GEMINI_API_KEY` | Powers the default Gemini provider (model: `gemini-2.5-flash`). |
|
| 29 |
+
| `OPENAI_API_KEY` | Optional — only required if you switch to the OpenAI provider. |
|
| 30 |
+
| `VLLM_API_KEY` | Optional — only required if you point the orchestrator/advisors at a Neon vLLM endpoint. |
|
| 31 |
+
|
| 32 |
|
|
|
|
| 33 |
|
| 34 |
## Features
|
| 35 |
|
docker-compose.yml
CHANGED
|
@@ -1,48 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
services:
|
| 2 |
-
|
| 3 |
build:
|
| 4 |
context: .
|
| 5 |
dockerfile: Dockerfile
|
| 6 |
-
|
| 7 |
ports:
|
| 8 |
-
- "
|
| 9 |
-
networks:
|
| 10 |
-
- application
|
| 11 |
-
depends_on:
|
| 12 |
-
- database
|
| 13 |
environment:
|
| 14 |
-
MONGODB_CONNECTION_STRING: "mongodb://database:27017"
|
| 15 |
-
MONGODB_DATABASE: phd_advisor
|
| 16 |
JWT_SECRET_KEY: ${JWT_SECRET_KEY:-CHANGEME-by-overriding-in-dot-env-file}
|
| 17 |
-
GEMINI_API_KEY: ${GEMINI_API_KEY:-
|
|
|
|
| 18 |
VLLM_API_KEY: ${VLLM_API_KEY:-}
|
| 19 |
-
|
| 20 |
-
GEMINI_MODEL: gemini-2.5-flash
|
| 21 |
-
CONFIG_PATH: ${CONFIG_PATH:-/
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
ports:
|
| 28 |
-
- "3000:3000"
|
| 29 |
-
networks:
|
| 30 |
-
- application
|
| 31 |
-
depends_on:
|
| 32 |
-
- backend
|
| 33 |
-
environment:
|
| 34 |
-
REACT_APP_API_URL: ${REACT_APP_API_URL:-http://localhost:8000}
|
| 35 |
-
REACT_APP_TESTING_ONBOARDING: ${REACT_APP_TESTING_ONBOARDING:-false}
|
| 36 |
-
database:
|
| 37 |
-
image: mongo:8.0
|
| 38 |
volumes:
|
| 39 |
-
-
|
| 40 |
-
|
| 41 |
-
- application
|
| 42 |
volumes:
|
| 43 |
-
|
| 44 |
-
networks:
|
| 45 |
-
application:
|
| 46 |
-
driver: bridge
|
| 47 |
-
driver_opts:
|
| 48 |
-
com.docker.network.bridge.host_binding_ipv4: "0.0.0.0"
|
|
|
|
| 1 |
+
# ---------------------------------------------------------------------------
|
| 2 |
+
# Local-dev wrapper around the single-container HuggingFace Spaces image.
|
| 3 |
+
#
|
| 4 |
+
# This file used to spin up three services (FastAPI backend, CRA dev server,
|
| 5 |
+
# MongoDB). The HF deployment shape (single-container, SQLite on the bucket)
|
| 6 |
+
# is now also the local-dev shape — same image you push to the Space — so
|
| 7 |
+
# this is a one-service compose with a named volume that emulates the HF
|
| 8 |
+
# Storage Bucket mount.
|
| 9 |
+
#
|
| 10 |
+
# Override CYBERPANEL_HOST_PORT via .env (or shell) if 7861 conflicts; the
|
| 11 |
+
# in-container port is fixed at 7860 to mirror HF Spaces.
|
| 12 |
+
# ---------------------------------------------------------------------------
|
| 13 |
+
|
| 14 |
services:
|
| 15 |
+
app:
|
| 16 |
build:
|
| 17 |
context: .
|
| 18 |
dockerfile: Dockerfile
|
| 19 |
+
image: cybersecurity-panel:dev
|
| 20 |
ports:
|
| 21 |
+
- "${CYBERPANEL_HOST_PORT:-7861}:7860"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
environment:
|
|
|
|
|
|
|
| 23 |
JWT_SECRET_KEY: ${JWT_SECRET_KEY:-CHANGEME-by-overriding-in-dot-env-file}
|
| 24 |
+
GEMINI_API_KEY: ${GEMINI_API_KEY:-}
|
| 25 |
+
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
| 26 |
VLLM_API_KEY: ${VLLM_API_KEY:-}
|
| 27 |
+
VLLM_API_USERNAME: ${VLLM_API_USERNAME:-}
|
| 28 |
+
GEMINI_MODEL: ${GEMINI_MODEL:-gemini-2.5-flash}
|
| 29 |
+
CONFIG_PATH: ${CONFIG_PATH:-/home/user/app/cybersecurity_config.yaml}
|
| 30 |
+
# Same-origin in single-container; only relevant for cross-origin dev.
|
| 31 |
+
CORS_ORIGINS: ${CORS_ORIGINS:-*}
|
| 32 |
+
# SQLite + ChromaDB persistence both land here. The named volume below
|
| 33 |
+
# mirrors the HF Storage Bucket mount so user data survives rebuilds.
|
| 34 |
+
DATA_DIR: /data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
volumes:
|
| 36 |
+
- cybersecurity_data:/data
|
| 37 |
+
|
|
|
|
| 38 |
volumes:
|
| 39 |
+
cybersecurity_data:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
multi_llm_chatbot_backend/app/api/routes/chat.py
CHANGED
|
@@ -16,12 +16,32 @@ from app.core.bootstrap import chat_orchestrator
|
|
| 16 |
from app.core.database import get_database
|
| 17 |
from app.core.session_manager import get_session_manager
|
| 18 |
from app.models.user import User
|
|
|
|
| 19 |
|
| 20 |
logger = logging.getLogger(__name__)
|
| 21 |
|
| 22 |
router = APIRouter()
|
| 23 |
session_manager = get_session_manager()
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
# Enhanced data models
|
| 26 |
class UserInput(BaseModel):
|
| 27 |
user_input: str
|
|
@@ -91,6 +111,7 @@ async def chat_stream(
|
|
| 91 |
sid = await get_or_create_session_for_request_async(request)
|
| 92 |
|
| 93 |
session = session_manager.get_session(sid)
|
|
|
|
| 94 |
|
| 95 |
# Append user message to in-memory session and persist to MongoDB
|
| 96 |
session.append_message("user", message.user_input)
|
|
@@ -137,11 +158,33 @@ async def chat_stream(
|
|
| 137 |
).to_ndjson()
|
| 138 |
return
|
| 139 |
|
| 140 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
top_personas = await chat_orchestrator.get_top_personas(
|
| 142 |
session_id=sid,
|
|
|
|
|
|
|
| 143 |
)
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
done_queue: asyncio.Queue = asyncio.Queue()
|
| 146 |
|
| 147 |
async def _run(pid: str) -> None:
|
|
@@ -155,9 +198,10 @@ async def chat_stream(
|
|
| 155 |
await done_queue.put(result)
|
| 156 |
except Exception as e:
|
| 157 |
logger.exception(f"chat-stream _run failed for {pid}: {e}")
|
|
|
|
| 158 |
await done_queue.put({
|
| 159 |
-
"persona_id":
|
| 160 |
-
"persona_name":
|
| 161 |
"response": f"I ran into a technical issue. Please try again. ({e!s})",
|
| 162 |
"used_documents": False,
|
| 163 |
"document_chunks_used": 0,
|
|
|
|
| 16 |
from app.core.database import get_database
|
| 17 |
from app.core.session_manager import get_session_manager
|
| 18 |
from app.models.user import User
|
| 19 |
+
from app.api.routes.user_profile import PROFILE_FIELDS, enrich_profile_from_user
|
| 20 |
|
| 21 |
logger = logging.getLogger(__name__)
|
| 22 |
|
| 23 |
router = APIRouter()
|
| 24 |
session_manager = get_session_manager()
|
| 25 |
|
| 26 |
+
|
| 27 |
+
async def _attach_user_profile_context(session, user: User) -> None:
|
| 28 |
+
"""Load Mongo profile + signup fields into session for persona prompts."""
|
| 29 |
+
try:
|
| 30 |
+
db = get_database()
|
| 31 |
+
doc = await db.user_profiles.find_one({"user_id": user.id})
|
| 32 |
+
profile = enrich_profile_from_user(doc, user)
|
| 33 |
+
parts = []
|
| 34 |
+
for key in PROFILE_FIELDS:
|
| 35 |
+
val = profile.get(key)
|
| 36 |
+
if val:
|
| 37 |
+
if isinstance(val, list):
|
| 38 |
+
val = ", ".join(str(v) for v in val)
|
| 39 |
+
parts.append(f"{key}: {val}")
|
| 40 |
+
if parts:
|
| 41 |
+
session.user_profile_context = "USER SECURITY PROFILE: " + "; ".join(parts)
|
| 42 |
+
except Exception as prof_err:
|
| 43 |
+
logger.warning(f"Could not load user profile: {prof_err}")
|
| 44 |
+
|
| 45 |
# Enhanced data models
|
| 46 |
class UserInput(BaseModel):
|
| 47 |
user_input: str
|
|
|
|
| 111 |
sid = await get_or_create_session_for_request_async(request)
|
| 112 |
|
| 113 |
session = session_manager.get_session(sid)
|
| 114 |
+
await _attach_user_profile_context(session, current_user)
|
| 115 |
|
| 116 |
# Append user message to in-memory session and persist to MongoDB
|
| 117 |
session.append_message("user", message.user_input)
|
|
|
|
| 158 |
).to_ndjson()
|
| 159 |
return
|
| 160 |
|
| 161 |
+
# Always relevance-rank to the top 3 advisors, scoped to the
|
| 162 |
+
# user's active-advisor selection (the header dropdown) when one is
|
| 163 |
+
# provided. The dropdown filters the candidate pool; the LLM
|
| 164 |
+
# ranking still picks the top 3 from that pool.
|
| 165 |
+
if message.active_advisors:
|
| 166 |
+
candidate_ids = [
|
| 167 |
+
pid for pid in message.active_advisors
|
| 168 |
+
if pid in chat_orchestrator.personas
|
| 169 |
+
]
|
| 170 |
+
else:
|
| 171 |
+
candidate_ids = list(chat_orchestrator.personas.keys())
|
| 172 |
top_personas = await chat_orchestrator.get_top_personas(
|
| 173 |
session_id=sid,
|
| 174 |
+
k=3,
|
| 175 |
+
candidate_ids=candidate_ids,
|
| 176 |
)
|
| 177 |
|
| 178 |
+
# Tell the client which advisors will respond so it can show
|
| 179 |
+
# thinking indicators for just those, not the entire active pool.
|
| 180 |
+
yield ChatStreamLine(
|
| 181 |
+
type="progress",
|
| 182 |
+
data={
|
| 183 |
+
"phase": "selected",
|
| 184 |
+
"selected_advisors": top_personas,
|
| 185 |
+
},
|
| 186 |
+
).to_ndjson()
|
| 187 |
+
|
| 188 |
done_queue: asyncio.Queue = asyncio.Queue()
|
| 189 |
|
| 190 |
async def _run(pid: str) -> None:
|
|
|
|
| 198 |
await done_queue.put(result)
|
| 199 |
except Exception as e:
|
| 200 |
logger.exception(f"chat-stream _run failed for {pid}: {e}")
|
| 201 |
+
failed_persona = chat_orchestrator.get_persona(pid)
|
| 202 |
await done_queue.put({
|
| 203 |
+
"persona_id": pid,
|
| 204 |
+
"persona_name": failed_persona.name if failed_persona else pid,
|
| 205 |
"response": f"I ran into a technical issue. Please try again. ({e!s})",
|
| 206 |
"used_documents": False,
|
| 207 |
"document_chunks_used": 0,
|
multi_llm_chatbot_backend/app/api/routes/chat_sessions.py
CHANGED
|
@@ -43,12 +43,7 @@ async def create_chat_session(
|
|
| 43 |
request: CreateChatSessionRequest,
|
| 44 |
current_user: User = Depends(get_current_active_user)
|
| 45 |
):
|
| 46 |
-
"""
|
| 47 |
-
Create a new chat session for the authenticated user.
|
| 48 |
-
@param request: CreateChatSessionRequest with the session title
|
| 49 |
-
@param current_user: Authenticated user from dependency injection
|
| 50 |
-
@return: Dict with the new session id, title, timestamps, and message_count
|
| 51 |
-
"""
|
| 52 |
try:
|
| 53 |
db = get_database()
|
| 54 |
|
|
@@ -85,13 +80,7 @@ async def get_user_chat_sessions(
|
|
| 85 |
limit: int = 50,
|
| 86 |
skip: int = 0
|
| 87 |
):
|
| 88 |
-
"""
|
| 89 |
-
Get all active chat sessions for the authenticated user.
|
| 90 |
-
@param current_user: Authenticated user from dependency injection
|
| 91 |
-
@param limit: Maximum number of sessions to return (default 50)
|
| 92 |
-
@param skip: Number of sessions to skip for pagination (default 0)
|
| 93 |
-
@return: List of ChatSessionResponse sorted by most recently updated
|
| 94 |
-
"""
|
| 95 |
try:
|
| 96 |
db = get_database()
|
| 97 |
|
|
@@ -124,11 +113,7 @@ async def get_user_chat_sessions(
|
|
| 124 |
async def get_chat_sessions_count(
|
| 125 |
current_user: User = Depends(get_current_active_user)
|
| 126 |
):
|
| 127 |
-
"""
|
| 128 |
-
Get count of active, non-deleted chat sessions for the authenticated user.
|
| 129 |
-
@param current_user: Authenticated user from dependency injection
|
| 130 |
-
@return: Dict with the session count
|
| 131 |
-
"""
|
| 132 |
try:
|
| 133 |
db = get_database()
|
| 134 |
user_object_id = ObjectId(str(current_user.id))
|
|
@@ -155,12 +140,7 @@ async def get_chat_session(
|
|
| 155 |
session_id: str,
|
| 156 |
current_user: User = Depends(get_current_active_user)
|
| 157 |
):
|
| 158 |
-
"""
|
| 159 |
-
Get a specific chat session with all messages.
|
| 160 |
-
@param session_id: MongoDB ObjectId of the chat session
|
| 161 |
-
@param current_user: Authenticated user from dependency injection
|
| 162 |
-
@return: Dict with session id, title, messages, and timestamps
|
| 163 |
-
"""
|
| 164 |
try:
|
| 165 |
db = get_database()
|
| 166 |
|
|
@@ -199,13 +179,7 @@ async def update_chat_session(
|
|
| 199 |
request: UpdateChatSessionRequest,
|
| 200 |
current_user: User = Depends(get_current_active_user)
|
| 201 |
):
|
| 202 |
-
"""
|
| 203 |
-
Update a chat session's title and/or messages.
|
| 204 |
-
@param session_id: MongoDB ObjectId of the chat session
|
| 205 |
-
@param request: UpdateChatSessionRequest with optional title and messages
|
| 206 |
-
@param current_user: Authenticated user from dependency injection
|
| 207 |
-
@return: Dict with a confirmation message
|
| 208 |
-
"""
|
| 209 |
try:
|
| 210 |
db = get_database()
|
| 211 |
|
|
@@ -252,13 +226,7 @@ async def save_message_to_session(
|
|
| 252 |
request: SaveMessageRequest,
|
| 253 |
current_user: User = Depends(get_current_active_user)
|
| 254 |
):
|
| 255 |
-
"""
|
| 256 |
-
Add a message to a chat session.
|
| 257 |
-
@param session_id: MongoDB ObjectId of the chat session
|
| 258 |
-
@param request: SaveMessageRequest with the message dict
|
| 259 |
-
@param current_user: Authenticated user from dependency injection
|
| 260 |
-
@return: Dict with a confirmation message
|
| 261 |
-
"""
|
| 262 |
try:
|
| 263 |
db = get_database()
|
| 264 |
|
|
@@ -290,50 +258,12 @@ async def save_message_to_session(
|
|
| 290 |
|
| 291 |
|
| 292 |
|
| 293 |
-
@router.delete("/chat-sessions")
|
| 294 |
-
async def delete_all_chat_sessions(
|
| 295 |
-
current_user: User = Depends(get_current_active_user)
|
| 296 |
-
):
|
| 297 |
-
"""
|
| 298 |
-
Soft-delete all chat sessions for the authenticated user.
|
| 299 |
-
@param current_user: Authenticated user from dependency injection
|
| 300 |
-
@return: Dict with a confirmation message and the number of deleted sessions
|
| 301 |
-
"""
|
| 302 |
-
try:
|
| 303 |
-
db = get_database()
|
| 304 |
-
|
| 305 |
-
result = await db.chat_sessions.update_many(
|
| 306 |
-
{
|
| 307 |
-
"user_id": current_user.id,
|
| 308 |
-
"is_active": True
|
| 309 |
-
},
|
| 310 |
-
{"$set": {"is_active": False, "updated_at": datetime.utcnow()}}
|
| 311 |
-
)
|
| 312 |
-
|
| 313 |
-
return {
|
| 314 |
-
"message": f"Deleted {result.modified_count} chat sessions",
|
| 315 |
-
"deleted_count": result.modified_count
|
| 316 |
-
}
|
| 317 |
-
|
| 318 |
-
except Exception as e:
|
| 319 |
-
logger.error(f"Error deleting all chat sessions for user {current_user.id}: {e}")
|
| 320 |
-
raise HTTPException(
|
| 321 |
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 322 |
-
detail="Could not delete chat sessions"
|
| 323 |
-
)
|
| 324 |
-
|
| 325 |
-
|
| 326 |
@router.delete("/chat-sessions/{session_id}")
|
| 327 |
async def delete_chat_session(
|
| 328 |
session_id: str,
|
| 329 |
current_user: User = Depends(get_current_active_user)
|
| 330 |
):
|
| 331 |
-
"""
|
| 332 |
-
Soft-delete a single chat session.
|
| 333 |
-
@param session_id: MongoDB ObjectId of the chat session
|
| 334 |
-
@param current_user: Authenticated user from dependency injection
|
| 335 |
-
@return: Dict with a confirmation message
|
| 336 |
-
"""
|
| 337 |
try:
|
| 338 |
db = get_database()
|
| 339 |
|
|
|
|
| 43 |
request: CreateChatSessionRequest,
|
| 44 |
current_user: User = Depends(get_current_active_user)
|
| 45 |
):
|
| 46 |
+
"""Create a new chat session for the user"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
try:
|
| 48 |
db = get_database()
|
| 49 |
|
|
|
|
| 80 |
limit: int = 50,
|
| 81 |
skip: int = 0
|
| 82 |
):
|
| 83 |
+
"""Get all chat sessions for the current user"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
try:
|
| 85 |
db = get_database()
|
| 86 |
|
|
|
|
| 113 |
async def get_chat_sessions_count(
|
| 114 |
current_user: User = Depends(get_current_active_user)
|
| 115 |
):
|
| 116 |
+
"""Get count of user's chat sessions"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
try:
|
| 118 |
db = get_database()
|
| 119 |
user_object_id = ObjectId(str(current_user.id))
|
|
|
|
| 140 |
session_id: str,
|
| 141 |
current_user: User = Depends(get_current_active_user)
|
| 142 |
):
|
| 143 |
+
"""Get a specific chat session with all messages"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
try:
|
| 145 |
db = get_database()
|
| 146 |
|
|
|
|
| 179 |
request: UpdateChatSessionRequest,
|
| 180 |
current_user: User = Depends(get_current_active_user)
|
| 181 |
):
|
| 182 |
+
"""Update a chat session (title or messages)"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
try:
|
| 184 |
db = get_database()
|
| 185 |
|
|
|
|
| 226 |
request: SaveMessageRequest,
|
| 227 |
current_user: User = Depends(get_current_active_user)
|
| 228 |
):
|
| 229 |
+
"""Add a message to a chat session"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
try:
|
| 231 |
db = get_database()
|
| 232 |
|
|
|
|
| 258 |
|
| 259 |
|
| 260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
@router.delete("/chat-sessions/{session_id}")
|
| 262 |
async def delete_chat_session(
|
| 263 |
session_id: str,
|
| 264 |
current_user: User = Depends(get_current_active_user)
|
| 265 |
):
|
| 266 |
+
"""Delete a chat session (soft delete)"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
try:
|
| 268 |
db = get_database()
|
| 269 |
|
multi_llm_chatbot_backend/app/config.py
CHANGED
|
@@ -19,7 +19,6 @@ import yaml
|
|
| 19 |
from pydantic import BaseModel, validator, Field, model_validator
|
| 20 |
|
| 21 |
from app.utils.avatar_helpers import get_bundled_avatar_path
|
| 22 |
-
from app.version import __version__
|
| 23 |
|
| 24 |
logger = logging.getLogger(__name__)
|
| 25 |
|
|
@@ -49,11 +48,19 @@ class FeatureConfig(_IconValidatorMixin):
|
|
| 49 |
icon: str = "HelpCircle"
|
| 50 |
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
class AppConfig(BaseModel):
|
| 53 |
title: str = "Advisor Canvas"
|
| 54 |
subtitle: str = "AI-Powered Guidance"
|
| 55 |
primary_color: str = "#7C3AED"
|
| 56 |
footer_text: str = ""
|
|
|
|
| 57 |
|
| 58 |
|
| 59 |
class HomepageConfig(BaseModel):
|
|
@@ -73,6 +80,8 @@ class LoginConfig(BaseModel):
|
|
| 73 |
subtitle: str = "Sign in to continue"
|
| 74 |
signup_subtitle: str = "Create your account to get personalized guidance from expert advisors"
|
| 75 |
academic_stages: List[AcademicStage] = []
|
|
|
|
|
|
|
| 76 |
|
| 77 |
|
| 78 |
class ExampleCategory(_IconValidatorMixin):
|
|
@@ -88,12 +97,6 @@ class ChatPageConfig(BaseModel):
|
|
| 88 |
examples: List[ExampleCategory] = []
|
| 89 |
|
| 90 |
|
| 91 |
-
class OnboardingConfig(BaseModel):
|
| 92 |
-
features: List[FeatureConfig] = []
|
| 93 |
-
tour_title: str = ""
|
| 94 |
-
tour_body: str = ""
|
| 95 |
-
|
| 96 |
-
|
| 97 |
class PersonaItemConfig(_IconValidatorMixin):
|
| 98 |
id: str
|
| 99 |
name: str
|
|
@@ -154,7 +157,11 @@ class PersonaItemConfig(_IconValidatorMixin):
|
|
| 154 |
self.avatar, self.id,
|
| 155 |
)
|
| 156 |
return f"icon://{self.icon}"
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
return f"{base}/api/avatars/bundled/{self.avatar}"
|
| 159 |
|
| 160 |
def to_frontend_config(self) -> dict:
|
|
@@ -194,6 +201,7 @@ class PersonasConfig(BaseModel):
|
|
| 194 |
|
| 195 |
class OrchestratorConfig(BaseModel):
|
| 196 |
min_words_without_keywords: int = 6
|
|
|
|
| 197 |
specific_keywords: List[str] = []
|
| 198 |
clarification_questions: List[str] = [
|
| 199 |
"Could you provide more details about what you need help with?"]
|
|
@@ -270,12 +278,30 @@ class OllamaConfig(BaseModel):
|
|
| 270 |
class VllmConfig(BaseModel):
|
| 271 |
api_url: str = ""
|
| 272 |
api_key: str = Field(default=os.getenv("VLLM_API_KEY", ""))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
|
| 275 |
class LLMConfig(BaseModel):
|
|
|
|
| 276 |
gemini: GeminiConfig = GeminiConfig()
|
| 277 |
ollama: OllamaConfig = OllamaConfig()
|
| 278 |
vllm: VllmConfig = VllmConfig()
|
|
|
|
|
|
|
| 279 |
|
| 280 |
|
| 281 |
class RAGConfig(BaseModel):
|
|
@@ -311,7 +337,6 @@ class AppSettings(BaseModel):
|
|
| 311 |
homepage: HomepageConfig = HomepageConfig()
|
| 312 |
login: LoginConfig = LoginConfig()
|
| 313 |
chat_page: ChatPageConfig = ChatPageConfig()
|
| 314 |
-
onboarding: OnboardingConfig = OnboardingConfig()
|
| 315 |
personas: PersonasConfig = PersonasConfig()
|
| 316 |
orchestrator: OrchestratorConfig = OrchestratorConfig()
|
| 317 |
auth: AuthConfig = AuthConfig()
|
|
@@ -333,11 +358,9 @@ class AppSettings(BaseModel):
|
|
| 333 |
"homepage": self.homepage.dict(),
|
| 334 |
"login": self.login.dict(),
|
| 335 |
"chat_page": self.chat_page.dict(),
|
| 336 |
-
"onboarding": self.onboarding.dict(),
|
| 337 |
"personas": {
|
| 338 |
"items": [p.to_frontend_config() for p in self.personas.items],
|
| 339 |
},
|
| 340 |
-
"version": __version__,
|
| 341 |
}
|
| 342 |
|
| 343 |
|
|
|
|
| 19 |
from pydantic import BaseModel, validator, Field, model_validator
|
| 20 |
|
| 21 |
from app.utils.avatar_helpers import get_bundled_avatar_path
|
|
|
|
| 22 |
|
| 23 |
logger = logging.getLogger(__name__)
|
| 24 |
|
|
|
|
| 48 |
icon: str = "HelpCircle"
|
| 49 |
|
| 50 |
|
| 51 |
+
class UserAvatarOption(BaseModel):
|
| 52 |
+
id: str
|
| 53 |
+
icon: str = "User"
|
| 54 |
+
color: str = "#2563EB"
|
| 55 |
+
bg: str = "#EFF6FF"
|
| 56 |
+
|
| 57 |
+
|
| 58 |
class AppConfig(BaseModel):
|
| 59 |
title: str = "Advisor Canvas"
|
| 60 |
subtitle: str = "AI-Powered Guidance"
|
| 61 |
primary_color: str = "#7C3AED"
|
| 62 |
footer_text: str = ""
|
| 63 |
+
user_avatars: List[UserAvatarOption] = []
|
| 64 |
|
| 65 |
|
| 66 |
class HomepageConfig(BaseModel):
|
|
|
|
| 80 |
subtitle: str = "Sign in to continue"
|
| 81 |
signup_subtitle: str = "Create your account to get personalized guidance from expert advisors"
|
| 82 |
academic_stages: List[AcademicStage] = []
|
| 83 |
+
knowledge_levels: List[AcademicStage] = []
|
| 84 |
+
timezones: List[AcademicStage] = []
|
| 85 |
|
| 86 |
|
| 87 |
class ExampleCategory(_IconValidatorMixin):
|
|
|
|
| 97 |
examples: List[ExampleCategory] = []
|
| 98 |
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
class PersonaItemConfig(_IconValidatorMixin):
|
| 101 |
id: str
|
| 102 |
name: str
|
|
|
|
| 157 |
self.avatar, self.id,
|
| 158 |
)
|
| 159 |
return f"icon://{self.icon}"
|
| 160 |
+
# Default to empty string (→ relative URL) so single-origin Spaces
|
| 161 |
+
# deployments serve avatars off the same host as the SPA. Local
|
| 162 |
+
# ``npm start`` development setups can still set REACT_APP_API_URL
|
| 163 |
+
# explicitly to point at the backend on a different port.
|
| 164 |
+
base = os.getenv("REACT_APP_API_URL", "").rstrip("/")
|
| 165 |
return f"{base}/api/avatars/bundled/{self.avatar}"
|
| 166 |
|
| 167 |
def to_frontend_config(self) -> dict:
|
|
|
|
| 201 |
|
| 202 |
class OrchestratorConfig(BaseModel):
|
| 203 |
min_words_without_keywords: int = 6
|
| 204 |
+
conversation_history_token_threshold: int = 4000
|
| 205 |
specific_keywords: List[str] = []
|
| 206 |
clarification_questions: List[str] = [
|
| 207 |
"Could you provide more details about what you need help with?"]
|
|
|
|
| 278 |
class VllmConfig(BaseModel):
|
| 279 |
api_url: str = ""
|
| 280 |
api_key: str = Field(default=os.getenv("VLLM_API_KEY", ""))
|
| 281 |
+
api_username: str = Field(default=os.getenv("VLLM_API_USERNAME", ""))
|
| 282 |
+
model_id: str = ""
|
| 283 |
+
neon_persona_orchestrator: str = "vanilla"
|
| 284 |
+
neon_persona_advisors: str = "CybersecurityExpert"
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
class OpenAIConfig(BaseModel):
|
| 288 |
+
api_key: str = Field(default=os.getenv("OPENAI_API_KEY", ""))
|
| 289 |
+
model: str = "gpt-5.4"
|
| 290 |
+
orchestrator_reasoning_effort: str = "low"
|
| 291 |
+
persona_reasoning_effort: str = "none"
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
class ResilientConfig(BaseModel):
|
| 295 |
+
race_timeout_seconds: float = 3.0
|
| 296 |
|
| 297 |
|
| 298 |
class LLMConfig(BaseModel):
|
| 299 |
+
provider: str = "gemini"
|
| 300 |
gemini: GeminiConfig = GeminiConfig()
|
| 301 |
ollama: OllamaConfig = OllamaConfig()
|
| 302 |
vllm: VllmConfig = VllmConfig()
|
| 303 |
+
openai: OpenAIConfig = OpenAIConfig()
|
| 304 |
+
resilient: ResilientConfig = ResilientConfig()
|
| 305 |
|
| 306 |
|
| 307 |
class RAGConfig(BaseModel):
|
|
|
|
| 337 |
homepage: HomepageConfig = HomepageConfig()
|
| 338 |
login: LoginConfig = LoginConfig()
|
| 339 |
chat_page: ChatPageConfig = ChatPageConfig()
|
|
|
|
| 340 |
personas: PersonasConfig = PersonasConfig()
|
| 341 |
orchestrator: OrchestratorConfig = OrchestratorConfig()
|
| 342 |
auth: AuthConfig = AuthConfig()
|
|
|
|
| 358 |
"homepage": self.homepage.dict(),
|
| 359 |
"login": self.login.dict(),
|
| 360 |
"chat_page": self.chat_page.dict(),
|
|
|
|
| 361 |
"personas": {
|
| 362 |
"items": [p.to_frontend_config() for p in self.personas.items],
|
| 363 |
},
|
|
|
|
| 364 |
}
|
| 365 |
|
| 366 |
|
multi_llm_chatbot_backend/app/core/canvas_database.py
CHANGED
|
@@ -1,68 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import logging
|
| 2 |
-
from
|
| 3 |
|
| 4 |
logger = logging.getLogger(__name__)
|
| 5 |
|
| 6 |
-
async def setup_canvas_collections(db: AsyncIOMotorDatabase):
|
| 7 |
-
"""Setup MongoDB collections and indexes for PhD Canvas"""
|
| 8 |
-
try:
|
| 9 |
-
collection = db.phd_canvases
|
| 10 |
-
|
| 11 |
-
# Create simple indexes
|
| 12 |
-
await collection.create_index("user_id")
|
| 13 |
-
logger.info("Created index on user_id")
|
| 14 |
-
|
| 15 |
-
await collection.create_index("last_updated", background=True)
|
| 16 |
-
logger.info("Created index on last_updated")
|
| 17 |
-
|
| 18 |
-
# Create compound indexes
|
| 19 |
-
await collection.create_index([("user_id", 1), ("last_updated", -1)])
|
| 20 |
-
logger.info("Created compound index on user_id and last_updated")
|
| 21 |
-
|
| 22 |
-
await collection.create_index([("user_id", 1), ("created_at", -1)])
|
| 23 |
-
logger.info("Created compound index on user_id and created_at")
|
| 24 |
-
|
| 25 |
-
# Ensure TTL index for old canvases (optional cleanup after 2 years)
|
| 26 |
-
await collection.create_index(
|
| 27 |
-
"created_at",
|
| 28 |
-
expireAfterSeconds=63072000 # 2 years in seconds
|
| 29 |
-
)
|
| 30 |
-
logger.info("Created TTL index for canvas cleanup")
|
| 31 |
-
|
| 32 |
-
logger.info("PhD Canvas database setup completed successfully")
|
| 33 |
-
|
| 34 |
-
except Exception as e:
|
| 35 |
-
logger.error("Error setting up canvas collections: %s", str(e))
|
| 36 |
-
raise
|
| 37 |
|
| 38 |
-
async def
|
| 39 |
-
"""
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
}
|
| 56 |
-
]
|
| 57 |
-
|
| 58 |
-
orphaned_canvases = await db.phd_canvases.aggregate(pipeline).to_list(length=100)
|
| 59 |
-
|
| 60 |
-
if orphaned_canvases:
|
| 61 |
-
orphaned_ids = [canvas["_id"] for canvas in orphaned_canvases]
|
| 62 |
-
result = await db.phd_canvases.delete_many({"_id": {"$in": orphaned_ids}})
|
| 63 |
-
logger.info("Cleaned up %d orphaned canvas records", result.deleted_count)
|
| 64 |
-
else:
|
| 65 |
-
logger.info("No orphaned canvas records found")
|
| 66 |
-
|
| 67 |
-
except Exception as e:
|
| 68 |
-
logger.error("Error during canvas cleanup: %s", str(e))
|
|
|
|
| 1 |
+
"""PhD Canvas database setup — SQLite-shim aware.
|
| 2 |
+
|
| 3 |
+
The original implementation was a Mongo-only module that created compound
|
| 4 |
+
indexes and a TTL index on the ``phd_canvases`` collection at startup. With
|
| 5 |
+
the SQLite-on-HF-bucket persistence shim, the schema (including the
|
| 6 |
+
``user_id`` index) is declared once at connection time inside
|
| 7 |
+
``app.core.db``, so this module becomes a compatibility no-op.
|
| 8 |
+
|
| 9 |
+
We keep the function signatures so any caller that still imports
|
| 10 |
+
``setup_canvas_collections`` or ``cleanup_old_canvas_data`` keeps working.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
import logging
|
| 14 |
+
from typing import Any
|
| 15 |
|
| 16 |
logger = logging.getLogger(__name__)
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
async def setup_canvas_collections(db: Any) -> None:
|
| 20 |
+
"""No-op on the SQLite shim — indexes are created by ``app.core.db``."""
|
| 21 |
+
logger.info(
|
| 22 |
+
"PhD Canvas setup: schema and indexes are managed by the SQLite shim; "
|
| 23 |
+
"skipping legacy Mongo index creation."
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
async def cleanup_old_canvas_data(db: Any) -> None:
|
| 28 |
+
"""Maintenance helper — disabled on the SQLite shim.
|
| 29 |
+
|
| 30 |
+
The original used a Mongo ``$lookup`` aggregation against the ``users``
|
| 31 |
+
collection, which the in-process aggregation runner doesn't support. If
|
| 32 |
+
you need the orphan cleanup, run it via a separate script that does two
|
| 33 |
+
passes (``users``, then ``phd_canvases``) and calls ``delete_many``.
|
| 34 |
+
"""
|
| 35 |
+
logger.info("cleanup_old_canvas_data is a no-op on the SQLite shim.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
multi_llm_chatbot_backend/app/core/database.py
CHANGED
|
@@ -1,83 +1,50 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import logging
|
| 5 |
|
| 6 |
-
from app.
|
| 7 |
|
| 8 |
logger = logging.getLogger(__name__)
|
| 9 |
|
| 10 |
-
class Database:
|
| 11 |
-
client: AsyncIOMotorClient = None
|
| 12 |
-
database = None
|
| 13 |
-
|
| 14 |
-
db = Database()
|
| 15 |
|
| 16 |
-
async def connect_to_mongo():
|
| 17 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
try:
|
| 19 |
-
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
mongo_url = os.getenv("MONGODB_CONNECTION_STRING", "")
|
| 26 |
-
if not mongo_url:
|
| 27 |
-
raise ValueError(
|
| 28 |
-
"MongoDB connection string not set. "
|
| 29 |
-
"Provide it in config.yaml (mongodb.connection_string) "
|
| 30 |
-
"or as the MONGODB_CONNECTION_STRING environment variable."
|
| 31 |
-
)
|
| 32 |
|
| 33 |
-
db_name = settings.mongodb.database_name
|
| 34 |
-
|
| 35 |
-
db.client = AsyncIOMotorClient(mongo_url)
|
| 36 |
-
db.database = db.client[db_name]
|
| 37 |
-
|
| 38 |
-
# Test connection
|
| 39 |
-
await db.client.admin.command('ping')
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
try:
|
| 46 |
-
from app.core.canvas_database import setup_canvas_collections
|
| 47 |
-
await setup_canvas_collections(db.database) # Pass db directly
|
| 48 |
-
logger.info("Canvas database initialization completed")
|
| 49 |
-
except Exception as canvas_error:
|
| 50 |
-
logger.error(f"Canvas database initialization failed: {canvas_error}")
|
| 51 |
-
|
| 52 |
-
except ConnectionFailure as e:
|
| 53 |
-
logger.error(f"Failed to connect to MongoDB: {e}")
|
| 54 |
-
raise
|
| 55 |
-
except Exception as e:
|
| 56 |
-
logger.error(f"Unexpected error connecting to MongoDB: {e}")
|
| 57 |
-
raise
|
| 58 |
|
| 59 |
-
async def close_mongo_connection():
|
| 60 |
-
"""Close database connection"""
|
| 61 |
-
if db.client:
|
| 62 |
-
db.client.close()
|
| 63 |
-
logger.info("Disconnected from MongoDB")
|
| 64 |
|
| 65 |
-
async def create_indexes():
|
| 66 |
-
"""
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
# Indexes for chat_sessions collection
|
| 73 |
-
await db.database.chat_sessions.create_index("user_id")
|
| 74 |
-
await db.database.chat_sessions.create_index("created_at")
|
| 75 |
-
await db.database.chat_sessions.create_index([("user_id", 1), ("created_at", -1)])
|
| 76 |
-
|
| 77 |
-
logger.info("Database indexes created successfully")
|
| 78 |
-
except Exception as e:
|
| 79 |
-
logger.warning(f"Error creating indexes: {e}")
|
| 80 |
|
| 81 |
def get_database():
|
| 82 |
-
"""
|
| 83 |
-
return
|
|
|
|
| 1 |
+
"""Database connection facade.
|
| 2 |
+
|
| 3 |
+
Used to wrap pymongo / motor against a real MongoDB instance. For Hugging
|
| 4 |
+
Face Spaces deployment we now back the same public API with SQLite on a HF
|
| 5 |
+
Storage Bucket mount (see ``app.core.db``). The shim emulates the Mongo
|
| 6 |
+
idioms the routers and services use, so this module simply forwards to it
|
| 7 |
+
and keeps the legacy ``connect_to_mongo`` / ``close_mongo_connection`` /
|
| 8 |
+
``get_database`` API intact for ``main.py`` and the route files.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
import logging
|
| 12 |
|
| 13 |
+
from app.core.db import _close, _open, get_database as _get_database
|
| 14 |
|
| 15 |
logger = logging.getLogger(__name__)
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
async def connect_to_mongo() -> None:
|
| 19 |
+
"""Open the SQLite-backed database and run schema bootstrap."""
|
| 20 |
+
await _open()
|
| 21 |
+
logger.info("SQLite-backed database ready (Mongo API shim active).")
|
| 22 |
+
# Legacy hook: previously called ``setup_canvas_collections`` here.
|
| 23 |
+
# The phd_canvases table is already declared in the shim's schema, so
|
| 24 |
+
# the call below is a no-op but kept for symmetry with the original
|
| 25 |
+
# startup path.
|
| 26 |
try:
|
| 27 |
+
from app.core.canvas_database import setup_canvas_collections
|
| 28 |
|
| 29 |
+
await setup_canvas_collections(_get_database())
|
| 30 |
+
logger.info("Canvas database initialization completed (no-op on SQLite shim).")
|
| 31 |
+
except Exception as canvas_error: # pragma: no cover
|
| 32 |
+
logger.error(f"Canvas database initialization failed: {canvas_error}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
+
async def close_mongo_connection() -> None:
|
| 36 |
+
"""Best-effort connection close on FastAPI shutdown."""
|
| 37 |
+
await _close()
|
| 38 |
+
logger.info("Disconnected from SQLite-backed database.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
async def create_indexes() -> None:
|
| 42 |
+
"""Index creation is handled by the shim's schema bootstrap; kept for
|
| 43 |
+
backwards compatibility so any out-of-process maintenance script that
|
| 44 |
+
imports this name keeps working."""
|
| 45 |
+
return None
|
| 46 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
def get_database():
|
| 49 |
+
"""Return the SQLite-backed Mongo-API-compatible Database object."""
|
| 50 |
+
return _get_database()
|
multi_llm_chatbot_backend/app/core/improved_orchestrator.py
CHANGED
|
@@ -6,6 +6,7 @@ from app.core.rag_manager import get_rag_manager
|
|
| 6 |
from app.config import get_settings
|
| 7 |
from app.llm.llm_client import LLMClient, ToolCallResult
|
| 8 |
from app.tools import get_tool_definitions, get_tool_executor
|
|
|
|
| 9 |
|
| 10 |
import json
|
| 11 |
import logging
|
|
@@ -61,15 +62,13 @@ class ImprovedChatOrchestrator:
|
|
| 61 |
return ToolCallResult(text="", used_tool=False)
|
| 62 |
|
| 63 |
system_prompt = (
|
| 64 |
-
"You are a helpful assistant with access to external tools. "
|
| 65 |
-
"Use the available tools when the user's question can be answered "
|
| 66 |
-
"
|
| 67 |
-
"
|
| 68 |
-
"
|
| 69 |
-
"
|
| 70 |
-
"
|
| 71 |
-
"Format your responses using markdown. Use bullet points "
|
| 72 |
-
"to present structured data like course listings or professor ratings."
|
| 73 |
)
|
| 74 |
|
| 75 |
return await self.llm_client.generate_with_tools(
|
|
@@ -687,9 +686,14 @@ When analyzing the document context:
|
|
| 687 |
"""
|
| 688 |
enhanced_context = []
|
| 689 |
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
# Check if we actually have meaningful document content
|
| 694 |
has_documents = bool(document_context and document_context.strip() and len(document_context.strip()) > 50)
|
| 695 |
|
|
@@ -711,11 +715,6 @@ When analyzing the document context:
|
|
| 711 |
|
| 712 |
Always cite your sources when referencing information from their documents using the format: "According to your [document_name]..." or "In your [section_name] from [document_name]..."
|
| 713 |
"""
|
| 714 |
-
|
| 715 |
-
enhanced_context.append({
|
| 716 |
-
"role": "system",
|
| 717 |
-
"content": system_message
|
| 718 |
-
})
|
| 719 |
else:
|
| 720 |
# NO DOCUMENTS - Explicitly tell persona not to reference documents
|
| 721 |
system_message = f"""{persona.system_prompt}
|
|
@@ -728,19 +727,58 @@ When analyzing the document context:
|
|
| 728 |
3. Provide general guidance based on best practices in your area of expertise
|
| 729 |
|
| 730 |
Do NOT make up document names or pretend to have access to files that don't exist."""
|
| 731 |
-
|
| 732 |
-
enhanced_context.append({
|
| 733 |
-
"role": "system",
|
| 734 |
-
"content": system_message
|
| 735 |
-
})
|
| 736 |
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 740 |
enhanced_context.append({
|
| 741 |
-
"role": message[
|
| 742 |
-
"content": message[
|
| 743 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
|
| 745 |
return enhanced_context
|
| 746 |
|
|
@@ -871,10 +909,19 @@ When analyzing the document context:
|
|
| 871 |
}
|
| 872 |
|
| 873 |
|
| 874 |
-
async def get_top_personas(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 875 |
"""
|
| 876 |
Use the LLM to rank personas based on current session context.
|
| 877 |
Falls back to default persona order if LLM fails or returns invalid data.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
"""
|
| 879 |
try:
|
| 880 |
session = self.session_manager.get_session(session_id)
|
|
@@ -883,22 +930,33 @@ When analyzing the document context:
|
|
| 883 |
logger.warning("No personas registered.")
|
| 884 |
return []
|
| 885 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 886 |
# Use the LLM from one of the existing persona objects
|
| 887 |
-
llm = next(iter(
|
| 888 |
|
| 889 |
# Use recent conversation context (last 5 messages)
|
| 890 |
recent_context = "\n".join(
|
| 891 |
msg['content'] for msg in session.get_recent_messages(5)
|
| 892 |
)
|
| 893 |
|
| 894 |
-
# Format available persona descriptions
|
| 895 |
persona_descriptions = "\n".join([
|
| 896 |
f"- ID: {p.id}\n Name: {p.name}\n Prompt: {p.system_prompt.strip()}"
|
| 897 |
-
for p in
|
| 898 |
])
|
| 899 |
|
| 900 |
-
# Ensure k does not exceed the number of
|
| 901 |
-
k = min(k, len(
|
| 902 |
|
| 903 |
app_title = get_settings().app.title
|
| 904 |
|
|
@@ -935,15 +993,19 @@ When analyzing the document context:
|
|
| 935 |
if isinstance(top_ids, dict):
|
| 936 |
top_ids = next(iter(top_ids.values()), [])
|
| 937 |
|
| 938 |
-
# Step 3: Filter valid persona IDs
|
| 939 |
-
valid_ids = [pid for pid in top_ids if pid in
|
| 940 |
|
| 941 |
if len(valid_ids) < k:
|
| 942 |
logger.warning(f"LLM returned insufficient or invalid IDs. Got: {valid_ids}")
|
| 943 |
-
return list(
|
| 944 |
|
| 945 |
return valid_ids[:k]
|
| 946 |
|
| 947 |
except Exception as e:
|
| 948 |
logger.error(f"Error selecting top personas: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 949 |
return list(self.personas.keys())[:k]
|
|
|
|
| 6 |
from app.config import get_settings
|
| 7 |
from app.llm.llm_client import LLMClient, ToolCallResult
|
| 8 |
from app.tools import get_tool_definitions, get_tool_executor
|
| 9 |
+
from app.utils.chat_summary import generate_conversation_context_summary
|
| 10 |
|
| 11 |
import json
|
| 12 |
import logging
|
|
|
|
| 62 |
return ToolCallResult(text="", used_tool=False)
|
| 63 |
|
| 64 |
system_prompt = (
|
| 65 |
+
"You are a helpful cybersecurity assistant with access to external tools. "
|
| 66 |
+
"Use the available tools when the user's question can be answered by one of them. "
|
| 67 |
+
"Call get_current_datetime when the user asks about today, deadlines, timelines, "
|
| 68 |
+
"schedules, incident timing, or when accurate date/time context would improve "
|
| 69 |
+
"your guidance — then weave the result into your answer. "
|
| 70 |
+
"If no tool is relevant, respond with a brief text answer. "
|
| 71 |
+
"Format your responses using markdown."
|
|
|
|
|
|
|
| 72 |
)
|
| 73 |
|
| 74 |
return await self.llm_client.generate_with_tools(
|
|
|
|
| 686 |
"""
|
| 687 |
enhanced_context = []
|
| 688 |
|
| 689 |
+
conversation_messages = [
|
| 690 |
+
m for m in session.messages if m.get("role") != "system"
|
| 691 |
+
]
|
| 692 |
+
history_tokens = self.context_manager._estimate_tokens_for_messages(
|
| 693 |
+
conversation_messages
|
| 694 |
+
)
|
| 695 |
+
threshold = get_settings().orchestrator.conversation_history_token_threshold
|
| 696 |
+
|
| 697 |
# Check if we actually have meaningful document content
|
| 698 |
has_documents = bool(document_context and document_context.strip() and len(document_context.strip()) > 50)
|
| 699 |
|
|
|
|
| 715 |
|
| 716 |
Always cite your sources when referencing information from their documents using the format: "According to your [document_name]..." or "In your [section_name] from [document_name]..."
|
| 717 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
else:
|
| 719 |
# NO DOCUMENTS - Explicitly tell persona not to reference documents
|
| 720 |
system_message = f"""{persona.system_prompt}
|
|
|
|
| 727 |
3. Provide general guidance based on best practices in your area of expertise
|
| 728 |
|
| 729 |
Do NOT make up document names or pretend to have access to files that don't exist."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
|
| 731 |
+
if hasattr(session, "user_profile_context") and session.user_profile_context:
|
| 732 |
+
system_message += (
|
| 733 |
+
f"\n\n{session.user_profile_context}\n"
|
| 734 |
+
"Use this background to calibrate technical depth, examples, and priorities."
|
| 735 |
+
)
|
| 736 |
+
|
| 737 |
+
enhanced_context.append({
|
| 738 |
+
"role": "system",
|
| 739 |
+
"content": system_message,
|
| 740 |
+
})
|
| 741 |
+
|
| 742 |
+
if history_tokens < threshold:
|
| 743 |
+
for message in conversation_messages:
|
| 744 |
enhanced_context.append({
|
| 745 |
+
"role": message["role"],
|
| 746 |
+
"content": message["content"],
|
| 747 |
})
|
| 748 |
+
else:
|
| 749 |
+
msg_count = len(conversation_messages)
|
| 750 |
+
if (
|
| 751 |
+
session.conversation_summary
|
| 752 |
+
and session.conversation_summary_message_count == msg_count
|
| 753 |
+
):
|
| 754 |
+
summary = session.conversation_summary
|
| 755 |
+
else:
|
| 756 |
+
llm = self.llm_client
|
| 757 |
+
if llm is None and self.personas:
|
| 758 |
+
llm = next(iter(self.personas.values())).llm
|
| 759 |
+
persona_names = {p.id: p.name for p in self.personas.values()}
|
| 760 |
+
summary = ""
|
| 761 |
+
if llm is not None:
|
| 762 |
+
summary = await generate_conversation_context_summary(
|
| 763 |
+
conversation_messages,
|
| 764 |
+
llm,
|
| 765 |
+
persona_names=persona_names,
|
| 766 |
+
)
|
| 767 |
+
if summary:
|
| 768 |
+
session.conversation_summary = summary
|
| 769 |
+
session.conversation_summary_message_count = msg_count
|
| 770 |
+
system_message += f"\n\nSummary of earlier conversation:\n{summary}"
|
| 771 |
+
enhanced_context[0]["content"] = system_message
|
| 772 |
+
else:
|
| 773 |
+
logger.warning(
|
| 774 |
+
"Summary generation failed; including full history (%d tokens)",
|
| 775 |
+
history_tokens,
|
| 776 |
+
)
|
| 777 |
+
for message in conversation_messages:
|
| 778 |
+
enhanced_context.append({
|
| 779 |
+
"role": message["role"],
|
| 780 |
+
"content": message["content"],
|
| 781 |
+
})
|
| 782 |
|
| 783 |
return enhanced_context
|
| 784 |
|
|
|
|
| 909 |
}
|
| 910 |
|
| 911 |
|
| 912 |
+
async def get_top_personas(
|
| 913 |
+
self,
|
| 914 |
+
session_id: str,
|
| 915 |
+
k: int = 3,
|
| 916 |
+
candidate_ids: Optional[List[str]] = None,
|
| 917 |
+
) -> List[str]:
|
| 918 |
"""
|
| 919 |
Use the LLM to rank personas based on current session context.
|
| 920 |
Falls back to default persona order if LLM fails or returns invalid data.
|
| 921 |
+
|
| 922 |
+
When ``candidate_ids`` is provided (e.g., from the header's advisor
|
| 923 |
+
selection dropdown) ranking is restricted to that pool so users only
|
| 924 |
+
receive responses from advisors they've enabled.
|
| 925 |
"""
|
| 926 |
try:
|
| 927 |
session = self.session_manager.get_session(session_id)
|
|
|
|
| 930 |
logger.warning("No personas registered.")
|
| 931 |
return []
|
| 932 |
|
| 933 |
+
if candidate_ids:
|
| 934 |
+
candidate_personas = {
|
| 935 |
+
pid: self.personas[pid]
|
| 936 |
+
for pid in candidate_ids
|
| 937 |
+
if pid in self.personas
|
| 938 |
+
}
|
| 939 |
+
if not candidate_personas:
|
| 940 |
+
candidate_personas = dict(self.personas)
|
| 941 |
+
else:
|
| 942 |
+
candidate_personas = dict(self.personas)
|
| 943 |
+
|
| 944 |
# Use the LLM from one of the existing persona objects
|
| 945 |
+
llm = next(iter(candidate_personas.values())).llm
|
| 946 |
|
| 947 |
# Use recent conversation context (last 5 messages)
|
| 948 |
recent_context = "\n".join(
|
| 949 |
msg['content'] for msg in session.get_recent_messages(5)
|
| 950 |
)
|
| 951 |
|
| 952 |
+
# Format available persona descriptions (only the candidate pool)
|
| 953 |
persona_descriptions = "\n".join([
|
| 954 |
f"- ID: {p.id}\n Name: {p.name}\n Prompt: {p.system_prompt.strip()}"
|
| 955 |
+
for p in candidate_personas.values()
|
| 956 |
])
|
| 957 |
|
| 958 |
+
# Ensure k does not exceed the number of candidate personas
|
| 959 |
+
k = min(k, len(candidate_personas))
|
| 960 |
|
| 961 |
app_title = get_settings().app.title
|
| 962 |
|
|
|
|
| 993 |
if isinstance(top_ids, dict):
|
| 994 |
top_ids = next(iter(top_ids.values()), [])
|
| 995 |
|
| 996 |
+
# Step 3: Filter valid persona IDs against the candidate pool
|
| 997 |
+
valid_ids = [pid for pid in top_ids if pid in candidate_personas]
|
| 998 |
|
| 999 |
if len(valid_ids) < k:
|
| 1000 |
logger.warning(f"LLM returned insufficient or invalid IDs. Got: {valid_ids}")
|
| 1001 |
+
return list(candidate_personas.keys())[:k]
|
| 1002 |
|
| 1003 |
return valid_ids[:k]
|
| 1004 |
|
| 1005 |
except Exception as e:
|
| 1006 |
logger.error(f"Error selecting top personas: {e}")
|
| 1007 |
+
if candidate_ids:
|
| 1008 |
+
fallback_ids = [pid for pid in candidate_ids if pid in self.personas]
|
| 1009 |
+
if fallback_ids:
|
| 1010 |
+
return fallback_ids[:k]
|
| 1011 |
return list(self.personas.keys())[:k]
|
multi_llm_chatbot_backend/app/core/rag_manager.py
CHANGED
|
@@ -12,6 +12,20 @@ from pathlib import Path
|
|
| 12 |
|
| 13 |
logger = logging.getLogger(__name__)
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# Download required NLTK data
|
| 16 |
try:
|
| 17 |
nltk.data.find('tokenizers/punkt')
|
|
@@ -177,14 +191,16 @@ class RAGManager:
|
|
| 177 |
Handles document storage, embedding, and retrieval using ChromaDB
|
| 178 |
"""
|
| 179 |
|
| 180 |
-
def __init__(self, embedding_model: str = None, persist_directory: str =
|
| 181 |
from app.config import get_settings
|
| 182 |
settings = get_settings()
|
| 183 |
if embedding_model is None:
|
| 184 |
embedding_model = settings.rag.embedding_model
|
| 185 |
self.embedding_model_name = embedding_model
|
|
|
|
|
|
|
| 186 |
self.persist_directory = Path(persist_directory)
|
| 187 |
-
self.persist_directory.mkdir(exist_ok=True)
|
| 188 |
|
| 189 |
# Initialize embedding model
|
| 190 |
logger.info(f"Loading embedding model: {embedding_model}")
|
|
@@ -459,13 +475,15 @@ class RAGManager:
|
|
| 459 |
|
| 460 |
|
| 461 |
class EnhancedRAGManager:
|
| 462 |
-
def __init__(self, persist_directory: str =
|
| 463 |
"""Initialize enhanced RAG manager with improved document handling"""
|
| 464 |
from app.config import get_settings
|
| 465 |
settings = get_settings()
|
| 466 |
|
|
|
|
|
|
|
| 467 |
self.persist_directory = persist_directory
|
| 468 |
-
Path(persist_directory).mkdir(exist_ok=True)
|
| 469 |
|
| 470 |
# Initialize ChromaDB client
|
| 471 |
self.client = chromadb.PersistentClient(
|
|
|
|
| 12 |
|
| 13 |
logger = logging.getLogger(__name__)
|
| 14 |
|
| 15 |
+
|
| 16 |
+
def _default_chroma_path(subdir: str) -> str:
|
| 17 |
+
"""Resolve the default ChromaDB persistence directory.
|
| 18 |
+
|
| 19 |
+
On HF Spaces (and any deployment that sets ``DATA_DIR``) the embeddings
|
| 20 |
+
must live on the bucket mount so they survive Space rebuilds. In local
|
| 21 |
+
dev DATA_DIR is unset, so we keep the historical working-directory path
|
| 22 |
+
so existing local installs and tests don't move their data.
|
| 23 |
+
"""
|
| 24 |
+
data_dir = os.environ.get("DATA_DIR", "").strip()
|
| 25 |
+
if data_dir:
|
| 26 |
+
return str(Path(data_dir) / "chroma" / subdir)
|
| 27 |
+
return f"./{subdir}"
|
| 28 |
+
|
| 29 |
# Download required NLTK data
|
| 30 |
try:
|
| 31 |
nltk.data.find('tokenizers/punkt')
|
|
|
|
| 191 |
Handles document storage, embedding, and retrieval using ChromaDB
|
| 192 |
"""
|
| 193 |
|
| 194 |
+
def __init__(self, embedding_model: str = None, persist_directory: str = None):
|
| 195 |
from app.config import get_settings
|
| 196 |
settings = get_settings()
|
| 197 |
if embedding_model is None:
|
| 198 |
embedding_model = settings.rag.embedding_model
|
| 199 |
self.embedding_model_name = embedding_model
|
| 200 |
+
if persist_directory is None:
|
| 201 |
+
persist_directory = _default_chroma_path("chroma_db")
|
| 202 |
self.persist_directory = Path(persist_directory)
|
| 203 |
+
self.persist_directory.mkdir(parents=True, exist_ok=True)
|
| 204 |
|
| 205 |
# Initialize embedding model
|
| 206 |
logger.info(f"Loading embedding model: {embedding_model}")
|
|
|
|
| 475 |
|
| 476 |
|
| 477 |
class EnhancedRAGManager:
|
| 478 |
+
def __init__(self, persist_directory: str = None):
|
| 479 |
"""Initialize enhanced RAG manager with improved document handling"""
|
| 480 |
from app.config import get_settings
|
| 481 |
settings = get_settings()
|
| 482 |
|
| 483 |
+
if persist_directory is None:
|
| 484 |
+
persist_directory = _default_chroma_path("chromadb_storage")
|
| 485 |
self.persist_directory = persist_directory
|
| 486 |
+
Path(persist_directory).mkdir(parents=True, exist_ok=True)
|
| 487 |
|
| 488 |
# Initialize ChromaDB client
|
| 489 |
self.client = chromadb.PersistentClient(
|
multi_llm_chatbot_backend/app/main.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
import os
|
| 2 |
-
from
|
| 3 |
|
| 4 |
-
|
| 5 |
|
| 6 |
-
|
| 7 |
|
| 8 |
from fastapi import FastAPI
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -12,7 +12,6 @@ from contextlib import asynccontextmanager
|
|
| 12 |
|
| 13 |
# Load configuration FIRST so every module can use it
|
| 14 |
from app.config import load_settings
|
| 15 |
-
from app.version import __version__
|
| 16 |
settings = load_settings()
|
| 17 |
|
| 18 |
# Import the new database functions
|
|
@@ -23,6 +22,8 @@ from app.api.routes import router as main_router
|
|
| 23 |
from app.api.routes.auth import router as auth_router
|
| 24 |
from app.api.routes.chat_sessions import router as chat_sessions_router
|
| 25 |
from app.api.routes.phd_canvas import router as phd_canvas_router
|
|
|
|
|
|
|
| 26 |
|
| 27 |
import logging
|
| 28 |
|
|
@@ -41,7 +42,7 @@ async def lifespan(app: FastAPI):
|
|
| 41 |
|
| 42 |
app = FastAPI(
|
| 43 |
title=f"{settings.app.title} Backend",
|
| 44 |
-
version=
|
| 45 |
lifespan=lifespan
|
| 46 |
)
|
| 47 |
|
|
@@ -61,6 +62,8 @@ app.include_router(main_router)
|
|
| 61 |
app.include_router(auth_router, prefix="/auth", tags=["authentication"])
|
| 62 |
app.include_router(chat_sessions_router, prefix="/api", tags=["chat-sessions"])
|
| 63 |
app.include_router(phd_canvas_router, prefix="/api", tags=["phd-canvas"])
|
|
|
|
|
|
|
| 64 |
|
| 65 |
# Serve bundled avatar images
|
| 66 |
_avatars_dir = Path(__file__).resolve().parent / "assets" / "avatars"
|
|
@@ -80,17 +83,34 @@ def get_public_config():
|
|
| 80 |
"""Return the public (non-secret) application configuration."""
|
| 81 |
return settings.get_frontend_config()
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
|
| 4 |
+
from app.core.env_loader import load_application_env
|
| 5 |
|
| 6 |
+
load_application_env()
|
| 7 |
|
| 8 |
from fastapi import FastAPI
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 12 |
|
| 13 |
# Load configuration FIRST so every module can use it
|
| 14 |
from app.config import load_settings
|
|
|
|
| 15 |
settings = load_settings()
|
| 16 |
|
| 17 |
# Import the new database functions
|
|
|
|
| 22 |
from app.api.routes.auth import router as auth_router
|
| 23 |
from app.api.routes.chat_sessions import router as chat_sessions_router
|
| 24 |
from app.api.routes.phd_canvas import router as phd_canvas_router
|
| 25 |
+
from app.api.routes.user_profile import router as user_profile_router
|
| 26 |
+
from app.api.routes.onboarding import router as onboarding_router
|
| 27 |
|
| 28 |
import logging
|
| 29 |
|
|
|
|
| 42 |
|
| 43 |
app = FastAPI(
|
| 44 |
title=f"{settings.app.title} Backend",
|
| 45 |
+
version="2.0.0",
|
| 46 |
lifespan=lifespan
|
| 47 |
)
|
| 48 |
|
|
|
|
| 62 |
app.include_router(auth_router, prefix="/auth", tags=["authentication"])
|
| 63 |
app.include_router(chat_sessions_router, prefix="/api", tags=["chat-sessions"])
|
| 64 |
app.include_router(phd_canvas_router, prefix="/api", tags=["phd-canvas"])
|
| 65 |
+
app.include_router(user_profile_router, prefix="/api", tags=["user-profile"])
|
| 66 |
+
app.include_router(onboarding_router, prefix="/api", tags=["onboarding"])
|
| 67 |
|
| 68 |
# Serve bundled avatar images
|
| 69 |
_avatars_dir = Path(__file__).resolve().parent / "assets" / "avatars"
|
|
|
|
| 83 |
"""Return the public (non-secret) application configuration."""
|
| 84 |
return settings.get_frontend_config()
|
| 85 |
|
| 86 |
+
|
| 87 |
+
# ---------------------------------------------------------------------------
|
| 88 |
+
# Static SPA mount — Hugging Face Spaces / single-container deployment
|
| 89 |
+
# ---------------------------------------------------------------------------
|
| 90 |
+
# When the Docker build copies the React production bundle into ``./static``
|
| 91 |
+
# (sibling of this app/ directory), expose it at "/" so the API and the
|
| 92 |
+
# SPA share the FastAPI origin. In local development the static dir is
|
| 93 |
+
# absent and a JSON banner is returned at "/" instead.
|
| 94 |
+
_static_dir = Path(__file__).resolve().parent.parent / "static"
|
| 95 |
+
_should_mount_static = _static_dir.is_dir()
|
| 96 |
+
|
| 97 |
+
if _should_mount_static:
|
| 98 |
+
app.mount(
|
| 99 |
+
"/",
|
| 100 |
+
StaticFiles(directory=str(_static_dir), html=True),
|
| 101 |
+
name="spa",
|
| 102 |
+
)
|
| 103 |
+
else:
|
| 104 |
+
@app.get("/")
|
| 105 |
+
def root():
|
| 106 |
+
return {
|
| 107 |
+
"message": f"{settings.app.title} Backend",
|
| 108 |
+
"version": "2.0.0",
|
| 109 |
+
"features": [
|
| 110 |
+
"User Authentication",
|
| 111 |
+
"Persistent Chat Sessions (SQLite via aiosqlite)",
|
| 112 |
+
"Ollama Support",
|
| 113 |
+
"Gemini API Support",
|
| 114 |
+
"Configurable Personas",
|
| 115 |
+
],
|
| 116 |
+
}
|
multi_llm_chatbot_backend/app/utils/chroma_client.py
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import chromadb
|
| 2 |
from chromadb.config import Settings
|
|
|
|
| 3 |
from app.llm.embedding_client import get_embedding
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
collection = client.get_or_create_collection("persona_knowledge")
|
| 8 |
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
import chromadb
|
| 5 |
from chromadb.config import Settings
|
| 6 |
+
|
| 7 |
from app.llm.embedding_client import get_embedding
|
| 8 |
|
| 9 |
+
|
| 10 |
+
def _persona_knowledge_path() -> str:
|
| 11 |
+
"""Resolve the persona-knowledge ChromaDB path.
|
| 12 |
+
|
| 13 |
+
Mirrors ``app.core.rag_manager._default_chroma_path``: when ``DATA_DIR``
|
| 14 |
+
is set (HF Spaces, any bucket-mounted deployment) the embeddings live
|
| 15 |
+
under ``${DATA_DIR}/chroma/persona_knowledge`` so they survive Space
|
| 16 |
+
rebuilds. Local installs keep the historical relative path.
|
| 17 |
+
"""
|
| 18 |
+
data_dir = os.environ.get("DATA_DIR", "").strip()
|
| 19 |
+
if data_dir:
|
| 20 |
+
return str(Path(data_dir) / "chroma" / "persona_knowledge")
|
| 21 |
+
return "./chroma_storage"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
_path = _persona_knowledge_path()
|
| 25 |
+
Path(_path).mkdir(parents=True, exist_ok=True)
|
| 26 |
+
client = chromadb.PersistentClient(path=_path)
|
| 27 |
|
| 28 |
collection = client.get_or_create_collection("persona_knowledge")
|
| 29 |
|
multi_llm_chatbot_backend/requirements.txt
CHANGED
|
@@ -6,6 +6,7 @@ python-multipart~=0.0
|
|
| 6 |
# HTTP client for LLM APIs
|
| 7 |
httpx~=0.28
|
| 8 |
openai~=2.30
|
|
|
|
| 9 |
|
| 10 |
# Document processing
|
| 11 |
PyPDF2~=3.0
|
|
@@ -31,9 +32,11 @@ markdown~=3.0
|
|
| 31 |
# PDF generation and export
|
| 32 |
reportlab~=4.4
|
| 33 |
|
| 34 |
-
#
|
|
|
|
|
|
|
|
|
|
| 35 |
pymongo~=4.16
|
| 36 |
-
motor~=3.7
|
| 37 |
|
| 38 |
# Authentication and security
|
| 39 |
passlib[bcrypt]~=1.7
|
|
|
|
| 6 |
# HTTP client for LLM APIs
|
| 7 |
httpx~=0.28
|
| 8 |
openai~=2.30
|
| 9 |
+
huggingface-hub~=0.36
|
| 10 |
|
| 11 |
# Document processing
|
| 12 |
PyPDF2~=3.0
|
|
|
|
| 32 |
# PDF generation and export
|
| 33 |
reportlab~=4.4
|
| 34 |
|
| 35 |
+
# Persistence: SQLite via aiosqlite on the HF Storage Bucket mount (no MongoDB).
|
| 36 |
+
# pymongo is kept *only* for bson.ObjectId and the DuplicateKeyError /
|
| 37 |
+
# PyMongoError types that the existing routers and the SQLite shim raise.
|
| 38 |
+
aiosqlite~=0.20
|
| 39 |
pymongo~=4.16
|
|
|
|
| 40 |
|
| 41 |
# Authentication and security
|
| 42 |
passlib[bcrypt]~=1.7
|
phd-advisor-frontend/package-lock.json
CHANGED
|
@@ -8,17 +8,22 @@
|
|
| 8 |
"name": "phd-advisor-frontend",
|
| 9 |
"version": "0.1.0",
|
| 10 |
"dependencies": {
|
| 11 |
-
"@
|
| 12 |
"@testing-library/dom": "^10.4.0",
|
| 13 |
"@testing-library/jest-dom": "^6.6.3",
|
| 14 |
"@testing-library/react": "^16.3.0",
|
| 15 |
"@testing-library/user-event": "^13.5.0",
|
|
|
|
|
|
|
|
|
|
| 16 |
"lucide-react": "^0.544.0",
|
| 17 |
"react": "^19.1.0",
|
| 18 |
"react-dom": "^19.1.0",
|
| 19 |
"react-markdown": "^10.1.0",
|
| 20 |
"react-scripts": "5.0.1",
|
|
|
|
| 21 |
"remark-gfm": "^4.0.1",
|
|
|
|
| 22 |
"web-vitals": "^2.1.4"
|
| 23 |
}
|
| 24 |
},
|
|
@@ -2069,6 +2074,108 @@
|
|
| 2069 |
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
| 2070 |
"license": "MIT"
|
| 2071 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2072 |
"node_modules/@csstools/normalize.css": {
|
| 2073 |
"version": "12.1.1",
|
| 2074 |
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz",
|
|
@@ -2967,6 +3074,36 @@
|
|
| 2967 |
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
|
| 2968 |
"license": "MIT"
|
| 2969 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2970 |
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
| 2971 |
"version": "5.1.1-v1",
|
| 2972 |
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
|
@@ -3033,6 +3170,12 @@
|
|
| 3033 |
"node": ">= 8"
|
| 3034 |
}
|
| 3035 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3036 |
"node_modules/@pkgjs/parseargs": {
|
| 3037 |
"version": "0.11.0",
|
| 3038 |
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
|
@@ -3091,57 +3234,6 @@
|
|
| 3091 |
}
|
| 3092 |
}
|
| 3093 |
},
|
| 3094 |
-
"node_modules/@reactour/mask": {
|
| 3095 |
-
"version": "1.2.0",
|
| 3096 |
-
"resolved": "https://registry.npmjs.org/@reactour/mask/-/mask-1.2.0.tgz",
|
| 3097 |
-
"integrity": "sha512-XLgBLWfKJybtZjNTSO5lt/SIvRlCZBadB6JfE/hO1ErqURRjYhnv+edC0Ki1haUCqMGFppWk3lwcPCjmK0xNog==",
|
| 3098 |
-
"license": "MIT",
|
| 3099 |
-
"dependencies": {
|
| 3100 |
-
"@reactour/utils": "*"
|
| 3101 |
-
},
|
| 3102 |
-
"peerDependencies": {
|
| 3103 |
-
"react": "16.x || 17.x || 18.x || 19.x"
|
| 3104 |
-
}
|
| 3105 |
-
},
|
| 3106 |
-
"node_modules/@reactour/popover": {
|
| 3107 |
-
"version": "1.3.0",
|
| 3108 |
-
"resolved": "https://registry.npmjs.org/@reactour/popover/-/popover-1.3.0.tgz",
|
| 3109 |
-
"integrity": "sha512-YdyjSmHPvEeQEcJM4gcGFa5pI/Yf4nZGqwG4JnT+rK1SyUJBIPnm4Gkl/h7/+1g0KCFMkwNwagS3ZiXvZB7ThA==",
|
| 3110 |
-
"license": "MIT",
|
| 3111 |
-
"dependencies": {
|
| 3112 |
-
"@reactour/utils": "*"
|
| 3113 |
-
},
|
| 3114 |
-
"peerDependencies": {
|
| 3115 |
-
"react": "16.x || 17.x || 18.x || 19.x"
|
| 3116 |
-
}
|
| 3117 |
-
},
|
| 3118 |
-
"node_modules/@reactour/tour": {
|
| 3119 |
-
"version": "3.8.0",
|
| 3120 |
-
"resolved": "https://registry.npmjs.org/@reactour/tour/-/tour-3.8.0.tgz",
|
| 3121 |
-
"integrity": "sha512-KZTFi1pAvoTVKKRdBN5+XCYxXBp4k4Ql/acZcXyPvec8VU24fkMSEeV+v8krfYQpoVcewxIu3gM6xWZZLjxi7w==",
|
| 3122 |
-
"license": "MIT",
|
| 3123 |
-
"dependencies": {
|
| 3124 |
-
"@reactour/mask": "*",
|
| 3125 |
-
"@reactour/popover": "*",
|
| 3126 |
-
"@reactour/utils": "*"
|
| 3127 |
-
},
|
| 3128 |
-
"peerDependencies": {
|
| 3129 |
-
"react": "16.x || 17.x || 18.x || 19.x"
|
| 3130 |
-
}
|
| 3131 |
-
},
|
| 3132 |
-
"node_modules/@reactour/utils": {
|
| 3133 |
-
"version": "0.6.0",
|
| 3134 |
-
"resolved": "https://registry.npmjs.org/@reactour/utils/-/utils-0.6.0.tgz",
|
| 3135 |
-
"integrity": "sha512-GqaLjQi7MJsgtAKjdiw2Eak1toFkADoLRnm1+HZpaD+yl+DkaHpC1N7JAl+kVOO5I17bWInPA+OFbXjO9Co8Qg==",
|
| 3136 |
-
"license": "MIT",
|
| 3137 |
-
"dependencies": {
|
| 3138 |
-
"@rooks/use-mutation-observer": "^4.11.2",
|
| 3139 |
-
"resize-observer-polyfill": "^1.5.1"
|
| 3140 |
-
},
|
| 3141 |
-
"peerDependencies": {
|
| 3142 |
-
"react": "16.x || 17.x || 18.x || 19.x"
|
| 3143 |
-
}
|
| 3144 |
-
},
|
| 3145 |
"node_modules/@rollup/plugin-babel": {
|
| 3146 |
"version": "5.3.1",
|
| 3147 |
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
|
|
@@ -3221,15 +3313,6 @@
|
|
| 3221 |
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
|
| 3222 |
"license": "MIT"
|
| 3223 |
},
|
| 3224 |
-
"node_modules/@rooks/use-mutation-observer": {
|
| 3225 |
-
"version": "4.11.2",
|
| 3226 |
-
"resolved": "https://registry.npmjs.org/@rooks/use-mutation-observer/-/use-mutation-observer-4.11.2.tgz",
|
| 3227 |
-
"integrity": "sha512-vpsdrZdr6TkB1zZJcHx+fR1YC/pHs2BaqcuYiEGjBVbwY5xcC49+h0hAUtQKHth3oJqXfIX/Ng8S7s5HFHdM/A==",
|
| 3228 |
-
"license": "MIT",
|
| 3229 |
-
"peerDependencies": {
|
| 3230 |
-
"react": ">=16.8.0"
|
| 3231 |
-
}
|
| 3232 |
-
},
|
| 3233 |
"node_modules/@rtsao/scc": {
|
| 3234 |
"version": "1.1.0",
|
| 3235 |
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
|
@@ -3499,6 +3582,15 @@
|
|
| 3499 |
"url": "https://github.com/sponsors/gregberge"
|
| 3500 |
}
|
| 3501 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3502 |
"node_modules/@testing-library/dom": {
|
| 3503 |
"version": "10.4.0",
|
| 3504 |
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
|
@@ -3858,6 +3950,12 @@
|
|
| 3858 |
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
| 3859 |
"license": "MIT"
|
| 3860 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3861 |
"node_modules/@types/mdast": {
|
| 3862 |
"version": "4.0.4",
|
| 3863 |
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
|
@@ -3927,6 +4025,16 @@
|
|
| 3927 |
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
| 3928 |
"license": "MIT"
|
| 3929 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3930 |
"node_modules/@types/resolve": {
|
| 3931 |
"version": "1.17.1",
|
| 3932 |
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
|
@@ -4258,6 +4366,59 @@
|
|
| 4258 |
"url": "https://opencollective.com/typescript-eslint"
|
| 4259 |
}
|
| 4260 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4261 |
"node_modules/@ungap/structured-clone": {
|
| 4262 |
"version": "1.3.0",
|
| 4263 |
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
|
@@ -4429,6 +4590,15 @@
|
|
| 4429 |
"deprecated": "Use your platform's native atob() and btoa() methods instead",
|
| 4430 |
"license": "BSD-3-Clause"
|
| 4431 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4432 |
"node_modules/accepts": {
|
| 4433 |
"version": "1.3.8",
|
| 4434 |
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
|
@@ -5278,6 +5448,26 @@
|
|
| 5278 |
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
| 5279 |
"license": "MIT"
|
| 5280 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5281 |
"node_modules/batch": {
|
| 5282 |
"version": "0.6.1",
|
| 5283 |
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
|
@@ -5416,6 +5606,15 @@
|
|
| 5416 |
"node": ">=8"
|
| 5417 |
}
|
| 5418 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5419 |
"node_modules/browser-process-hrtime": {
|
| 5420 |
"version": "1.0.0",
|
| 5421 |
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
|
|
@@ -5797,6 +5996,15 @@
|
|
| 5797 |
"wrap-ansi": "^7.0.0"
|
| 5798 |
}
|
| 5799 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5800 |
"node_modules/co": {
|
| 5801 |
"version": "4.6.0",
|
| 5802 |
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
|
@@ -5892,6 +6100,21 @@
|
|
| 5892 |
"node": ">=4"
|
| 5893 |
}
|
| 5894 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5895 |
"node_modules/collect-v8-coverage": {
|
| 5896 |
"version": "1.0.2",
|
| 5897 |
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
|
|
@@ -6025,6 +6248,16 @@
|
|
| 6025 |
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
| 6026 |
"license": "MIT"
|
| 6027 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6028 |
"node_modules/confusing-browser-globals": {
|
| 6029 |
"version": "1.0.11",
|
| 6030 |
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
|
|
@@ -6139,6 +6372,12 @@
|
|
| 6139 |
"node": ">=10"
|
| 6140 |
}
|
| 6141 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6142 |
"node_modules/cross-spawn": {
|
| 6143 |
"version": "7.0.6",
|
| 6144 |
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
|
@@ -6537,6 +6776,13 @@
|
|
| 6537 |
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
| 6538 |
"license": "MIT"
|
| 6539 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6540 |
"node_modules/damerau-levenshtein": {
|
| 6541 |
"version": "1.0.8",
|
| 6542 |
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
|
@@ -6817,6 +7063,12 @@
|
|
| 6817 |
"url": "https://github.com/sponsors/wooorm"
|
| 6818 |
}
|
| 6819 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6820 |
"node_modules/didyoumean": {
|
| 6821 |
"version": "1.2.2",
|
| 6822 |
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
|
@@ -7017,6 +7269,57 @@
|
|
| 7017 |
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
| 7018 |
"license": "MIT"
|
| 7019 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7020 |
"node_modules/ee-first": {
|
| 7021 |
"version": "1.1.1",
|
| 7022 |
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
|
@@ -8438,6 +8741,23 @@
|
|
| 8438 |
}
|
| 8439 |
}
|
| 8440 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8441 |
"node_modules/for-each": {
|
| 8442 |
"version": "0.3.5",
|
| 8443 |
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
|
@@ -9055,10 +9375,129 @@
|
|
| 9055 |
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
| 9056 |
"license": "MIT",
|
| 9057 |
"dependencies": {
|
| 9058 |
-
"function-bind": "^1.1.2"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9059 |
},
|
| 9060 |
-
"
|
| 9061 |
-
"
|
|
|
|
| 9062 |
}
|
| 9063 |
},
|
| 9064 |
"node_modules/hast-util-to-jsx-runtime": {
|
|
@@ -9088,6 +9527,22 @@
|
|
| 9088 |
"url": "https://opencollective.com/unified"
|
| 9089 |
}
|
| 9090 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9091 |
"node_modules/hast-util-whitespace": {
|
| 9092 |
"version": "3.0.0",
|
| 9093 |
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
|
@@ -9101,6 +9556,23 @@
|
|
| 9101 |
"url": "https://opencollective.com/unified"
|
| 9102 |
}
|
| 9103 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9104 |
"node_modules/he": {
|
| 9105 |
"version": "1.2.0",
|
| 9106 |
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
|
@@ -9385,6 +9857,28 @@
|
|
| 9385 |
"node": ">=10.17.0"
|
| 9386 |
}
|
| 9387 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9388 |
"node_modules/iconv-lite": {
|
| 9389 |
"version": "0.6.3",
|
| 9390 |
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
|
@@ -9436,6 +9930,21 @@
|
|
| 9436 |
"node": ">= 4"
|
| 9437 |
}
|
| 9438 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9439 |
"node_modules/immer": {
|
| 9440 |
"version": "9.0.21",
|
| 9441 |
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
|
@@ -11178,6 +11687,71 @@
|
|
| 11178 |
"jiti": "bin/jiti.js"
|
| 11179 |
}
|
| 11180 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11181 |
"node_modules/js-tokens": {
|
| 11182 |
"version": "4.0.0",
|
| 11183 |
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
|
@@ -11356,6 +11930,22 @@
|
|
| 11356 |
"node": ">=4.0"
|
| 11357 |
}
|
| 11358 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11359 |
"node_modules/keyv": {
|
| 11360 |
"version": "4.5.4",
|
| 11361 |
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
|
@@ -11410,6 +12000,27 @@
|
|
| 11410 |
"node": ">=0.10"
|
| 11411 |
}
|
| 11412 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11413 |
"node_modules/launch-editor": {
|
| 11414 |
"version": "2.10.0",
|
| 11415 |
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz",
|
|
@@ -11800,6 +12411,25 @@
|
|
| 11800 |
"url": "https://opencollective.com/unified"
|
| 11801 |
}
|
| 11802 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11803 |
"node_modules/mdast-util-mdx-expression": {
|
| 11804 |
"version": "2.0.1",
|
| 11805 |
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
|
|
@@ -12179,6 +12809,25 @@
|
|
| 12179 |
"url": "https://opencollective.com/unified"
|
| 12180 |
}
|
| 12181 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12182 |
"node_modules/micromark-factory-destination": {
|
| 12183 |
"version": "2.0.1",
|
| 12184 |
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
|
@@ -12790,6 +13439,21 @@
|
|
| 12790 |
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
| 12791 |
"license": "MIT"
|
| 12792 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12793 |
"node_modules/normalize-path": {
|
| 12794 |
"version": "3.0.0",
|
| 12795 |
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
|
@@ -13152,6 +13816,12 @@
|
|
| 13152 |
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
| 13153 |
"license": "BlueOak-1.0.0"
|
| 13154 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13155 |
"node_modules/param-case": {
|
| 13156 |
"version": "3.0.4",
|
| 13157 |
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
|
@@ -14854,6 +15524,12 @@
|
|
| 14854 |
"url": "https://github.com/sponsors/wooorm"
|
| 14855 |
}
|
| 14856 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14857 |
"node_modules/proxy-addr": {
|
| 14858 |
"version": "2.0.7",
|
| 14859 |
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
|
@@ -14929,6 +15605,15 @@
|
|
| 14929 |
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
| 14930 |
"license": "MIT"
|
| 14931 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14932 |
"node_modules/queue-microtask": {
|
| 14933 |
"version": "1.2.3",
|
| 14934 |
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
|
@@ -15495,6 +16180,25 @@
|
|
| 15495 |
"node": ">=6"
|
| 15496 |
}
|
| 15497 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15498 |
"node_modules/relateurl": {
|
| 15499 |
"version": "0.2.7",
|
| 15500 |
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
|
|
@@ -15522,6 +16226,22 @@
|
|
| 15522 |
"url": "https://opencollective.com/unified"
|
| 15523 |
}
|
| 15524 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15525 |
"node_modules/remark-parse": {
|
| 15526 |
"version": "11.0.0",
|
| 15527 |
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
|
@@ -15607,12 +16327,6 @@
|
|
| 15607 |
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
| 15608 |
"license": "MIT"
|
| 15609 |
},
|
| 15610 |
-
"node_modules/resize-observer-polyfill": {
|
| 15611 |
-
"version": "1.5.1",
|
| 15612 |
-
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
| 15613 |
-
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
| 15614 |
-
"license": "MIT"
|
| 15615 |
-
},
|
| 15616 |
"node_modules/resolve": {
|
| 15617 |
"version": "1.22.10",
|
| 15618 |
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
|
@@ -15729,6 +16443,12 @@
|
|
| 15729 |
"node": ">=10"
|
| 15730 |
}
|
| 15731 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15732 |
"node_modules/retry": {
|
| 15733 |
"version": "0.13.1",
|
| 15734 |
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
|
@@ -16670,6 +17390,11 @@
|
|
| 16670 |
"node": ">= 0.8"
|
| 16671 |
}
|
| 16672 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16673 |
"node_modules/stop-iteration-iterator": {
|
| 16674 |
"version": "1.1.0",
|
| 16675 |
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
|
@@ -16979,6 +17704,12 @@
|
|
| 16979 |
"webpack": "^5.0.0"
|
| 16980 |
}
|
| 16981 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16982 |
"node_modules/style-to-js": {
|
| 16983 |
"version": "1.1.17",
|
| 16984 |
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz",
|
|
@@ -17131,6 +17862,30 @@
|
|
| 17131 |
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
|
| 17132 |
"license": "MIT"
|
| 17133 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17134 |
"node_modules/svgo": {
|
| 17135 |
"version": "1.3.2",
|
| 17136 |
"resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
|
|
@@ -17470,6 +18225,12 @@
|
|
| 17470 |
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
| 17471 |
"license": "MIT"
|
| 17472 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17473 |
"node_modules/tmpl": {
|
| 17474 |
"version": "1.0.5",
|
| 17475 |
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
|
@@ -17754,6 +18515,20 @@
|
|
| 17754 |
"is-typedarray": "^1.0.0"
|
| 17755 |
}
|
| 17756 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17757 |
"node_modules/unbox-primitive": {
|
| 17758 |
"version": "1.1.0",
|
| 17759 |
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
|
@@ -17815,6 +18590,16 @@
|
|
| 17815 |
"node": ">=4"
|
| 17816 |
}
|
| 17817 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17818 |
"node_modules/unicode-property-aliases-ecmascript": {
|
| 17819 |
"version": "2.1.0",
|
| 17820 |
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
|
|
@@ -17824,6 +18609,16 @@
|
|
| 17824 |
"node": ">=4"
|
| 17825 |
}
|
| 17826 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17827 |
"node_modules/unified": {
|
| 17828 |
"version": "11.0.5",
|
| 17829 |
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
|
@@ -17867,6 +18662,20 @@
|
|
| 17867 |
"node": ">=8"
|
| 17868 |
}
|
| 17869 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17870 |
"node_modules/unist-util-is": {
|
| 17871 |
"version": "6.0.0",
|
| 17872 |
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
|
|
@@ -17893,6 +18702,20 @@
|
|
| 17893 |
"url": "https://opencollective.com/unified"
|
| 17894 |
}
|
| 17895 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17896 |
"node_modules/unist-util-stringify-position": {
|
| 17897 |
"version": "4.0.0",
|
| 17898 |
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
|
@@ -18106,6 +18929,20 @@
|
|
| 18106 |
"url": "https://opencollective.com/unified"
|
| 18107 |
}
|
| 18108 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18109 |
"node_modules/vfile-message": {
|
| 18110 |
"version": "4.0.3",
|
| 18111 |
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
|
|
@@ -18130,6 +18967,12 @@
|
|
| 18130 |
"browser-process-hrtime": "^1.0.0"
|
| 18131 |
}
|
| 18132 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18133 |
"node_modules/w3c-xmlserializer": {
|
| 18134 |
"version": "2.0.0",
|
| 18135 |
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
|
|
@@ -18173,6 +19016,16 @@
|
|
| 18173 |
"minimalistic-assert": "^1.0.0"
|
| 18174 |
}
|
| 18175 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18176 |
"node_modules/web-vitals": {
|
| 18177 |
"version": "2.1.4",
|
| 18178 |
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
|
|
|
|
| 8 |
"name": "phd-advisor-frontend",
|
| 9 |
"version": "0.1.0",
|
| 10 |
"dependencies": {
|
| 11 |
+
"@codemirror/legacy-modes": "^6.5.2",
|
| 12 |
"@testing-library/dom": "^10.4.0",
|
| 13 |
"@testing-library/jest-dom": "^6.6.3",
|
| 14 |
"@testing-library/react": "^16.3.0",
|
| 15 |
"@testing-library/user-event": "^13.5.0",
|
| 16 |
+
"@uiw/react-codemirror": "^4.25.9",
|
| 17 |
+
"katex": "^0.16.45",
|
| 18 |
+
"latex.js": "^0.12.6",
|
| 19 |
"lucide-react": "^0.544.0",
|
| 20 |
"react": "^19.1.0",
|
| 21 |
"react-dom": "^19.1.0",
|
| 22 |
"react-markdown": "^10.1.0",
|
| 23 |
"react-scripts": "5.0.1",
|
| 24 |
+
"rehype-katex": "^7.0.1",
|
| 25 |
"remark-gfm": "^4.0.1",
|
| 26 |
+
"remark-math": "^6.0.0",
|
| 27 |
"web-vitals": "^2.1.4"
|
| 28 |
}
|
| 29 |
},
|
|
|
|
| 2074 |
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
| 2075 |
"license": "MIT"
|
| 2076 |
},
|
| 2077 |
+
"node_modules/@codemirror/autocomplete": {
|
| 2078 |
+
"version": "6.20.2",
|
| 2079 |
+
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz",
|
| 2080 |
+
"integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==",
|
| 2081 |
+
"license": "MIT",
|
| 2082 |
+
"dependencies": {
|
| 2083 |
+
"@codemirror/language": "^6.0.0",
|
| 2084 |
+
"@codemirror/state": "^6.0.0",
|
| 2085 |
+
"@codemirror/view": "^6.17.0",
|
| 2086 |
+
"@lezer/common": "^1.0.0"
|
| 2087 |
+
}
|
| 2088 |
+
},
|
| 2089 |
+
"node_modules/@codemirror/commands": {
|
| 2090 |
+
"version": "6.10.3",
|
| 2091 |
+
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz",
|
| 2092 |
+
"integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==",
|
| 2093 |
+
"license": "MIT",
|
| 2094 |
+
"dependencies": {
|
| 2095 |
+
"@codemirror/language": "^6.0.0",
|
| 2096 |
+
"@codemirror/state": "^6.6.0",
|
| 2097 |
+
"@codemirror/view": "^6.27.0",
|
| 2098 |
+
"@lezer/common": "^1.1.0"
|
| 2099 |
+
}
|
| 2100 |
+
},
|
| 2101 |
+
"node_modules/@codemirror/language": {
|
| 2102 |
+
"version": "6.12.3",
|
| 2103 |
+
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz",
|
| 2104 |
+
"integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==",
|
| 2105 |
+
"license": "MIT",
|
| 2106 |
+
"dependencies": {
|
| 2107 |
+
"@codemirror/state": "^6.0.0",
|
| 2108 |
+
"@codemirror/view": "^6.23.0",
|
| 2109 |
+
"@lezer/common": "^1.5.0",
|
| 2110 |
+
"@lezer/highlight": "^1.0.0",
|
| 2111 |
+
"@lezer/lr": "^1.0.0",
|
| 2112 |
+
"style-mod": "^4.0.0"
|
| 2113 |
+
}
|
| 2114 |
+
},
|
| 2115 |
+
"node_modules/@codemirror/legacy-modes": {
|
| 2116 |
+
"version": "6.5.2",
|
| 2117 |
+
"resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz",
|
| 2118 |
+
"integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==",
|
| 2119 |
+
"license": "MIT",
|
| 2120 |
+
"dependencies": {
|
| 2121 |
+
"@codemirror/language": "^6.0.0"
|
| 2122 |
+
}
|
| 2123 |
+
},
|
| 2124 |
+
"node_modules/@codemirror/lint": {
|
| 2125 |
+
"version": "6.9.6",
|
| 2126 |
+
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz",
|
| 2127 |
+
"integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==",
|
| 2128 |
+
"license": "MIT",
|
| 2129 |
+
"dependencies": {
|
| 2130 |
+
"@codemirror/state": "^6.0.0",
|
| 2131 |
+
"@codemirror/view": "^6.42.0",
|
| 2132 |
+
"crelt": "^1.0.5"
|
| 2133 |
+
}
|
| 2134 |
+
},
|
| 2135 |
+
"node_modules/@codemirror/search": {
|
| 2136 |
+
"version": "6.7.0",
|
| 2137 |
+
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz",
|
| 2138 |
+
"integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==",
|
| 2139 |
+
"license": "MIT",
|
| 2140 |
+
"dependencies": {
|
| 2141 |
+
"@codemirror/state": "^6.0.0",
|
| 2142 |
+
"@codemirror/view": "^6.37.0",
|
| 2143 |
+
"crelt": "^1.0.5"
|
| 2144 |
+
}
|
| 2145 |
+
},
|
| 2146 |
+
"node_modules/@codemirror/state": {
|
| 2147 |
+
"version": "6.6.0",
|
| 2148 |
+
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
|
| 2149 |
+
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
|
| 2150 |
+
"license": "MIT",
|
| 2151 |
+
"dependencies": {
|
| 2152 |
+
"@marijn/find-cluster-break": "^1.0.0"
|
| 2153 |
+
}
|
| 2154 |
+
},
|
| 2155 |
+
"node_modules/@codemirror/theme-one-dark": {
|
| 2156 |
+
"version": "6.1.3",
|
| 2157 |
+
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
| 2158 |
+
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
|
| 2159 |
+
"license": "MIT",
|
| 2160 |
+
"dependencies": {
|
| 2161 |
+
"@codemirror/language": "^6.0.0",
|
| 2162 |
+
"@codemirror/state": "^6.0.0",
|
| 2163 |
+
"@codemirror/view": "^6.0.0",
|
| 2164 |
+
"@lezer/highlight": "^1.0.0"
|
| 2165 |
+
}
|
| 2166 |
+
},
|
| 2167 |
+
"node_modules/@codemirror/view": {
|
| 2168 |
+
"version": "6.42.1",
|
| 2169 |
+
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.42.1.tgz",
|
| 2170 |
+
"integrity": "sha512-ToN3oFc0nsxNUYVF5P0ztLgbC4UPPjPtA9aKYhkOKQaZASpOUo6ISXyQLP66ctVwlDc+j6Jv0uK5IFALkiXztg==",
|
| 2171 |
+
"license": "MIT",
|
| 2172 |
+
"dependencies": {
|
| 2173 |
+
"@codemirror/state": "^6.6.0",
|
| 2174 |
+
"crelt": "^1.0.6",
|
| 2175 |
+
"style-mod": "^4.1.0",
|
| 2176 |
+
"w3c-keyname": "^2.2.4"
|
| 2177 |
+
}
|
| 2178 |
+
},
|
| 2179 |
"node_modules/@csstools/normalize.css": {
|
| 2180 |
"version": "12.1.1",
|
| 2181 |
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz",
|
|
|
|
| 3074 |
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
|
| 3075 |
"license": "MIT"
|
| 3076 |
},
|
| 3077 |
+
"node_modules/@lezer/common": {
|
| 3078 |
+
"version": "1.5.2",
|
| 3079 |
+
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
| 3080 |
+
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
| 3081 |
+
"license": "MIT"
|
| 3082 |
+
},
|
| 3083 |
+
"node_modules/@lezer/highlight": {
|
| 3084 |
+
"version": "1.2.3",
|
| 3085 |
+
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
| 3086 |
+
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
| 3087 |
+
"license": "MIT",
|
| 3088 |
+
"dependencies": {
|
| 3089 |
+
"@lezer/common": "^1.3.0"
|
| 3090 |
+
}
|
| 3091 |
+
},
|
| 3092 |
+
"node_modules/@lezer/lr": {
|
| 3093 |
+
"version": "1.4.10",
|
| 3094 |
+
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
| 3095 |
+
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
| 3096 |
+
"license": "MIT",
|
| 3097 |
+
"dependencies": {
|
| 3098 |
+
"@lezer/common": "^1.0.0"
|
| 3099 |
+
}
|
| 3100 |
+
},
|
| 3101 |
+
"node_modules/@marijn/find-cluster-break": {
|
| 3102 |
+
"version": "1.0.2",
|
| 3103 |
+
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
| 3104 |
+
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
| 3105 |
+
"license": "MIT"
|
| 3106 |
+
},
|
| 3107 |
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
| 3108 |
"version": "5.1.1-v1",
|
| 3109 |
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
|
|
|
| 3170 |
"node": ">= 8"
|
| 3171 |
}
|
| 3172 |
},
|
| 3173 |
+
"node_modules/@one-ini/wasm": {
|
| 3174 |
+
"version": "0.1.1",
|
| 3175 |
+
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
| 3176 |
+
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
|
| 3177 |
+
"license": "MIT"
|
| 3178 |
+
},
|
| 3179 |
"node_modules/@pkgjs/parseargs": {
|
| 3180 |
"version": "0.11.0",
|
| 3181 |
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
|
|
|
| 3234 |
}
|
| 3235 |
}
|
| 3236 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3237 |
"node_modules/@rollup/plugin-babel": {
|
| 3238 |
"version": "5.3.1",
|
| 3239 |
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
|
|
|
|
| 3313 |
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
|
| 3314 |
"license": "MIT"
|
| 3315 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3316 |
"node_modules/@rtsao/scc": {
|
| 3317 |
"version": "1.1.0",
|
| 3318 |
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
|
|
|
| 3582 |
"url": "https://github.com/sponsors/gregberge"
|
| 3583 |
}
|
| 3584 |
},
|
| 3585 |
+
"node_modules/@swc/helpers": {
|
| 3586 |
+
"version": "0.5.21",
|
| 3587 |
+
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz",
|
| 3588 |
+
"integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
|
| 3589 |
+
"license": "Apache-2.0",
|
| 3590 |
+
"dependencies": {
|
| 3591 |
+
"tslib": "^2.8.0"
|
| 3592 |
+
}
|
| 3593 |
+
},
|
| 3594 |
"node_modules/@testing-library/dom": {
|
| 3595 |
"version": "10.4.0",
|
| 3596 |
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
|
|
|
| 3950 |
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
| 3951 |
"license": "MIT"
|
| 3952 |
},
|
| 3953 |
+
"node_modules/@types/katex": {
|
| 3954 |
+
"version": "0.16.8",
|
| 3955 |
+
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz",
|
| 3956 |
+
"integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==",
|
| 3957 |
+
"license": "MIT"
|
| 3958 |
+
},
|
| 3959 |
"node_modules/@types/mdast": {
|
| 3960 |
"version": "4.0.4",
|
| 3961 |
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
|
|
|
| 4025 |
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
| 4026 |
"license": "MIT"
|
| 4027 |
},
|
| 4028 |
+
"node_modules/@types/react": {
|
| 4029 |
+
"version": "19.2.14",
|
| 4030 |
+
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
| 4031 |
+
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
| 4032 |
+
"license": "MIT",
|
| 4033 |
+
"peer": true,
|
| 4034 |
+
"dependencies": {
|
| 4035 |
+
"csstype": "^3.2.2"
|
| 4036 |
+
}
|
| 4037 |
+
},
|
| 4038 |
"node_modules/@types/resolve": {
|
| 4039 |
"version": "1.17.1",
|
| 4040 |
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
|
|
|
| 4366 |
"url": "https://opencollective.com/typescript-eslint"
|
| 4367 |
}
|
| 4368 |
},
|
| 4369 |
+
"node_modules/@uiw/codemirror-extensions-basic-setup": {
|
| 4370 |
+
"version": "4.25.9",
|
| 4371 |
+
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.9.tgz",
|
| 4372 |
+
"integrity": "sha512-QFAqr+pu6lDmNpAlecODcF49TlsrZ0bj15zPzfhiqSDl+Um3EsDLFLppixC7kFLn+rdDM2LTvVjn5CPvefpRgw==",
|
| 4373 |
+
"license": "MIT",
|
| 4374 |
+
"dependencies": {
|
| 4375 |
+
"@codemirror/autocomplete": "^6.0.0",
|
| 4376 |
+
"@codemirror/commands": "^6.0.0",
|
| 4377 |
+
"@codemirror/language": "^6.0.0",
|
| 4378 |
+
"@codemirror/lint": "^6.0.0",
|
| 4379 |
+
"@codemirror/search": "^6.0.0",
|
| 4380 |
+
"@codemirror/state": "^6.0.0",
|
| 4381 |
+
"@codemirror/view": "^6.0.0"
|
| 4382 |
+
},
|
| 4383 |
+
"funding": {
|
| 4384 |
+
"url": "https://jaywcjlove.github.io/#/sponsor"
|
| 4385 |
+
},
|
| 4386 |
+
"peerDependencies": {
|
| 4387 |
+
"@codemirror/autocomplete": ">=6.0.0",
|
| 4388 |
+
"@codemirror/commands": ">=6.0.0",
|
| 4389 |
+
"@codemirror/language": ">=6.0.0",
|
| 4390 |
+
"@codemirror/lint": ">=6.0.0",
|
| 4391 |
+
"@codemirror/search": ">=6.0.0",
|
| 4392 |
+
"@codemirror/state": ">=6.0.0",
|
| 4393 |
+
"@codemirror/view": ">=6.0.0"
|
| 4394 |
+
}
|
| 4395 |
+
},
|
| 4396 |
+
"node_modules/@uiw/react-codemirror": {
|
| 4397 |
+
"version": "4.25.9",
|
| 4398 |
+
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.9.tgz",
|
| 4399 |
+
"integrity": "sha512-HftqCBUYShAOH0pGi1CHP8vfm5L8fQ3+0j0VI6lQD6QpK+UBu3J7nxfEN5O/BXMilMNf9ZyFJRvRcuMMOLHMng==",
|
| 4400 |
+
"license": "MIT",
|
| 4401 |
+
"dependencies": {
|
| 4402 |
+
"@babel/runtime": "^7.18.6",
|
| 4403 |
+
"@codemirror/commands": "^6.1.0",
|
| 4404 |
+
"@codemirror/state": "^6.1.1",
|
| 4405 |
+
"@codemirror/theme-one-dark": "^6.0.0",
|
| 4406 |
+
"@uiw/codemirror-extensions-basic-setup": "4.25.9",
|
| 4407 |
+
"codemirror": "^6.0.0"
|
| 4408 |
+
},
|
| 4409 |
+
"funding": {
|
| 4410 |
+
"url": "https://jaywcjlove.github.io/#/sponsor"
|
| 4411 |
+
},
|
| 4412 |
+
"peerDependencies": {
|
| 4413 |
+
"@babel/runtime": ">=7.11.0",
|
| 4414 |
+
"@codemirror/state": ">=6.0.0",
|
| 4415 |
+
"@codemirror/theme-one-dark": ">=6.0.0",
|
| 4416 |
+
"@codemirror/view": ">=6.0.0",
|
| 4417 |
+
"codemirror": ">=6.0.0",
|
| 4418 |
+
"react": ">=17.0.0",
|
| 4419 |
+
"react-dom": ">=17.0.0"
|
| 4420 |
+
}
|
| 4421 |
+
},
|
| 4422 |
"node_modules/@ungap/structured-clone": {
|
| 4423 |
"version": "1.3.0",
|
| 4424 |
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
|
|
|
| 4590 |
"deprecated": "Use your platform's native atob() and btoa() methods instead",
|
| 4591 |
"license": "BSD-3-Clause"
|
| 4592 |
},
|
| 4593 |
+
"node_modules/abbrev": {
|
| 4594 |
+
"version": "2.0.0",
|
| 4595 |
+
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
| 4596 |
+
"integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
|
| 4597 |
+
"license": "ISC",
|
| 4598 |
+
"engines": {
|
| 4599 |
+
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
| 4600 |
+
}
|
| 4601 |
+
},
|
| 4602 |
"node_modules/accepts": {
|
| 4603 |
"version": "1.3.8",
|
| 4604 |
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
|
|
|
| 5448 |
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
| 5449 |
"license": "MIT"
|
| 5450 |
},
|
| 5451 |
+
"node_modules/base64-js": {
|
| 5452 |
+
"version": "1.5.1",
|
| 5453 |
+
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
| 5454 |
+
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
| 5455 |
+
"funding": [
|
| 5456 |
+
{
|
| 5457 |
+
"type": "github",
|
| 5458 |
+
"url": "https://github.com/sponsors/feross"
|
| 5459 |
+
},
|
| 5460 |
+
{
|
| 5461 |
+
"type": "patreon",
|
| 5462 |
+
"url": "https://www.patreon.com/feross"
|
| 5463 |
+
},
|
| 5464 |
+
{
|
| 5465 |
+
"type": "consulting",
|
| 5466 |
+
"url": "https://feross.org/support"
|
| 5467 |
+
}
|
| 5468 |
+
],
|
| 5469 |
+
"license": "MIT"
|
| 5470 |
+
},
|
| 5471 |
"node_modules/batch": {
|
| 5472 |
"version": "0.6.1",
|
| 5473 |
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
|
|
|
| 5606 |
"node": ">=8"
|
| 5607 |
}
|
| 5608 |
},
|
| 5609 |
+
"node_modules/brotli": {
|
| 5610 |
+
"version": "1.3.3",
|
| 5611 |
+
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
| 5612 |
+
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
| 5613 |
+
"license": "MIT",
|
| 5614 |
+
"dependencies": {
|
| 5615 |
+
"base64-js": "^1.1.2"
|
| 5616 |
+
}
|
| 5617 |
+
},
|
| 5618 |
"node_modules/browser-process-hrtime": {
|
| 5619 |
"version": "1.0.0",
|
| 5620 |
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
|
|
|
|
| 5996 |
"wrap-ansi": "^7.0.0"
|
| 5997 |
}
|
| 5998 |
},
|
| 5999 |
+
"node_modules/clone": {
|
| 6000 |
+
"version": "2.1.2",
|
| 6001 |
+
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
| 6002 |
+
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
| 6003 |
+
"license": "MIT",
|
| 6004 |
+
"engines": {
|
| 6005 |
+
"node": ">=0.8"
|
| 6006 |
+
}
|
| 6007 |
+
},
|
| 6008 |
"node_modules/co": {
|
| 6009 |
"version": "4.6.0",
|
| 6010 |
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
|
|
|
| 6100 |
"node": ">=4"
|
| 6101 |
}
|
| 6102 |
},
|
| 6103 |
+
"node_modules/codemirror": {
|
| 6104 |
+
"version": "6.0.2",
|
| 6105 |
+
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
| 6106 |
+
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
| 6107 |
+
"license": "MIT",
|
| 6108 |
+
"dependencies": {
|
| 6109 |
+
"@codemirror/autocomplete": "^6.0.0",
|
| 6110 |
+
"@codemirror/commands": "^6.0.0",
|
| 6111 |
+
"@codemirror/language": "^6.0.0",
|
| 6112 |
+
"@codemirror/lint": "^6.0.0",
|
| 6113 |
+
"@codemirror/search": "^6.0.0",
|
| 6114 |
+
"@codemirror/state": "^6.0.0",
|
| 6115 |
+
"@codemirror/view": "^6.0.0"
|
| 6116 |
+
}
|
| 6117 |
+
},
|
| 6118 |
"node_modules/collect-v8-coverage": {
|
| 6119 |
"version": "1.0.2",
|
| 6120 |
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
|
|
|
|
| 6248 |
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
| 6249 |
"license": "MIT"
|
| 6250 |
},
|
| 6251 |
+
"node_modules/config-chain": {
|
| 6252 |
+
"version": "1.1.13",
|
| 6253 |
+
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
| 6254 |
+
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
|
| 6255 |
+
"license": "MIT",
|
| 6256 |
+
"dependencies": {
|
| 6257 |
+
"ini": "^1.3.4",
|
| 6258 |
+
"proto-list": "~1.2.1"
|
| 6259 |
+
}
|
| 6260 |
+
},
|
| 6261 |
"node_modules/confusing-browser-globals": {
|
| 6262 |
"version": "1.0.11",
|
| 6263 |
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
|
|
|
|
| 6372 |
"node": ">=10"
|
| 6373 |
}
|
| 6374 |
},
|
| 6375 |
+
"node_modules/crelt": {
|
| 6376 |
+
"version": "1.0.6",
|
| 6377 |
+
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
| 6378 |
+
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
| 6379 |
+
"license": "MIT"
|
| 6380 |
+
},
|
| 6381 |
"node_modules/cross-spawn": {
|
| 6382 |
"version": "7.0.6",
|
| 6383 |
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
|
|
|
| 6776 |
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
| 6777 |
"license": "MIT"
|
| 6778 |
},
|
| 6779 |
+
"node_modules/csstype": {
|
| 6780 |
+
"version": "3.2.3",
|
| 6781 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
| 6782 |
+
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 6783 |
+
"license": "MIT",
|
| 6784 |
+
"peer": true
|
| 6785 |
+
},
|
| 6786 |
"node_modules/damerau-levenshtein": {
|
| 6787 |
"version": "1.0.8",
|
| 6788 |
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
|
|
|
| 7063 |
"url": "https://github.com/sponsors/wooorm"
|
| 7064 |
}
|
| 7065 |
},
|
| 7066 |
+
"node_modules/dfa": {
|
| 7067 |
+
"version": "1.2.0",
|
| 7068 |
+
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
| 7069 |
+
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
|
| 7070 |
+
"license": "MIT"
|
| 7071 |
+
},
|
| 7072 |
"node_modules/didyoumean": {
|
| 7073 |
"version": "1.2.2",
|
| 7074 |
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
|
|
|
| 7269 |
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
| 7270 |
"license": "MIT"
|
| 7271 |
},
|
| 7272 |
+
"node_modules/editorconfig": {
|
| 7273 |
+
"version": "1.0.7",
|
| 7274 |
+
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz",
|
| 7275 |
+
"integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==",
|
| 7276 |
+
"license": "MIT",
|
| 7277 |
+
"dependencies": {
|
| 7278 |
+
"@one-ini/wasm": "0.1.1",
|
| 7279 |
+
"commander": "^10.0.0",
|
| 7280 |
+
"minimatch": "^9.0.1",
|
| 7281 |
+
"semver": "^7.5.3"
|
| 7282 |
+
},
|
| 7283 |
+
"bin": {
|
| 7284 |
+
"editorconfig": "bin/editorconfig"
|
| 7285 |
+
},
|
| 7286 |
+
"engines": {
|
| 7287 |
+
"node": ">=14"
|
| 7288 |
+
}
|
| 7289 |
+
},
|
| 7290 |
+
"node_modules/editorconfig/node_modules/brace-expansion": {
|
| 7291 |
+
"version": "2.1.0",
|
| 7292 |
+
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
| 7293 |
+
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
| 7294 |
+
"license": "MIT",
|
| 7295 |
+
"dependencies": {
|
| 7296 |
+
"balanced-match": "^1.0.0"
|
| 7297 |
+
}
|
| 7298 |
+
},
|
| 7299 |
+
"node_modules/editorconfig/node_modules/commander": {
|
| 7300 |
+
"version": "10.0.1",
|
| 7301 |
+
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
| 7302 |
+
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
| 7303 |
+
"license": "MIT",
|
| 7304 |
+
"engines": {
|
| 7305 |
+
"node": ">=14"
|
| 7306 |
+
}
|
| 7307 |
+
},
|
| 7308 |
+
"node_modules/editorconfig/node_modules/minimatch": {
|
| 7309 |
+
"version": "9.0.9",
|
| 7310 |
+
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
| 7311 |
+
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
| 7312 |
+
"license": "ISC",
|
| 7313 |
+
"dependencies": {
|
| 7314 |
+
"brace-expansion": "^2.0.2"
|
| 7315 |
+
},
|
| 7316 |
+
"engines": {
|
| 7317 |
+
"node": ">=16 || 14 >=14.17"
|
| 7318 |
+
},
|
| 7319 |
+
"funding": {
|
| 7320 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 7321 |
+
}
|
| 7322 |
+
},
|
| 7323 |
"node_modules/ee-first": {
|
| 7324 |
"version": "1.1.1",
|
| 7325 |
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
|
|
|
| 8741 |
}
|
| 8742 |
}
|
| 8743 |
},
|
| 8744 |
+
"node_modules/fontkit": {
|
| 8745 |
+
"version": "2.0.4",
|
| 8746 |
+
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
| 8747 |
+
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
|
| 8748 |
+
"license": "MIT",
|
| 8749 |
+
"dependencies": {
|
| 8750 |
+
"@swc/helpers": "^0.5.12",
|
| 8751 |
+
"brotli": "^1.3.2",
|
| 8752 |
+
"clone": "^2.1.2",
|
| 8753 |
+
"dfa": "^1.2.0",
|
| 8754 |
+
"fast-deep-equal": "^3.1.3",
|
| 8755 |
+
"restructure": "^3.0.0",
|
| 8756 |
+
"tiny-inflate": "^1.0.3",
|
| 8757 |
+
"unicode-properties": "^1.4.0",
|
| 8758 |
+
"unicode-trie": "^2.0.0"
|
| 8759 |
+
}
|
| 8760 |
+
},
|
| 8761 |
"node_modules/for-each": {
|
| 8762 |
"version": "0.3.5",
|
| 8763 |
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
|
|
|
| 9375 |
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
| 9376 |
"license": "MIT",
|
| 9377 |
"dependencies": {
|
| 9378 |
+
"function-bind": "^1.1.2"
|
| 9379 |
+
},
|
| 9380 |
+
"engines": {
|
| 9381 |
+
"node": ">= 0.4"
|
| 9382 |
+
}
|
| 9383 |
+
},
|
| 9384 |
+
"node_modules/hast-util-from-dom": {
|
| 9385 |
+
"version": "5.0.1",
|
| 9386 |
+
"resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz",
|
| 9387 |
+
"integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==",
|
| 9388 |
+
"license": "ISC",
|
| 9389 |
+
"dependencies": {
|
| 9390 |
+
"@types/hast": "^3.0.0",
|
| 9391 |
+
"hastscript": "^9.0.0",
|
| 9392 |
+
"web-namespaces": "^2.0.0"
|
| 9393 |
+
},
|
| 9394 |
+
"funding": {
|
| 9395 |
+
"type": "opencollective",
|
| 9396 |
+
"url": "https://opencollective.com/unified"
|
| 9397 |
+
}
|
| 9398 |
+
},
|
| 9399 |
+
"node_modules/hast-util-from-html": {
|
| 9400 |
+
"version": "2.0.3",
|
| 9401 |
+
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
|
| 9402 |
+
"integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
|
| 9403 |
+
"license": "MIT",
|
| 9404 |
+
"dependencies": {
|
| 9405 |
+
"@types/hast": "^3.0.0",
|
| 9406 |
+
"devlop": "^1.1.0",
|
| 9407 |
+
"hast-util-from-parse5": "^8.0.0",
|
| 9408 |
+
"parse5": "^7.0.0",
|
| 9409 |
+
"vfile": "^6.0.0",
|
| 9410 |
+
"vfile-message": "^4.0.0"
|
| 9411 |
+
},
|
| 9412 |
+
"funding": {
|
| 9413 |
+
"type": "opencollective",
|
| 9414 |
+
"url": "https://opencollective.com/unified"
|
| 9415 |
+
}
|
| 9416 |
+
},
|
| 9417 |
+
"node_modules/hast-util-from-html-isomorphic": {
|
| 9418 |
+
"version": "2.0.0",
|
| 9419 |
+
"resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz",
|
| 9420 |
+
"integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==",
|
| 9421 |
+
"license": "MIT",
|
| 9422 |
+
"dependencies": {
|
| 9423 |
+
"@types/hast": "^3.0.0",
|
| 9424 |
+
"hast-util-from-dom": "^5.0.0",
|
| 9425 |
+
"hast-util-from-html": "^2.0.0",
|
| 9426 |
+
"unist-util-remove-position": "^5.0.0"
|
| 9427 |
+
},
|
| 9428 |
+
"funding": {
|
| 9429 |
+
"type": "opencollective",
|
| 9430 |
+
"url": "https://opencollective.com/unified"
|
| 9431 |
+
}
|
| 9432 |
+
},
|
| 9433 |
+
"node_modules/hast-util-from-html/node_modules/entities": {
|
| 9434 |
+
"version": "6.0.1",
|
| 9435 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
| 9436 |
+
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
| 9437 |
+
"license": "BSD-2-Clause",
|
| 9438 |
+
"engines": {
|
| 9439 |
+
"node": ">=0.12"
|
| 9440 |
+
},
|
| 9441 |
+
"funding": {
|
| 9442 |
+
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 9443 |
+
}
|
| 9444 |
+
},
|
| 9445 |
+
"node_modules/hast-util-from-html/node_modules/parse5": {
|
| 9446 |
+
"version": "7.3.0",
|
| 9447 |
+
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
| 9448 |
+
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
| 9449 |
+
"license": "MIT",
|
| 9450 |
+
"dependencies": {
|
| 9451 |
+
"entities": "^6.0.0"
|
| 9452 |
+
},
|
| 9453 |
+
"funding": {
|
| 9454 |
+
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
| 9455 |
+
}
|
| 9456 |
+
},
|
| 9457 |
+
"node_modules/hast-util-from-parse5": {
|
| 9458 |
+
"version": "8.0.3",
|
| 9459 |
+
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
|
| 9460 |
+
"integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
|
| 9461 |
+
"license": "MIT",
|
| 9462 |
+
"dependencies": {
|
| 9463 |
+
"@types/hast": "^3.0.0",
|
| 9464 |
+
"@types/unist": "^3.0.0",
|
| 9465 |
+
"devlop": "^1.0.0",
|
| 9466 |
+
"hastscript": "^9.0.0",
|
| 9467 |
+
"property-information": "^7.0.0",
|
| 9468 |
+
"vfile": "^6.0.0",
|
| 9469 |
+
"vfile-location": "^5.0.0",
|
| 9470 |
+
"web-namespaces": "^2.0.0"
|
| 9471 |
+
},
|
| 9472 |
+
"funding": {
|
| 9473 |
+
"type": "opencollective",
|
| 9474 |
+
"url": "https://opencollective.com/unified"
|
| 9475 |
+
}
|
| 9476 |
+
},
|
| 9477 |
+
"node_modules/hast-util-is-element": {
|
| 9478 |
+
"version": "3.0.0",
|
| 9479 |
+
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
|
| 9480 |
+
"integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
|
| 9481 |
+
"license": "MIT",
|
| 9482 |
+
"dependencies": {
|
| 9483 |
+
"@types/hast": "^3.0.0"
|
| 9484 |
+
},
|
| 9485 |
+
"funding": {
|
| 9486 |
+
"type": "opencollective",
|
| 9487 |
+
"url": "https://opencollective.com/unified"
|
| 9488 |
+
}
|
| 9489 |
+
},
|
| 9490 |
+
"node_modules/hast-util-parse-selector": {
|
| 9491 |
+
"version": "4.0.0",
|
| 9492 |
+
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
|
| 9493 |
+
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
|
| 9494 |
+
"license": "MIT",
|
| 9495 |
+
"dependencies": {
|
| 9496 |
+
"@types/hast": "^3.0.0"
|
| 9497 |
},
|
| 9498 |
+
"funding": {
|
| 9499 |
+
"type": "opencollective",
|
| 9500 |
+
"url": "https://opencollective.com/unified"
|
| 9501 |
}
|
| 9502 |
},
|
| 9503 |
"node_modules/hast-util-to-jsx-runtime": {
|
|
|
|
| 9527 |
"url": "https://opencollective.com/unified"
|
| 9528 |
}
|
| 9529 |
},
|
| 9530 |
+
"node_modules/hast-util-to-text": {
|
| 9531 |
+
"version": "4.0.2",
|
| 9532 |
+
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
|
| 9533 |
+
"integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
|
| 9534 |
+
"license": "MIT",
|
| 9535 |
+
"dependencies": {
|
| 9536 |
+
"@types/hast": "^3.0.0",
|
| 9537 |
+
"@types/unist": "^3.0.0",
|
| 9538 |
+
"hast-util-is-element": "^3.0.0",
|
| 9539 |
+
"unist-util-find-after": "^5.0.0"
|
| 9540 |
+
},
|
| 9541 |
+
"funding": {
|
| 9542 |
+
"type": "opencollective",
|
| 9543 |
+
"url": "https://opencollective.com/unified"
|
| 9544 |
+
}
|
| 9545 |
+
},
|
| 9546 |
"node_modules/hast-util-whitespace": {
|
| 9547 |
"version": "3.0.0",
|
| 9548 |
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
|
|
|
| 9556 |
"url": "https://opencollective.com/unified"
|
| 9557 |
}
|
| 9558 |
},
|
| 9559 |
+
"node_modules/hastscript": {
|
| 9560 |
+
"version": "9.0.1",
|
| 9561 |
+
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
|
| 9562 |
+
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
|
| 9563 |
+
"license": "MIT",
|
| 9564 |
+
"dependencies": {
|
| 9565 |
+
"@types/hast": "^3.0.0",
|
| 9566 |
+
"comma-separated-tokens": "^2.0.0",
|
| 9567 |
+
"hast-util-parse-selector": "^4.0.0",
|
| 9568 |
+
"property-information": "^7.0.0",
|
| 9569 |
+
"space-separated-tokens": "^2.0.0"
|
| 9570 |
+
},
|
| 9571 |
+
"funding": {
|
| 9572 |
+
"type": "opencollective",
|
| 9573 |
+
"url": "https://opencollective.com/unified"
|
| 9574 |
+
}
|
| 9575 |
+
},
|
| 9576 |
"node_modules/he": {
|
| 9577 |
"version": "1.2.0",
|
| 9578 |
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
|
|
|
| 9857 |
"node": ">=10.17.0"
|
| 9858 |
}
|
| 9859 |
},
|
| 9860 |
+
"node_modules/hyphenation.de": {
|
| 9861 |
+
"version": "0.2.1",
|
| 9862 |
+
"resolved": "https://registry.npmjs.org/hyphenation.de/-/hyphenation.de-0.2.1.tgz",
|
| 9863 |
+
"integrity": "sha512-s6Y4TFA8xWjRLneOPI6HV/+wzfm2c2yurTvFaXlznmsbeI6waZhMpxu94fSXGNGsrPxrzI1zTtYDEWeEeaANnw==",
|
| 9864 |
+
"dependencies": {
|
| 9865 |
+
"hypher": "*"
|
| 9866 |
+
}
|
| 9867 |
+
},
|
| 9868 |
+
"node_modules/hyphenation.en-us": {
|
| 9869 |
+
"version": "0.2.1",
|
| 9870 |
+
"resolved": "https://registry.npmjs.org/hyphenation.en-us/-/hyphenation.en-us-0.2.1.tgz",
|
| 9871 |
+
"integrity": "sha512-ItXYgvIpfN8rfXl/GTBQC7DsSb5PPsKh9gGzViK/iWzCS5mvjDebFJ6xCcIYo8dal+nSp2rUzvTT7BosrKlL8A==",
|
| 9872 |
+
"dependencies": {
|
| 9873 |
+
"hypher": "*"
|
| 9874 |
+
}
|
| 9875 |
+
},
|
| 9876 |
+
"node_modules/hypher": {
|
| 9877 |
+
"version": "0.2.5",
|
| 9878 |
+
"resolved": "https://registry.npmjs.org/hypher/-/hypher-0.2.5.tgz",
|
| 9879 |
+
"integrity": "sha512-kUTpuyzBWWDO2VakmjHC/cxesg4lKQP+Fdc+7lrK4yvjNjkV9vm5UTZMDAwOyyHTOpbkYrAMlNZHG61NnE9vYQ==",
|
| 9880 |
+
"license": "BSD-3-Clause"
|
| 9881 |
+
},
|
| 9882 |
"node_modules/iconv-lite": {
|
| 9883 |
"version": "0.6.3",
|
| 9884 |
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
|
|
|
| 9930 |
"node": ">= 4"
|
| 9931 |
}
|
| 9932 |
},
|
| 9933 |
+
"node_modules/image-size": {
|
| 9934 |
+
"version": "1.2.1",
|
| 9935 |
+
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
|
| 9936 |
+
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
|
| 9937 |
+
"license": "MIT",
|
| 9938 |
+
"dependencies": {
|
| 9939 |
+
"queue": "6.0.2"
|
| 9940 |
+
},
|
| 9941 |
+
"bin": {
|
| 9942 |
+
"image-size": "bin/image-size.js"
|
| 9943 |
+
},
|
| 9944 |
+
"engines": {
|
| 9945 |
+
"node": ">=16.x"
|
| 9946 |
+
}
|
| 9947 |
+
},
|
| 9948 |
"node_modules/immer": {
|
| 9949 |
"version": "9.0.21",
|
| 9950 |
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
|
|
|
| 11687 |
"jiti": "bin/jiti.js"
|
| 11688 |
}
|
| 11689 |
},
|
| 11690 |
+
"node_modules/js-beautify": {
|
| 11691 |
+
"version": "1.14.11",
|
| 11692 |
+
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.11.tgz",
|
| 11693 |
+
"integrity": "sha512-rPogWqAfoYh1Ryqqh2agUpVfbxAhbjuN1SmU86dskQUKouRiggUTCO4+2ym9UPXllc2WAp0J+T5qxn7Um3lCdw==",
|
| 11694 |
+
"license": "MIT",
|
| 11695 |
+
"dependencies": {
|
| 11696 |
+
"config-chain": "^1.1.13",
|
| 11697 |
+
"editorconfig": "^1.0.3",
|
| 11698 |
+
"glob": "^10.3.3",
|
| 11699 |
+
"nopt": "^7.2.0"
|
| 11700 |
+
},
|
| 11701 |
+
"bin": {
|
| 11702 |
+
"css-beautify": "js/bin/css-beautify.js",
|
| 11703 |
+
"html-beautify": "js/bin/html-beautify.js",
|
| 11704 |
+
"js-beautify": "js/bin/js-beautify.js"
|
| 11705 |
+
},
|
| 11706 |
+
"engines": {
|
| 11707 |
+
"node": ">=14"
|
| 11708 |
+
}
|
| 11709 |
+
},
|
| 11710 |
+
"node_modules/js-beautify/node_modules/brace-expansion": {
|
| 11711 |
+
"version": "2.1.0",
|
| 11712 |
+
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
| 11713 |
+
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
| 11714 |
+
"license": "MIT",
|
| 11715 |
+
"dependencies": {
|
| 11716 |
+
"balanced-match": "^1.0.0"
|
| 11717 |
+
}
|
| 11718 |
+
},
|
| 11719 |
+
"node_modules/js-beautify/node_modules/glob": {
|
| 11720 |
+
"version": "10.5.0",
|
| 11721 |
+
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
| 11722 |
+
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
| 11723 |
+
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
| 11724 |
+
"license": "ISC",
|
| 11725 |
+
"dependencies": {
|
| 11726 |
+
"foreground-child": "^3.1.0",
|
| 11727 |
+
"jackspeak": "^3.1.2",
|
| 11728 |
+
"minimatch": "^9.0.4",
|
| 11729 |
+
"minipass": "^7.1.2",
|
| 11730 |
+
"package-json-from-dist": "^1.0.0",
|
| 11731 |
+
"path-scurry": "^1.11.1"
|
| 11732 |
+
},
|
| 11733 |
+
"bin": {
|
| 11734 |
+
"glob": "dist/esm/bin.mjs"
|
| 11735 |
+
},
|
| 11736 |
+
"funding": {
|
| 11737 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 11738 |
+
}
|
| 11739 |
+
},
|
| 11740 |
+
"node_modules/js-beautify/node_modules/minimatch": {
|
| 11741 |
+
"version": "9.0.9",
|
| 11742 |
+
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
| 11743 |
+
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
| 11744 |
+
"license": "ISC",
|
| 11745 |
+
"dependencies": {
|
| 11746 |
+
"brace-expansion": "^2.0.2"
|
| 11747 |
+
},
|
| 11748 |
+
"engines": {
|
| 11749 |
+
"node": ">=16 || 14 >=14.17"
|
| 11750 |
+
},
|
| 11751 |
+
"funding": {
|
| 11752 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 11753 |
+
}
|
| 11754 |
+
},
|
| 11755 |
"node_modules/js-tokens": {
|
| 11756 |
"version": "4.0.0",
|
| 11757 |
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
|
|
|
| 11930 |
"node": ">=4.0"
|
| 11931 |
}
|
| 11932 |
},
|
| 11933 |
+
"node_modules/katex": {
|
| 11934 |
+
"version": "0.16.45",
|
| 11935 |
+
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz",
|
| 11936 |
+
"integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==",
|
| 11937 |
+
"funding": [
|
| 11938 |
+
"https://opencollective.com/katex",
|
| 11939 |
+
"https://github.com/sponsors/katex"
|
| 11940 |
+
],
|
| 11941 |
+
"license": "MIT",
|
| 11942 |
+
"dependencies": {
|
| 11943 |
+
"commander": "^8.3.0"
|
| 11944 |
+
},
|
| 11945 |
+
"bin": {
|
| 11946 |
+
"katex": "cli.js"
|
| 11947 |
+
}
|
| 11948 |
+
},
|
| 11949 |
"node_modules/keyv": {
|
| 11950 |
"version": "4.5.4",
|
| 11951 |
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
|
|
|
| 12000 |
"node": ">=0.10"
|
| 12001 |
}
|
| 12002 |
},
|
| 12003 |
+
"node_modules/latex.js": {
|
| 12004 |
+
"version": "0.12.6",
|
| 12005 |
+
"resolved": "https://registry.npmjs.org/latex.js/-/latex.js-0.12.6.tgz",
|
| 12006 |
+
"integrity": "sha512-spMTeSq9cP4vidMQuPgoYGKmsQTMElZhDtNF3NyLIRc03XkuuPvMA0tzb0ShS/otaIJrB7DIHDk0GL3knCisEw==",
|
| 12007 |
+
"license": "MIT",
|
| 12008 |
+
"dependencies": {
|
| 12009 |
+
"commander": "8.x",
|
| 12010 |
+
"fs-extra": "10.x",
|
| 12011 |
+
"hyphenation.de": "*",
|
| 12012 |
+
"hyphenation.en-us": "*",
|
| 12013 |
+
"js-beautify": "1.14.x",
|
| 12014 |
+
"stdin": "*",
|
| 12015 |
+
"svgdom": "^0.1.8"
|
| 12016 |
+
},
|
| 12017 |
+
"bin": {
|
| 12018 |
+
"latex.js": "bin/latex.js"
|
| 12019 |
+
},
|
| 12020 |
+
"engines": {
|
| 12021 |
+
"node": ">= 14.0"
|
| 12022 |
+
}
|
| 12023 |
+
},
|
| 12024 |
"node_modules/launch-editor": {
|
| 12025 |
"version": "2.10.0",
|
| 12026 |
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz",
|
|
|
|
| 12411 |
"url": "https://opencollective.com/unified"
|
| 12412 |
}
|
| 12413 |
},
|
| 12414 |
+
"node_modules/mdast-util-math": {
|
| 12415 |
+
"version": "3.0.0",
|
| 12416 |
+
"resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz",
|
| 12417 |
+
"integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==",
|
| 12418 |
+
"license": "MIT",
|
| 12419 |
+
"dependencies": {
|
| 12420 |
+
"@types/hast": "^3.0.0",
|
| 12421 |
+
"@types/mdast": "^4.0.0",
|
| 12422 |
+
"devlop": "^1.0.0",
|
| 12423 |
+
"longest-streak": "^3.0.0",
|
| 12424 |
+
"mdast-util-from-markdown": "^2.0.0",
|
| 12425 |
+
"mdast-util-to-markdown": "^2.1.0",
|
| 12426 |
+
"unist-util-remove-position": "^5.0.0"
|
| 12427 |
+
},
|
| 12428 |
+
"funding": {
|
| 12429 |
+
"type": "opencollective",
|
| 12430 |
+
"url": "https://opencollective.com/unified"
|
| 12431 |
+
}
|
| 12432 |
+
},
|
| 12433 |
"node_modules/mdast-util-mdx-expression": {
|
| 12434 |
"version": "2.0.1",
|
| 12435 |
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
|
|
|
|
| 12809 |
"url": "https://opencollective.com/unified"
|
| 12810 |
}
|
| 12811 |
},
|
| 12812 |
+
"node_modules/micromark-extension-math": {
|
| 12813 |
+
"version": "3.1.0",
|
| 12814 |
+
"resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz",
|
| 12815 |
+
"integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==",
|
| 12816 |
+
"license": "MIT",
|
| 12817 |
+
"dependencies": {
|
| 12818 |
+
"@types/katex": "^0.16.0",
|
| 12819 |
+
"devlop": "^1.0.0",
|
| 12820 |
+
"katex": "^0.16.0",
|
| 12821 |
+
"micromark-factory-space": "^2.0.0",
|
| 12822 |
+
"micromark-util-character": "^2.0.0",
|
| 12823 |
+
"micromark-util-symbol": "^2.0.0",
|
| 12824 |
+
"micromark-util-types": "^2.0.0"
|
| 12825 |
+
},
|
| 12826 |
+
"funding": {
|
| 12827 |
+
"type": "opencollective",
|
| 12828 |
+
"url": "https://opencollective.com/unified"
|
| 12829 |
+
}
|
| 12830 |
+
},
|
| 12831 |
"node_modules/micromark-factory-destination": {
|
| 12832 |
"version": "2.0.1",
|
| 12833 |
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
|
|
|
| 13439 |
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
| 13440 |
"license": "MIT"
|
| 13441 |
},
|
| 13442 |
+
"node_modules/nopt": {
|
| 13443 |
+
"version": "7.2.1",
|
| 13444 |
+
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
|
| 13445 |
+
"integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
|
| 13446 |
+
"license": "ISC",
|
| 13447 |
+
"dependencies": {
|
| 13448 |
+
"abbrev": "^2.0.0"
|
| 13449 |
+
},
|
| 13450 |
+
"bin": {
|
| 13451 |
+
"nopt": "bin/nopt.js"
|
| 13452 |
+
},
|
| 13453 |
+
"engines": {
|
| 13454 |
+
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
| 13455 |
+
}
|
| 13456 |
+
},
|
| 13457 |
"node_modules/normalize-path": {
|
| 13458 |
"version": "3.0.0",
|
| 13459 |
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
|
|
|
| 13816 |
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
| 13817 |
"license": "BlueOak-1.0.0"
|
| 13818 |
},
|
| 13819 |
+
"node_modules/pako": {
|
| 13820 |
+
"version": "0.2.9",
|
| 13821 |
+
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
| 13822 |
+
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
|
| 13823 |
+
"license": "MIT"
|
| 13824 |
+
},
|
| 13825 |
"node_modules/param-case": {
|
| 13826 |
"version": "3.0.4",
|
| 13827 |
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
|
|
|
| 15524 |
"url": "https://github.com/sponsors/wooorm"
|
| 15525 |
}
|
| 15526 |
},
|
| 15527 |
+
"node_modules/proto-list": {
|
| 15528 |
+
"version": "1.2.4",
|
| 15529 |
+
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
| 15530 |
+
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
|
| 15531 |
+
"license": "ISC"
|
| 15532 |
+
},
|
| 15533 |
"node_modules/proxy-addr": {
|
| 15534 |
"version": "2.0.7",
|
| 15535 |
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
|
|
|
| 15605 |
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
| 15606 |
"license": "MIT"
|
| 15607 |
},
|
| 15608 |
+
"node_modules/queue": {
|
| 15609 |
+
"version": "6.0.2",
|
| 15610 |
+
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
| 15611 |
+
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
| 15612 |
+
"license": "MIT",
|
| 15613 |
+
"dependencies": {
|
| 15614 |
+
"inherits": "~2.0.3"
|
| 15615 |
+
}
|
| 15616 |
+
},
|
| 15617 |
"node_modules/queue-microtask": {
|
| 15618 |
"version": "1.2.3",
|
| 15619 |
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
|
|
|
| 16180 |
"node": ">=6"
|
| 16181 |
}
|
| 16182 |
},
|
| 16183 |
+
"node_modules/rehype-katex": {
|
| 16184 |
+
"version": "7.0.1",
|
| 16185 |
+
"resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz",
|
| 16186 |
+
"integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==",
|
| 16187 |
+
"license": "MIT",
|
| 16188 |
+
"dependencies": {
|
| 16189 |
+
"@types/hast": "^3.0.0",
|
| 16190 |
+
"@types/katex": "^0.16.0",
|
| 16191 |
+
"hast-util-from-html-isomorphic": "^2.0.0",
|
| 16192 |
+
"hast-util-to-text": "^4.0.0",
|
| 16193 |
+
"katex": "^0.16.0",
|
| 16194 |
+
"unist-util-visit-parents": "^6.0.0",
|
| 16195 |
+
"vfile": "^6.0.0"
|
| 16196 |
+
},
|
| 16197 |
+
"funding": {
|
| 16198 |
+
"type": "opencollective",
|
| 16199 |
+
"url": "https://opencollective.com/unified"
|
| 16200 |
+
}
|
| 16201 |
+
},
|
| 16202 |
"node_modules/relateurl": {
|
| 16203 |
"version": "0.2.7",
|
| 16204 |
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
|
|
|
|
| 16226 |
"url": "https://opencollective.com/unified"
|
| 16227 |
}
|
| 16228 |
},
|
| 16229 |
+
"node_modules/remark-math": {
|
| 16230 |
+
"version": "6.0.0",
|
| 16231 |
+
"resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz",
|
| 16232 |
+
"integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==",
|
| 16233 |
+
"license": "MIT",
|
| 16234 |
+
"dependencies": {
|
| 16235 |
+
"@types/mdast": "^4.0.0",
|
| 16236 |
+
"mdast-util-math": "^3.0.0",
|
| 16237 |
+
"micromark-extension-math": "^3.0.0",
|
| 16238 |
+
"unified": "^11.0.0"
|
| 16239 |
+
},
|
| 16240 |
+
"funding": {
|
| 16241 |
+
"type": "opencollective",
|
| 16242 |
+
"url": "https://opencollective.com/unified"
|
| 16243 |
+
}
|
| 16244 |
+
},
|
| 16245 |
"node_modules/remark-parse": {
|
| 16246 |
"version": "11.0.0",
|
| 16247 |
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
|
|
|
| 16327 |
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
| 16328 |
"license": "MIT"
|
| 16329 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16330 |
"node_modules/resolve": {
|
| 16331 |
"version": "1.22.10",
|
| 16332 |
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
|
|
|
| 16443 |
"node": ">=10"
|
| 16444 |
}
|
| 16445 |
},
|
| 16446 |
+
"node_modules/restructure": {
|
| 16447 |
+
"version": "3.0.2",
|
| 16448 |
+
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
| 16449 |
+
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
|
| 16450 |
+
"license": "MIT"
|
| 16451 |
+
},
|
| 16452 |
"node_modules/retry": {
|
| 16453 |
"version": "0.13.1",
|
| 16454 |
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
|
|
|
| 17390 |
"node": ">= 0.8"
|
| 17391 |
}
|
| 17392 |
},
|
| 17393 |
+
"node_modules/stdin": {
|
| 17394 |
+
"version": "0.0.1",
|
| 17395 |
+
"resolved": "https://registry.npmjs.org/stdin/-/stdin-0.0.1.tgz",
|
| 17396 |
+
"integrity": "sha512-2bacd1TXzqOEsqRa+eEWkRdOSznwptrs4gqFcpMq5tOtmJUGPZd10W5Lam6wQ4YQ/+qjQt4e9u35yXCF6mrlfQ=="
|
| 17397 |
+
},
|
| 17398 |
"node_modules/stop-iteration-iterator": {
|
| 17399 |
"version": "1.1.0",
|
| 17400 |
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
|
|
|
| 17704 |
"webpack": "^5.0.0"
|
| 17705 |
}
|
| 17706 |
},
|
| 17707 |
+
"node_modules/style-mod": {
|
| 17708 |
+
"version": "4.1.3",
|
| 17709 |
+
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
| 17710 |
+
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
| 17711 |
+
"license": "MIT"
|
| 17712 |
+
},
|
| 17713 |
"node_modules/style-to-js": {
|
| 17714 |
"version": "1.1.17",
|
| 17715 |
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz",
|
|
|
|
| 17862 |
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
|
| 17863 |
"license": "MIT"
|
| 17864 |
},
|
| 17865 |
+
"node_modules/svgdom": {
|
| 17866 |
+
"version": "0.1.23",
|
| 17867 |
+
"resolved": "https://registry.npmjs.org/svgdom/-/svgdom-0.1.23.tgz",
|
| 17868 |
+
"integrity": "sha512-zTT8cz8rf07OCvRhTipJv+bt/MgL5Zm2ZF3IttK9zr/MKVDTrgo+usbDQMOK1PGzqDT7GA1SshnYlXvtafK7Fw==",
|
| 17869 |
+
"license": "MIT",
|
| 17870 |
+
"dependencies": {
|
| 17871 |
+
"fontkit": "^2.0.4",
|
| 17872 |
+
"image-size": "^1.2.1",
|
| 17873 |
+
"sax": "^1.4.1"
|
| 17874 |
+
},
|
| 17875 |
+
"funding": {
|
| 17876 |
+
"type": "github",
|
| 17877 |
+
"url": "https://github.com/sponsors/Fuzzyma"
|
| 17878 |
+
}
|
| 17879 |
+
},
|
| 17880 |
+
"node_modules/svgdom/node_modules/sax": {
|
| 17881 |
+
"version": "1.6.0",
|
| 17882 |
+
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
| 17883 |
+
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
| 17884 |
+
"license": "BlueOak-1.0.0",
|
| 17885 |
+
"engines": {
|
| 17886 |
+
"node": ">=11.0.0"
|
| 17887 |
+
}
|
| 17888 |
+
},
|
| 17889 |
"node_modules/svgo": {
|
| 17890 |
"version": "1.3.2",
|
| 17891 |
"resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
|
|
|
|
| 18225 |
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
| 18226 |
"license": "MIT"
|
| 18227 |
},
|
| 18228 |
+
"node_modules/tiny-inflate": {
|
| 18229 |
+
"version": "1.0.3",
|
| 18230 |
+
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
| 18231 |
+
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
| 18232 |
+
"license": "MIT"
|
| 18233 |
+
},
|
| 18234 |
"node_modules/tmpl": {
|
| 18235 |
"version": "1.0.5",
|
| 18236 |
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
|
|
|
| 18515 |
"is-typedarray": "^1.0.0"
|
| 18516 |
}
|
| 18517 |
},
|
| 18518 |
+
"node_modules/typescript": {
|
| 18519 |
+
"version": "4.9.5",
|
| 18520 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
| 18521 |
+
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
| 18522 |
+
"license": "Apache-2.0",
|
| 18523 |
+
"peer": true,
|
| 18524 |
+
"bin": {
|
| 18525 |
+
"tsc": "bin/tsc",
|
| 18526 |
+
"tsserver": "bin/tsserver"
|
| 18527 |
+
},
|
| 18528 |
+
"engines": {
|
| 18529 |
+
"node": ">=4.2.0"
|
| 18530 |
+
}
|
| 18531 |
+
},
|
| 18532 |
"node_modules/unbox-primitive": {
|
| 18533 |
"version": "1.1.0",
|
| 18534 |
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
|
|
|
| 18590 |
"node": ">=4"
|
| 18591 |
}
|
| 18592 |
},
|
| 18593 |
+
"node_modules/unicode-properties": {
|
| 18594 |
+
"version": "1.4.1",
|
| 18595 |
+
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
| 18596 |
+
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
| 18597 |
+
"license": "MIT",
|
| 18598 |
+
"dependencies": {
|
| 18599 |
+
"base64-js": "^1.3.0",
|
| 18600 |
+
"unicode-trie": "^2.0.0"
|
| 18601 |
+
}
|
| 18602 |
+
},
|
| 18603 |
"node_modules/unicode-property-aliases-ecmascript": {
|
| 18604 |
"version": "2.1.0",
|
| 18605 |
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
|
|
|
|
| 18609 |
"node": ">=4"
|
| 18610 |
}
|
| 18611 |
},
|
| 18612 |
+
"node_modules/unicode-trie": {
|
| 18613 |
+
"version": "2.0.0",
|
| 18614 |
+
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
| 18615 |
+
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
| 18616 |
+
"license": "MIT",
|
| 18617 |
+
"dependencies": {
|
| 18618 |
+
"pako": "^0.2.5",
|
| 18619 |
+
"tiny-inflate": "^1.0.0"
|
| 18620 |
+
}
|
| 18621 |
+
},
|
| 18622 |
"node_modules/unified": {
|
| 18623 |
"version": "11.0.5",
|
| 18624 |
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
|
|
|
| 18662 |
"node": ">=8"
|
| 18663 |
}
|
| 18664 |
},
|
| 18665 |
+
"node_modules/unist-util-find-after": {
|
| 18666 |
+
"version": "5.0.0",
|
| 18667 |
+
"resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
|
| 18668 |
+
"integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
|
| 18669 |
+
"license": "MIT",
|
| 18670 |
+
"dependencies": {
|
| 18671 |
+
"@types/unist": "^3.0.0",
|
| 18672 |
+
"unist-util-is": "^6.0.0"
|
| 18673 |
+
},
|
| 18674 |
+
"funding": {
|
| 18675 |
+
"type": "opencollective",
|
| 18676 |
+
"url": "https://opencollective.com/unified"
|
| 18677 |
+
}
|
| 18678 |
+
},
|
| 18679 |
"node_modules/unist-util-is": {
|
| 18680 |
"version": "6.0.0",
|
| 18681 |
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
|
|
|
|
| 18702 |
"url": "https://opencollective.com/unified"
|
| 18703 |
}
|
| 18704 |
},
|
| 18705 |
+
"node_modules/unist-util-remove-position": {
|
| 18706 |
+
"version": "5.0.0",
|
| 18707 |
+
"resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
|
| 18708 |
+
"integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==",
|
| 18709 |
+
"license": "MIT",
|
| 18710 |
+
"dependencies": {
|
| 18711 |
+
"@types/unist": "^3.0.0",
|
| 18712 |
+
"unist-util-visit": "^5.0.0"
|
| 18713 |
+
},
|
| 18714 |
+
"funding": {
|
| 18715 |
+
"type": "opencollective",
|
| 18716 |
+
"url": "https://opencollective.com/unified"
|
| 18717 |
+
}
|
| 18718 |
+
},
|
| 18719 |
"node_modules/unist-util-stringify-position": {
|
| 18720 |
"version": "4.0.0",
|
| 18721 |
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
|
|
|
| 18929 |
"url": "https://opencollective.com/unified"
|
| 18930 |
}
|
| 18931 |
},
|
| 18932 |
+
"node_modules/vfile-location": {
|
| 18933 |
+
"version": "5.0.3",
|
| 18934 |
+
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
|
| 18935 |
+
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
|
| 18936 |
+
"license": "MIT",
|
| 18937 |
+
"dependencies": {
|
| 18938 |
+
"@types/unist": "^3.0.0",
|
| 18939 |
+
"vfile": "^6.0.0"
|
| 18940 |
+
},
|
| 18941 |
+
"funding": {
|
| 18942 |
+
"type": "opencollective",
|
| 18943 |
+
"url": "https://opencollective.com/unified"
|
| 18944 |
+
}
|
| 18945 |
+
},
|
| 18946 |
"node_modules/vfile-message": {
|
| 18947 |
"version": "4.0.3",
|
| 18948 |
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
|
|
|
|
| 18967 |
"browser-process-hrtime": "^1.0.0"
|
| 18968 |
}
|
| 18969 |
},
|
| 18970 |
+
"node_modules/w3c-keyname": {
|
| 18971 |
+
"version": "2.2.8",
|
| 18972 |
+
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
| 18973 |
+
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
| 18974 |
+
"license": "MIT"
|
| 18975 |
+
},
|
| 18976 |
"node_modules/w3c-xmlserializer": {
|
| 18977 |
"version": "2.0.0",
|
| 18978 |
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
|
|
|
|
| 19016 |
"minimalistic-assert": "^1.0.0"
|
| 19017 |
}
|
| 19018 |
},
|
| 19019 |
+
"node_modules/web-namespaces": {
|
| 19020 |
+
"version": "2.0.1",
|
| 19021 |
+
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
|
| 19022 |
+
"integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
|
| 19023 |
+
"license": "MIT",
|
| 19024 |
+
"funding": {
|
| 19025 |
+
"type": "github",
|
| 19026 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 19027 |
+
}
|
| 19028 |
+
},
|
| 19029 |
"node_modules/web-vitals": {
|
| 19030 |
"version": "2.1.4",
|
| 19031 |
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
|
phd-advisor-frontend/package.json
CHANGED
|
@@ -3,17 +3,22 @@
|
|
| 3 |
"version": "0.1.0",
|
| 4 |
"private": true,
|
| 5 |
"dependencies": {
|
|
|
|
| 6 |
"@testing-library/dom": "^10.4.0",
|
| 7 |
"@testing-library/jest-dom": "^6.6.3",
|
| 8 |
"@testing-library/react": "^16.3.0",
|
| 9 |
"@testing-library/user-event": "^13.5.0",
|
|
|
|
|
|
|
|
|
|
| 10 |
"lucide-react": "^0.544.0",
|
| 11 |
"react": "^19.1.0",
|
| 12 |
"react-dom": "^19.1.0",
|
| 13 |
-
"@reactour/tour": "^3.8.0",
|
| 14 |
"react-markdown": "^10.1.0",
|
| 15 |
"react-scripts": "5.0.1",
|
|
|
|
| 16 |
"remark-gfm": "^4.0.1",
|
|
|
|
| 17 |
"web-vitals": "^2.1.4"
|
| 18 |
},
|
| 19 |
"scripts": {
|
|
|
|
| 3 |
"version": "0.1.0",
|
| 4 |
"private": true,
|
| 5 |
"dependencies": {
|
| 6 |
+
"@codemirror/legacy-modes": "^6.5.2",
|
| 7 |
"@testing-library/dom": "^10.4.0",
|
| 8 |
"@testing-library/jest-dom": "^6.6.3",
|
| 9 |
"@testing-library/react": "^16.3.0",
|
| 10 |
"@testing-library/user-event": "^13.5.0",
|
| 11 |
+
"@uiw/react-codemirror": "^4.25.9",
|
| 12 |
+
"katex": "^0.16.45",
|
| 13 |
+
"latex.js": "^0.12.6",
|
| 14 |
"lucide-react": "^0.544.0",
|
| 15 |
"react": "^19.1.0",
|
| 16 |
"react-dom": "^19.1.0",
|
|
|
|
| 17 |
"react-markdown": "^10.1.0",
|
| 18 |
"react-scripts": "5.0.1",
|
| 19 |
+
"rehype-katex": "^7.0.1",
|
| 20 |
"remark-gfm": "^4.0.1",
|
| 21 |
+
"remark-math": "^6.0.0",
|
| 22 |
"web-vitals": "^2.1.4"
|
| 23 |
},
|
| 24 |
"scripts": {
|
phd-advisor-frontend/public/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
| 7 |
<meta name="theme-color" content="#000000" />
|
| 8 |
<meta
|
| 9 |
name="description"
|
| 10 |
-
content="
|
| 11 |
/>
|
| 12 |
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
| 13 |
<!--
|
|
@@ -24,7 +24,7 @@
|
|
| 24 |
work correctly both with client-side routing and a non-root public URL.
|
| 25 |
Learn how to configure a non-root public URL by running `npm run build`.
|
| 26 |
-->
|
| 27 |
-
<title>
|
| 28 |
</head>
|
| 29 |
<body>
|
| 30 |
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
|
|
| 7 |
<meta name="theme-color" content="#000000" />
|
| 8 |
<meta
|
| 9 |
name="description"
|
| 10 |
+
content="Cybersecurity Advisor — AI-powered security guidance from expert advisors"
|
| 11 |
/>
|
| 12 |
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
| 13 |
<!--
|
|
|
|
| 24 |
work correctly both with client-side routing and a non-root public URL.
|
| 25 |
Learn how to configure a non-root public URL by running `npm run build`.
|
| 26 |
-->
|
| 27 |
+
<title>Cybersecurity Advisor</title>
|
| 28 |
</head>
|
| 29 |
<body>
|
| 30 |
<noscript>You need to enable JavaScript to run this app.</noscript>
|
phd-advisor-frontend/public/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
-
"short_name": "
|
| 3 |
-
"name": "
|
| 4 |
"icons": [
|
| 5 |
{
|
| 6 |
"src": "favicon.ico",
|
|
|
|
| 1 |
{
|
| 2 |
+
"short_name": "Cybersecurity Advisor",
|
| 3 |
+
"name": "Cybersecurity Advisor",
|
| 4 |
"icons": [
|
| 5 |
{
|
| 6 |
"src": "favicon.ico",
|
phd-advisor-frontend/src/App.js
CHANGED
|
@@ -8,11 +8,6 @@ import CanvasPage from './pages/CanvasPage';
|
|
| 8 |
import UserGuide from './components/UserGuide';
|
| 9 |
import './styles/components.css';
|
| 10 |
|
| 11 |
-
// Set REACT_APP_TESTING_ONBOARDING=true in your .env to force the onboarding
|
| 12 |
-
// tour to run on every page load. Leave unset in production — tour will only
|
| 13 |
-
// show once per user (localStorage).
|
| 14 |
-
export const TESTING_ONBOARDING = process.env.REACT_APP_TESTING_ONBOARDING === 'true';
|
| 15 |
-
|
| 16 |
function App() {
|
| 17 |
const [currentView, setCurrentView] = useState('home');
|
| 18 |
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
@@ -43,7 +38,10 @@ function App() {
|
|
| 43 |
setCurrentView('auth');
|
| 44 |
};
|
| 45 |
|
| 46 |
-
const navigateToCanvas = () => {
|
|
|
|
|
|
|
|
|
|
| 47 |
setCurrentView('canvas');
|
| 48 |
};
|
| 49 |
|
|
@@ -64,11 +62,6 @@ function App() {
|
|
| 64 |
setCurrentView('chat');
|
| 65 |
};
|
| 66 |
|
| 67 |
-
const handleUserUpdate = (updatedUser) => {
|
| 68 |
-
setUser(updatedUser);
|
| 69 |
-
localStorage.setItem('user', JSON.stringify(updatedUser));
|
| 70 |
-
};
|
| 71 |
-
|
| 72 |
const handleSignOut = () => {
|
| 73 |
localStorage.removeItem('authToken');
|
| 74 |
localStorage.removeItem('user');
|
|
@@ -84,7 +77,9 @@ function App() {
|
|
| 84 |
<div className="App">
|
| 85 |
{currentView === 'home' && (
|
| 86 |
<HomePage
|
|
|
|
| 87 |
onNavigateToChat={isAuthenticated ? navigateToChat : navigateToAuth}
|
|
|
|
| 88 |
isAuthenticated={isAuthenticated}
|
| 89 |
/>
|
| 90 |
)}
|
|
@@ -92,9 +87,10 @@ function App() {
|
|
| 92 |
<AuthPage onAuthSuccess={handleAuthSuccess} />
|
| 93 |
)}
|
| 94 |
{currentView === 'canvas' && isAuthenticated && (
|
| 95 |
-
<CanvasPage
|
| 96 |
user={user}
|
| 97 |
authToken={authToken}
|
|
|
|
| 98 |
onNavigateToChat={navigateToChat}
|
| 99 |
onSignOut={handleSignOut}
|
| 100 |
/>
|
|
@@ -106,7 +102,6 @@ function App() {
|
|
| 106 |
onNavigateToHome={navigateToHome}
|
| 107 |
onNavigateToCanvas={navigateToCanvas}
|
| 108 |
onSignOut={handleSignOut}
|
| 109 |
-
onUserUpdate={handleUserUpdate}
|
| 110 |
/>
|
| 111 |
)}
|
| 112 |
{/* Global help center — listens for the 'open-user-guide' event */}
|
|
|
|
| 8 |
import UserGuide from './components/UserGuide';
|
| 9 |
import './styles/components.css';
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
function App() {
|
| 12 |
const [currentView, setCurrentView] = useState('home');
|
| 13 |
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
|
|
| 38 |
setCurrentView('auth');
|
| 39 |
};
|
| 40 |
|
| 41 |
+
const navigateToCanvas = (canvasView) => {
|
| 42 |
+
if (['insights', 'workspace', 'deliverables'].includes(canvasView)) {
|
| 43 |
+
localStorage.setItem('canvas-view-v2', canvasView);
|
| 44 |
+
}
|
| 45 |
setCurrentView('canvas');
|
| 46 |
};
|
| 47 |
|
|
|
|
| 62 |
setCurrentView('chat');
|
| 63 |
};
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
const handleSignOut = () => {
|
| 66 |
localStorage.removeItem('authToken');
|
| 67 |
localStorage.removeItem('user');
|
|
|
|
| 77 |
<div className="App">
|
| 78 |
{currentView === 'home' && (
|
| 79 |
<HomePage
|
| 80 |
+
onNavigateToHome={navigateToHome}
|
| 81 |
onNavigateToChat={isAuthenticated ? navigateToChat : navigateToAuth}
|
| 82 |
+
onNavigateToCanvas={isAuthenticated ? navigateToCanvas : navigateToAuth}
|
| 83 |
isAuthenticated={isAuthenticated}
|
| 84 |
/>
|
| 85 |
)}
|
|
|
|
| 87 |
<AuthPage onAuthSuccess={handleAuthSuccess} />
|
| 88 |
)}
|
| 89 |
{currentView === 'canvas' && isAuthenticated && (
|
| 90 |
+
<CanvasPage
|
| 91 |
user={user}
|
| 92 |
authToken={authToken}
|
| 93 |
+
onNavigateToHome={navigateToHome}
|
| 94 |
onNavigateToChat={navigateToChat}
|
| 95 |
onSignOut={handleSignOut}
|
| 96 |
/>
|
|
|
|
| 102 |
onNavigateToHome={navigateToHome}
|
| 103 |
onNavigateToCanvas={navigateToCanvas}
|
| 104 |
onSignOut={handleSignOut}
|
|
|
|
| 105 |
/>
|
| 106 |
)}
|
| 107 |
{/* Global help center — listens for the 'open-user-guide' event */}
|
phd-advisor-frontend/src/components/AdvisorStatusDropdown.js
CHANGED
|
@@ -2,12 +2,19 @@ import React, { useState, useEffect } from 'react';
|
|
| 2 |
import { Users, ChevronDown, Pencil } from 'lucide-react';
|
| 3 |
import AvatarPickerModal from './AvatarPickerModal';
|
| 4 |
|
| 5 |
-
const AdvisorStatusDropdown = ({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
const [isOpen, setIsOpen] = useState(false);
|
| 7 |
const [hoveredId, setHoveredId] = useState(null);
|
| 8 |
const [pickerAdvisor, setPickerAdvisor] = useState(null);
|
| 9 |
-
|
| 10 |
-
// Close dropdown when clicking outside
|
| 11 |
useEffect(() => {
|
| 12 |
const handleClickOutside = (event) => {
|
| 13 |
if (isOpen && !event.target.closest('.advisor-status-dropdown')) {
|
|
@@ -22,10 +29,13 @@ const AdvisorStatusDropdown = ({ advisors, thinkingAdvisors, getAdvisorColors, i
|
|
| 22 |
if (!advisors || typeof advisors !== 'object') {
|
| 23 |
return null;
|
| 24 |
}
|
| 25 |
-
|
| 26 |
const advisorEntries = Object.entries(advisors);
|
| 27 |
-
const
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
| 29 |
: 0;
|
| 30 |
const totalAdvisors = advisorEntries.length;
|
| 31 |
|
|
@@ -33,16 +43,41 @@ const AdvisorStatusDropdown = ({ advisors, thinkingAdvisors, getAdvisorColors, i
|
|
| 33 |
setIsOpen(!isOpen);
|
| 34 |
};
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
return (
|
| 37 |
<div className="advisor-status-dropdown">
|
| 38 |
-
<button
|
|
|
|
| 39 |
className={`advisor-status-button ${isOpen ? 'open' : ''}`}
|
| 40 |
onClick={handleToggle}
|
|
|
|
|
|
|
|
|
|
| 41 |
>
|
| 42 |
<div className="advisor-status-info">
|
| 43 |
<Users size={16} />
|
| 44 |
<span className="advisor-count">
|
| 45 |
-
{
|
| 46 |
</span>
|
| 47 |
{thinkingCount > 0 && (
|
| 48 |
<div className="thinking-badge">
|
|
@@ -52,7 +87,7 @@ const AdvisorStatusDropdown = ({ advisors, thinkingAdvisors, getAdvisorColors, i
|
|
| 52 |
</div>
|
| 53 |
<ChevronDown size={14} className={`dropdown-arrow ${isOpen ? 'rotated' : ''}`} />
|
| 54 |
</button>
|
| 55 |
-
|
| 56 |
{pickerAdvisor && (
|
| 57 |
<AvatarPickerModal
|
| 58 |
advisorId={pickerAdvisor.id}
|
|
@@ -61,32 +96,62 @@ const AdvisorStatusDropdown = ({ advisors, thinkingAdvisors, getAdvisorColors, i
|
|
| 61 |
/>
|
| 62 |
)}
|
| 63 |
{isOpen && (
|
| 64 |
-
<div className="advisor-dropdown-panel">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
<div className="advisor-list">
|
| 66 |
{advisorEntries.map(([id, advisor]) => {
|
| 67 |
const IconComponent = advisor.icon;
|
| 68 |
const colors = getAdvisorColors(id, isDark);
|
| 69 |
const isThinking = Array.isArray(thinkingAdvisors) && thinkingAdvisors.includes(id);
|
| 70 |
-
|
|
|
|
| 71 |
return (
|
| 72 |
<div
|
| 73 |
key={id}
|
| 74 |
-
className={`advisor-item ${isThinking ? 'thinking' : ''}`}
|
| 75 |
style={{ '--advisor-color': colors.color, '--advisor-bg': colors.bgColor }}
|
| 76 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
<div
|
| 78 |
className="advisor-icon"
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
onMouseEnter={() => setHoveredId(id)}
|
| 81 |
onMouseLeave={() => setHoveredId(null)}
|
| 82 |
-
onClick={() => setPickerAdvisor({ id, name: advisor.name })}
|
| 83 |
>
|
| 84 |
{advisor.avatarUrl
|
| 85 |
-
? <img src={advisor.avatarUrl} alt={advisor.name}
|
| 86 |
-
: <IconComponent size={16} />
|
| 87 |
-
}
|
| 88 |
{hoveredId === id && (
|
| 89 |
-
<div
|
| 90 |
<Pencil size={10} color="#fff" />
|
| 91 |
</div>
|
| 92 |
)}
|
|
@@ -96,12 +161,14 @@ const AdvisorStatusDropdown = ({ advisors, thinkingAdvisors, getAdvisorColors, i
|
|
| 96 |
<div className="advisor-description">{advisor.description}</div>
|
| 97 |
</div>
|
| 98 |
<div className="advisor-status">
|
| 99 |
-
{
|
|
|
|
|
|
|
| 100 |
<div className="status-thinking">
|
| 101 |
<div className="thinking-dots">
|
| 102 |
-
<div className="dot"
|
| 103 |
-
<div className="dot"
|
| 104 |
-
<div className="dot"
|
| 105 |
</div>
|
| 106 |
</div>
|
| 107 |
) : (
|
|
@@ -114,13 +181,13 @@ const AdvisorStatusDropdown = ({ advisors, thinkingAdvisors, getAdvisorColors, i
|
|
| 114 |
</div>
|
| 115 |
</div>
|
| 116 |
)}
|
| 117 |
-
|
| 118 |
<style>{`
|
| 119 |
.advisor-status-dropdown {
|
| 120 |
position: relative;
|
| 121 |
display: inline-block;
|
| 122 |
}
|
| 123 |
-
|
| 124 |
.advisor-status-button {
|
| 125 |
display: flex;
|
| 126 |
align-items: center;
|
|
@@ -136,31 +203,32 @@ const AdvisorStatusDropdown = ({ advisors, thinkingAdvisors, getAdvisorColors, i
|
|
| 136 |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
| 137 |
color: var(--text-primary);
|
| 138 |
}
|
| 139 |
-
|
| 140 |
.advisor-status-button:hover {
|
| 141 |
background: var(--bg-secondary);
|
| 142 |
border-color: var(--accent-primary);
|
| 143 |
transform: translateY(-1px);
|
| 144 |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
| 145 |
}
|
| 146 |
-
|
| 147 |
.advisor-status-button.open {
|
| 148 |
background: var(--bg-secondary);
|
| 149 |
border-color: var(--accent-primary);
|
| 150 |
}
|
| 151 |
-
|
| 152 |
.advisor-status-info {
|
| 153 |
display: flex;
|
| 154 |
align-items: center;
|
| 155 |
gap: 6px;
|
| 156 |
flex: 1;
|
| 157 |
}
|
| 158 |
-
|
| 159 |
.advisor-count {
|
| 160 |
font-weight: 600;
|
| 161 |
color: var(--text-primary);
|
|
|
|
| 162 |
}
|
| 163 |
-
|
| 164 |
.thinking-badge {
|
| 165 |
background: var(--accent-primary);
|
| 166 |
color: white;
|
|
@@ -170,22 +238,23 @@ const AdvisorStatusDropdown = ({ advisors, thinkingAdvisors, getAdvisorColors, i
|
|
| 170 |
font-weight: 600;
|
| 171 |
animation: pulse 2s ease-in-out infinite;
|
| 172 |
}
|
| 173 |
-
|
| 174 |
.dropdown-arrow {
|
| 175 |
color: var(--text-secondary);
|
| 176 |
transition: transform 0.2s ease;
|
|
|
|
| 177 |
}
|
| 178 |
-
|
| 179 |
.dropdown-arrow.rotated {
|
| 180 |
transform: rotate(180deg);
|
| 181 |
}
|
| 182 |
-
|
| 183 |
.advisor-dropdown-panel {
|
| 184 |
position: absolute;
|
| 185 |
top: calc(100% + 8px);
|
| 186 |
right: 0;
|
| 187 |
-
min-width:
|
| 188 |
-
max-width:
|
| 189 |
background: var(--bg-primary);
|
| 190 |
border: 1px solid var(--border-primary);
|
| 191 |
border-radius: 12px;
|
|
@@ -195,77 +264,147 @@ const AdvisorStatusDropdown = ({ advisors, thinkingAdvisors, getAdvisorColors, i
|
|
| 195 |
backdrop-filter: blur(20px);
|
| 196 |
-webkit-backdrop-filter: blur(20px);
|
| 197 |
}
|
| 198 |
-
|
| 199 |
[data-theme="dark"] .advisor-dropdown-panel {
|
| 200 |
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
|
| 201 |
}
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
.advisor-list {
|
| 204 |
-
max-height:
|
| 205 |
overflow-y: auto;
|
| 206 |
scrollbar-width: thin;
|
| 207 |
scrollbar-color: var(--border-primary) transparent;
|
| 208 |
}
|
| 209 |
-
|
| 210 |
-
.advisor-list::-webkit-scrollbar {
|
| 211 |
-
width: 6px;
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
-
.advisor-list::-webkit-scrollbar-track {
|
| 215 |
-
background: transparent;
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
.advisor-list::-webkit-scrollbar-thumb {
|
| 219 |
-
background: var(--border-primary);
|
| 220 |
-
border-radius: 3px;
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
.advisor-item {
|
| 224 |
display: flex;
|
| 225 |
align-items: center;
|
| 226 |
-
gap:
|
| 227 |
-
padding:
|
| 228 |
border-bottom: 1px solid var(--border-primary);
|
| 229 |
-
transition: background-color 0.2s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
}
|
| 231 |
-
|
| 232 |
.advisor-item:last-child {
|
| 233 |
border-bottom: none;
|
| 234 |
}
|
| 235 |
-
|
| 236 |
.advisor-item:hover {
|
| 237 |
background: var(--bg-secondary);
|
| 238 |
}
|
| 239 |
-
|
| 240 |
.advisor-item.thinking {
|
| 241 |
background: var(--advisor-bg);
|
|
|
|
| 242 |
}
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
.advisor-icon {
|
|
|
|
|
|
|
|
|
|
| 245 |
width: 32px;
|
| 246 |
height: 32px;
|
| 247 |
border-radius: 8px;
|
|
|
|
| 248 |
background: var(--advisor-bg);
|
| 249 |
color: var(--advisor-color);
|
| 250 |
display: flex;
|
| 251 |
align-items: center;
|
| 252 |
justify-content: center;
|
| 253 |
-
flex-shrink: 0;
|
| 254 |
border: 1px solid var(--advisor-color);
|
| 255 |
}
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
.advisor-details {
|
| 258 |
flex: 1;
|
| 259 |
min-width: 0;
|
| 260 |
}
|
| 261 |
-
|
| 262 |
.advisor-name {
|
| 263 |
font-weight: 600;
|
| 264 |
color: var(--text-primary);
|
| 265 |
font-size: 13px;
|
| 266 |
margin-bottom: 2px;
|
| 267 |
}
|
| 268 |
-
|
| 269 |
.advisor-description {
|
| 270 |
font-size: 11px;
|
| 271 |
color: var(--text-secondary);
|
|
@@ -274,22 +413,27 @@ const AdvisorStatusDropdown = ({ advisors, thinkingAdvisors, getAdvisorColors, i
|
|
| 274 |
text-overflow: ellipsis;
|
| 275 |
white-space: nowrap;
|
| 276 |
}
|
| 277 |
-
|
| 278 |
.advisor-status {
|
| 279 |
flex-shrink: 0;
|
| 280 |
}
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
.status-thinking {
|
| 283 |
display: flex;
|
| 284 |
align-items: center;
|
| 285 |
-
gap: 4px;
|
| 286 |
}
|
| 287 |
-
|
| 288 |
.thinking-dots {
|
| 289 |
display: flex;
|
| 290 |
gap: 2px;
|
| 291 |
}
|
| 292 |
-
|
| 293 |
.thinking-dots .dot {
|
| 294 |
width: 4px;
|
| 295 |
height: 4px;
|
|
@@ -297,64 +441,34 @@ const AdvisorStatusDropdown = ({ advisors, thinkingAdvisors, getAdvisorColors, i
|
|
| 297 |
border-radius: 50%;
|
| 298 |
animation: thinking-bounce 1.4s infinite ease-in-out both;
|
| 299 |
}
|
| 300 |
-
|
| 301 |
.thinking-dots .dot:nth-child(1) { animation-delay: -0.32s; }
|
| 302 |
.thinking-dots .dot:nth-child(2) { animation-delay: -0.16s; }
|
| 303 |
-
|
| 304 |
-
|
| 305 |
.status-ready {
|
| 306 |
font-size: 11px;
|
| 307 |
color: var(--text-tertiary);
|
| 308 |
font-weight: 500;
|
| 309 |
}
|
| 310 |
-
|
| 311 |
@keyframes thinking-bounce {
|
| 312 |
0%, 80%, 100% { transform: scale(0); }
|
| 313 |
40% { transform: scale(1); }
|
| 314 |
}
|
| 315 |
-
|
| 316 |
@keyframes pulse {
|
| 317 |
0%, 100% { opacity: 1; }
|
| 318 |
50% { opacity: 0.7; }
|
| 319 |
}
|
| 320 |
-
|
| 321 |
-
/* Responsive Design */
|
| 322 |
@media (max-width: 768px) {
|
| 323 |
.advisor-status-dropdown {
|
| 324 |
display: none;
|
| 325 |
}
|
| 326 |
-
.advisor-dropdown-panel {
|
| 327 |
-
right: -20px;
|
| 328 |
-
left: -20px;
|
| 329 |
-
min-width: unset;
|
| 330 |
-
max-width: unset;
|
| 331 |
-
}
|
| 332 |
-
|
| 333 |
-
.advisor-status-button {
|
| 334 |
-
min-width: 120px;
|
| 335 |
-
font-size: 12px;
|
| 336 |
-
}
|
| 337 |
-
|
| 338 |
-
.advisor-item {
|
| 339 |
-
padding: 10px 12px;
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
.advisor-icon {
|
| 343 |
-
width: 28px;
|
| 344 |
-
height: 28px;
|
| 345 |
-
}
|
| 346 |
-
|
| 347 |
-
.advisor-name {
|
| 348 |
-
font-size: 12px;
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
.advisor-description {
|
| 352 |
-
font-size: 10px;
|
| 353 |
-
}
|
| 354 |
}
|
| 355 |
`}</style>
|
| 356 |
</div>
|
| 357 |
);
|
| 358 |
};
|
| 359 |
|
| 360 |
-
export default AdvisorStatusDropdown;
|
|
|
|
| 2 |
import { Users, ChevronDown, Pencil } from 'lucide-react';
|
| 3 |
import AvatarPickerModal from './AvatarPickerModal';
|
| 4 |
|
| 5 |
+
const AdvisorStatusDropdown = ({
|
| 6 |
+
advisors,
|
| 7 |
+
activeAdvisorIds = [],
|
| 8 |
+
onToggleAdvisor,
|
| 9 |
+
onSetActiveAdvisors,
|
| 10 |
+
thinkingAdvisors,
|
| 11 |
+
getAdvisorColors,
|
| 12 |
+
isDark,
|
| 13 |
+
}) => {
|
| 14 |
const [isOpen, setIsOpen] = useState(false);
|
| 15 |
const [hoveredId, setHoveredId] = useState(null);
|
| 16 |
const [pickerAdvisor, setPickerAdvisor] = useState(null);
|
| 17 |
+
|
|
|
|
| 18 |
useEffect(() => {
|
| 19 |
const handleClickOutside = (event) => {
|
| 20 |
if (isOpen && !event.target.closest('.advisor-status-dropdown')) {
|
|
|
|
| 29 |
if (!advisors || typeof advisors !== 'object') {
|
| 30 |
return null;
|
| 31 |
}
|
| 32 |
+
|
| 33 |
const advisorEntries = Object.entries(advisors);
|
| 34 |
+
const allIds = advisorEntries.map(([id]) => id);
|
| 35 |
+
const activeSet = new Set(activeAdvisorIds);
|
| 36 |
+
const activeCount = allIds.filter((id) => activeSet.has(id)).length;
|
| 37 |
+
const thinkingCount = Array.isArray(thinkingAdvisors)
|
| 38 |
+
? thinkingAdvisors.filter((id) => id !== 'system' && activeSet.has(id)).length
|
| 39 |
: 0;
|
| 40 |
const totalAdvisors = advisorEntries.length;
|
| 41 |
|
|
|
|
| 43 |
setIsOpen(!isOpen);
|
| 44 |
};
|
| 45 |
|
| 46 |
+
const handleCheckboxChange = (id, event) => {
|
| 47 |
+
event.stopPropagation();
|
| 48 |
+
if (onToggleAdvisor) {
|
| 49 |
+
onToggleAdvisor(id);
|
| 50 |
+
}
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
const selectAll = (event) => {
|
| 54 |
+
event.stopPropagation();
|
| 55 |
+
if (onSetActiveAdvisors) {
|
| 56 |
+
onSetActiveAdvisors([...allIds]);
|
| 57 |
+
}
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
const selectNone = (event) => {
|
| 61 |
+
event.stopPropagation();
|
| 62 |
+
if (onSetActiveAdvisors && allIds.length > 0) {
|
| 63 |
+
onSetActiveAdvisors([allIds[0]]);
|
| 64 |
+
}
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
return (
|
| 68 |
<div className="advisor-status-dropdown">
|
| 69 |
+
<button
|
| 70 |
+
type="button"
|
| 71 |
className={`advisor-status-button ${isOpen ? 'open' : ''}`}
|
| 72 |
onClick={handleToggle}
|
| 73 |
+
title="Choose which advisors are active"
|
| 74 |
+
aria-expanded={isOpen}
|
| 75 |
+
aria-haspopup="listbox"
|
| 76 |
>
|
| 77 |
<div className="advisor-status-info">
|
| 78 |
<Users size={16} />
|
| 79 |
<span className="advisor-count">
|
| 80 |
+
{activeCount}/{totalAdvisors} Active
|
| 81 |
</span>
|
| 82 |
{thinkingCount > 0 && (
|
| 83 |
<div className="thinking-badge">
|
|
|
|
| 87 |
</div>
|
| 88 |
<ChevronDown size={14} className={`dropdown-arrow ${isOpen ? 'rotated' : ''}`} />
|
| 89 |
</button>
|
| 90 |
+
|
| 91 |
{pickerAdvisor && (
|
| 92 |
<AvatarPickerModal
|
| 93 |
advisorId={pickerAdvisor.id}
|
|
|
|
| 96 |
/>
|
| 97 |
)}
|
| 98 |
{isOpen && (
|
| 99 |
+
<div className="advisor-dropdown-panel" role="listbox" aria-label="Active advisors">
|
| 100 |
+
<div className="advisor-panel-header">
|
| 101 |
+
<div className="advisor-panel-title">Active advisors</div>
|
| 102 |
+
<div className="advisor-panel-subtitle">
|
| 103 |
+
Only checked advisors respond to your messages.
|
| 104 |
+
</div>
|
| 105 |
+
<div className="advisor-panel-actions">
|
| 106 |
+
<button type="button" className="advisor-panel-link" onClick={selectAll}>
|
| 107 |
+
Select all
|
| 108 |
+
</button>
|
| 109 |
+
<span className="advisor-panel-sep">·</span>
|
| 110 |
+
<button type="button" className="advisor-panel-link" onClick={selectNone}>
|
| 111 |
+
Minimize
|
| 112 |
+
</button>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
<div className="advisor-list">
|
| 116 |
{advisorEntries.map(([id, advisor]) => {
|
| 117 |
const IconComponent = advisor.icon;
|
| 118 |
const colors = getAdvisorColors(id, isDark);
|
| 119 |
const isThinking = Array.isArray(thinkingAdvisors) && thinkingAdvisors.includes(id);
|
| 120 |
+
const isActive = activeSet.has(id);
|
| 121 |
+
|
| 122 |
return (
|
| 123 |
<div
|
| 124 |
key={id}
|
| 125 |
+
className={`advisor-item ${isThinking ? 'thinking' : ''} ${isActive ? '' : 'inactive'}`}
|
| 126 |
style={{ '--advisor-color': colors.color, '--advisor-bg': colors.bgColor }}
|
| 127 |
>
|
| 128 |
+
<label className="advisor-active-toggle" onClick={(e) => e.stopPropagation()}>
|
| 129 |
+
<input
|
| 130 |
+
type="checkbox"
|
| 131 |
+
checked={isActive}
|
| 132 |
+
onChange={(e) => handleCheckboxChange(id, e)}
|
| 133 |
+
aria-label={`${isActive ? 'Deactivate' : 'Activate'} ${advisor.name}`}
|
| 134 |
+
/>
|
| 135 |
+
</label>
|
| 136 |
<div
|
| 137 |
className="advisor-icon"
|
| 138 |
+
role="button"
|
| 139 |
+
tabIndex={0}
|
| 140 |
+
onKeyDown={(e) => {
|
| 141 |
+
if (e.key === 'Enter' || e.key === ' ') {
|
| 142 |
+
e.preventDefault();
|
| 143 |
+
setPickerAdvisor({ id, name: advisor.name });
|
| 144 |
+
}
|
| 145 |
+
}}
|
| 146 |
+
onClick={() => setPickerAdvisor({ id, name: advisor.name })}
|
| 147 |
onMouseEnter={() => setHoveredId(id)}
|
| 148 |
onMouseLeave={() => setHoveredId(null)}
|
|
|
|
| 149 |
>
|
| 150 |
{advisor.avatarUrl
|
| 151 |
+
? <img src={advisor.avatarUrl} alt={advisor.name} />
|
| 152 |
+
: <IconComponent size={16} />}
|
|
|
|
| 153 |
{hoveredId === id && (
|
| 154 |
+
<div className="advisor-icon-edit">
|
| 155 |
<Pencil size={10} color="#fff" />
|
| 156 |
</div>
|
| 157 |
)}
|
|
|
|
| 161 |
<div className="advisor-description">{advisor.description}</div>
|
| 162 |
</div>
|
| 163 |
<div className="advisor-status">
|
| 164 |
+
{!isActive ? (
|
| 165 |
+
<div className="status-inactive">Off</div>
|
| 166 |
+
) : isThinking ? (
|
| 167 |
<div className="status-thinking">
|
| 168 |
<div className="thinking-dots">
|
| 169 |
+
<div className="dot" />
|
| 170 |
+
<div className="dot" />
|
| 171 |
+
<div className="dot" />
|
| 172 |
</div>
|
| 173 |
</div>
|
| 174 |
) : (
|
|
|
|
| 181 |
</div>
|
| 182 |
</div>
|
| 183 |
)}
|
| 184 |
+
|
| 185 |
<style>{`
|
| 186 |
.advisor-status-dropdown {
|
| 187 |
position: relative;
|
| 188 |
display: inline-block;
|
| 189 |
}
|
| 190 |
+
|
| 191 |
.advisor-status-button {
|
| 192 |
display: flex;
|
| 193 |
align-items: center;
|
|
|
|
| 203 |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
| 204 |
color: var(--text-primary);
|
| 205 |
}
|
| 206 |
+
|
| 207 |
.advisor-status-button:hover {
|
| 208 |
background: var(--bg-secondary);
|
| 209 |
border-color: var(--accent-primary);
|
| 210 |
transform: translateY(-1px);
|
| 211 |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
| 212 |
}
|
| 213 |
+
|
| 214 |
.advisor-status-button.open {
|
| 215 |
background: var(--bg-secondary);
|
| 216 |
border-color: var(--accent-primary);
|
| 217 |
}
|
| 218 |
+
|
| 219 |
.advisor-status-info {
|
| 220 |
display: flex;
|
| 221 |
align-items: center;
|
| 222 |
gap: 6px;
|
| 223 |
flex: 1;
|
| 224 |
}
|
| 225 |
+
|
| 226 |
.advisor-count {
|
| 227 |
font-weight: 600;
|
| 228 |
color: var(--text-primary);
|
| 229 |
+
white-space: nowrap;
|
| 230 |
}
|
| 231 |
+
|
| 232 |
.thinking-badge {
|
| 233 |
background: var(--accent-primary);
|
| 234 |
color: white;
|
|
|
|
| 238 |
font-weight: 600;
|
| 239 |
animation: pulse 2s ease-in-out infinite;
|
| 240 |
}
|
| 241 |
+
|
| 242 |
.dropdown-arrow {
|
| 243 |
color: var(--text-secondary);
|
| 244 |
transition: transform 0.2s ease;
|
| 245 |
+
flex-shrink: 0;
|
| 246 |
}
|
| 247 |
+
|
| 248 |
.dropdown-arrow.rotated {
|
| 249 |
transform: rotate(180deg);
|
| 250 |
}
|
| 251 |
+
|
| 252 |
.advisor-dropdown-panel {
|
| 253 |
position: absolute;
|
| 254 |
top: calc(100% + 8px);
|
| 255 |
right: 0;
|
| 256 |
+
min-width: 300px;
|
| 257 |
+
max-width: 340px;
|
| 258 |
background: var(--bg-primary);
|
| 259 |
border: 1px solid var(--border-primary);
|
| 260 |
border-radius: 12px;
|
|
|
|
| 264 |
backdrop-filter: blur(20px);
|
| 265 |
-webkit-backdrop-filter: blur(20px);
|
| 266 |
}
|
| 267 |
+
|
| 268 |
[data-theme="dark"] .advisor-dropdown-panel {
|
| 269 |
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
|
| 270 |
}
|
| 271 |
+
|
| 272 |
+
.advisor-panel-header {
|
| 273 |
+
padding: 12px 16px 8px;
|
| 274 |
+
border-bottom: 1px solid var(--border-primary);
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.advisor-panel-title {
|
| 278 |
+
font-size: 13px;
|
| 279 |
+
font-weight: 600;
|
| 280 |
+
color: var(--text-primary);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.advisor-panel-subtitle {
|
| 284 |
+
font-size: 11px;
|
| 285 |
+
color: var(--text-secondary);
|
| 286 |
+
margin-top: 2px;
|
| 287 |
+
line-height: 1.35;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.advisor-panel-actions {
|
| 291 |
+
margin-top: 8px;
|
| 292 |
+
display: flex;
|
| 293 |
+
align-items: center;
|
| 294 |
+
gap: 6px;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.advisor-panel-link {
|
| 298 |
+
background: none;
|
| 299 |
+
border: none;
|
| 300 |
+
padding: 0;
|
| 301 |
+
font-size: 11px;
|
| 302 |
+
font-weight: 600;
|
| 303 |
+
color: var(--accent-primary);
|
| 304 |
+
cursor: pointer;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.advisor-panel-link:hover {
|
| 308 |
+
text-decoration: underline;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.advisor-panel-sep {
|
| 312 |
+
color: var(--text-tertiary);
|
| 313 |
+
font-size: 11px;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
.advisor-list {
|
| 317 |
+
max-height: 320px;
|
| 318 |
overflow-y: auto;
|
| 319 |
scrollbar-width: thin;
|
| 320 |
scrollbar-color: var(--border-primary) transparent;
|
| 321 |
}
|
| 322 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
.advisor-item {
|
| 324 |
display: flex;
|
| 325 |
align-items: center;
|
| 326 |
+
gap: 10px;
|
| 327 |
+
padding: 10px 14px;
|
| 328 |
border-bottom: 1px solid var(--border-primary);
|
| 329 |
+
transition: background-color 0.2s ease, opacity 0.2s ease;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.advisor-item.inactive {
|
| 333 |
+
opacity: 0.55;
|
| 334 |
}
|
| 335 |
+
|
| 336 |
.advisor-item:last-child {
|
| 337 |
border-bottom: none;
|
| 338 |
}
|
| 339 |
+
|
| 340 |
.advisor-item:hover {
|
| 341 |
background: var(--bg-secondary);
|
| 342 |
}
|
| 343 |
+
|
| 344 |
.advisor-item.thinking {
|
| 345 |
background: var(--advisor-bg);
|
| 346 |
+
opacity: 1;
|
| 347 |
}
|
| 348 |
+
|
| 349 |
+
.advisor-active-toggle {
|
| 350 |
+
display: flex;
|
| 351 |
+
align-items: center;
|
| 352 |
+
flex-shrink: 0;
|
| 353 |
+
cursor: pointer;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.advisor-active-toggle input {
|
| 357 |
+
width: 16px;
|
| 358 |
+
height: 16px;
|
| 359 |
+
accent-color: var(--accent-primary);
|
| 360 |
+
cursor: pointer;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
.advisor-icon {
|
| 364 |
+
position: relative;
|
| 365 |
+
cursor: pointer;
|
| 366 |
+
overflow: hidden;
|
| 367 |
width: 32px;
|
| 368 |
height: 32px;
|
| 369 |
border-radius: 8px;
|
| 370 |
+
flex-shrink: 0;
|
| 371 |
background: var(--advisor-bg);
|
| 372 |
color: var(--advisor-color);
|
| 373 |
display: flex;
|
| 374 |
align-items: center;
|
| 375 |
justify-content: center;
|
|
|
|
| 376 |
border: 1px solid var(--advisor-color);
|
| 377 |
}
|
| 378 |
+
|
| 379 |
+
.advisor-icon img {
|
| 380 |
+
width: 32px;
|
| 381 |
+
height: 32px;
|
| 382 |
+
object-fit: cover;
|
| 383 |
+
display: block;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.advisor-icon-edit {
|
| 387 |
+
position: absolute;
|
| 388 |
+
inset: 0;
|
| 389 |
+
border-radius: 8px;
|
| 390 |
+
background: rgba(0, 0, 0, 0.45);
|
| 391 |
+
display: flex;
|
| 392 |
+
align-items: center;
|
| 393 |
+
justify-content: center;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
.advisor-details {
|
| 397 |
flex: 1;
|
| 398 |
min-width: 0;
|
| 399 |
}
|
| 400 |
+
|
| 401 |
.advisor-name {
|
| 402 |
font-weight: 600;
|
| 403 |
color: var(--text-primary);
|
| 404 |
font-size: 13px;
|
| 405 |
margin-bottom: 2px;
|
| 406 |
}
|
| 407 |
+
|
| 408 |
.advisor-description {
|
| 409 |
font-size: 11px;
|
| 410 |
color: var(--text-secondary);
|
|
|
|
| 413 |
text-overflow: ellipsis;
|
| 414 |
white-space: nowrap;
|
| 415 |
}
|
| 416 |
+
|
| 417 |
.advisor-status {
|
| 418 |
flex-shrink: 0;
|
| 419 |
}
|
| 420 |
+
|
| 421 |
+
.status-inactive {
|
| 422 |
+
font-size: 11px;
|
| 423 |
+
color: var(--text-tertiary);
|
| 424 |
+
font-weight: 500;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
.status-thinking {
|
| 428 |
display: flex;
|
| 429 |
align-items: center;
|
|
|
|
| 430 |
}
|
| 431 |
+
|
| 432 |
.thinking-dots {
|
| 433 |
display: flex;
|
| 434 |
gap: 2px;
|
| 435 |
}
|
| 436 |
+
|
| 437 |
.thinking-dots .dot {
|
| 438 |
width: 4px;
|
| 439 |
height: 4px;
|
|
|
|
| 441 |
border-radius: 50%;
|
| 442 |
animation: thinking-bounce 1.4s infinite ease-in-out both;
|
| 443 |
}
|
| 444 |
+
|
| 445 |
.thinking-dots .dot:nth-child(1) { animation-delay: -0.32s; }
|
| 446 |
.thinking-dots .dot:nth-child(2) { animation-delay: -0.16s; }
|
| 447 |
+
|
|
|
|
| 448 |
.status-ready {
|
| 449 |
font-size: 11px;
|
| 450 |
color: var(--text-tertiary);
|
| 451 |
font-weight: 500;
|
| 452 |
}
|
| 453 |
+
|
| 454 |
@keyframes thinking-bounce {
|
| 455 |
0%, 80%, 100% { transform: scale(0); }
|
| 456 |
40% { transform: scale(1); }
|
| 457 |
}
|
| 458 |
+
|
| 459 |
@keyframes pulse {
|
| 460 |
0%, 100% { opacity: 1; }
|
| 461 |
50% { opacity: 0.7; }
|
| 462 |
}
|
| 463 |
+
|
|
|
|
| 464 |
@media (max-width: 768px) {
|
| 465 |
.advisor-status-dropdown {
|
| 466 |
display: none;
|
| 467 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
}
|
| 469 |
`}</style>
|
| 470 |
</div>
|
| 471 |
);
|
| 472 |
};
|
| 473 |
|
| 474 |
+
export default AdvisorStatusDropdown;
|
phd-advisor-frontend/src/components/CopyrightNotice.js
CHANGED
|
@@ -22,7 +22,7 @@ const CopyrightNotice = ({ variant = 'footer', className = '' }) => {
|
|
| 22 |
Neon.ai
|
| 23 |
</a>
|
| 24 |
)}
|
| 25 |
-
|
| 26 |
<a
|
| 27 |
href="https://www.neon.ai/contact"
|
| 28 |
target="_blank"
|
|
|
|
| 22 |
Neon.ai
|
| 23 |
</a>
|
| 24 |
)}
|
| 25 |
+
. All rights reserved.{' '}
|
| 26 |
<a
|
| 27 |
href="https://www.neon.ai/contact"
|
| 28 |
target="_blank"
|
phd-advisor-frontend/src/components/MessageBubble.js
CHANGED
|
@@ -302,34 +302,38 @@ const MessageBubble = ({
|
|
| 302 |
const colors = getAdvisorColors(personaId, isDark);
|
| 303 |
const isCopied = copiedStates[message.id];
|
| 304 |
|
| 305 |
-
const avatarElement = (size =
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
src={advisor.avatarUrl}
|
| 309 |
-
alt={advisor.name || 'Advisor'}
|
| 310 |
-
style={{ width: size, height: size, borderRadius: '50%', objectFit: 'cover', flexShrink: 0 }}
|
| 311 |
-
/>
|
| 312 |
-
) : Icon ? (
|
| 313 |
<div
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, overflow: 'hidden'
|
| 317 |
-
}}
|
| 318 |
>
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
</div>
|
| 331 |
-
)
|
| 332 |
-
|
| 333 |
|
| 334 |
return (
|
| 335 |
<div className={`advisor-message-container ${inlineAvatar ? 'inline-avatar-mode' : ''}`}>
|
|
@@ -350,7 +354,7 @@ const MessageBubble = ({
|
|
| 350 |
}}
|
| 351 |
>
|
| 352 |
<div className="advisor-message-header">
|
| 353 |
-
{inlineAvatar && avatarElement(
|
| 354 |
<h4
|
| 355 |
className="advisor-message-name"
|
| 356 |
style={{ color: colors.color || 'var(--text-primary)' }}
|
|
|
|
| 302 |
const colors = getAdvisorColors(personaId, isDark);
|
| 303 |
const isCopied = copiedStates[message.id];
|
| 304 |
|
| 305 |
+
const avatarElement = (size = 44) => {
|
| 306 |
+
const iconSize = Math.round(size * 0.52);
|
| 307 |
+
return (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
<div
|
| 309 |
+
className="advisor-message-avatar-ring"
|
| 310 |
+
style={{ width: size, height: size }}
|
|
|
|
|
|
|
| 311 |
>
|
| 312 |
+
{advisor.avatarUrl ? (
|
| 313 |
+
<img
|
| 314 |
+
src={advisor.avatarUrl}
|
| 315 |
+
alt={advisor.name || 'Advisor'}
|
| 316 |
+
/>
|
| 317 |
+
) : Icon ? (
|
| 318 |
+
<Icon
|
| 319 |
+
className="advisor-message-avatar-icon"
|
| 320 |
+
style={{
|
| 321 |
+
color: colors.color || 'var(--text-secondary)',
|
| 322 |
+
width: iconSize,
|
| 323 |
+
height: iconSize,
|
| 324 |
+
}}
|
| 325 |
+
/>
|
| 326 |
+
) : (
|
| 327 |
+
<span
|
| 328 |
+
className="advisor-message-avatar-initial"
|
| 329 |
+
style={{ color: colors.color || 'var(--text-secondary)', fontSize: iconSize }}
|
| 330 |
+
>
|
| 331 |
+
{advisor.name ? advisor.name.charAt(0) : 'A'}
|
| 332 |
+
</span>
|
| 333 |
+
)}
|
| 334 |
</div>
|
| 335 |
+
);
|
| 336 |
+
};
|
| 337 |
|
| 338 |
return (
|
| 339 |
<div className={`advisor-message-container ${inlineAvatar ? 'inline-avatar-mode' : ''}`}>
|
|
|
|
| 354 |
}}
|
| 355 |
>
|
| 356 |
<div className="advisor-message-header">
|
| 357 |
+
{inlineAvatar && avatarElement(44)}
|
| 358 |
<h4
|
| 359 |
className="advisor-message-name"
|
| 360 |
style={{ color: colors.color || 'var(--text-primary)' }}
|
phd-advisor-frontend/src/components/Sidebar.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
| 1 |
import React, { useState, useEffect } from 'react';
|
| 2 |
import {
|
| 3 |
MessageSquare,
|
| 4 |
-
Plus,
|
| 5 |
SquarePen,
|
| 6 |
Search,
|
| 7 |
MoreVertical,
|
| 8 |
Trash2,
|
| 9 |
-
Edit3,
|
| 10 |
LogOut,
|
| 11 |
User,
|
| 12 |
-
|
|
|
|
|
|
|
| 13 |
PanelLeft,
|
| 14 |
-
FileText
|
|
|
|
|
|
|
| 15 |
} from 'lucide-react';
|
|
|
|
| 16 |
import { useAppConfig } from '../contexts/AppConfigContext';
|
| 17 |
-
import
|
| 18 |
import CopyrightNotice from './CopyrightNotice';
|
| 19 |
-
import SettingsModal from './SettingsModal';
|
| 20 |
import '../styles/Sidebar.css';
|
| 21 |
|
| 22 |
const Sidebar = ({
|
|
@@ -25,27 +27,46 @@ const Sidebar = ({
|
|
| 25 |
onSelectSession,
|
| 26 |
onNewChat,
|
| 27 |
onSignOut,
|
| 28 |
-
onUserUpdate,
|
| 29 |
authToken,
|
| 30 |
onSidebarToggle,
|
| 31 |
isMobileOpen = false,
|
| 32 |
onMobileToggle,
|
| 33 |
-
onNavigateToCanvas,
|
| 34 |
refreshTrigger,
|
| 35 |
-
onCurrentSessionDeleted
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
}) => {
|
| 37 |
const { config } = useAppConfig();
|
| 38 |
-
const
|
| 39 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
const [chatSessions, setChatSessions] = useState([]);
|
| 41 |
const [searchTerm, setSearchTerm] = useState('');
|
| 42 |
const [isLoading, setIsLoading] = useState(true);
|
| 43 |
const [showUserMenu, setShowUserMenu] = useState(false);
|
| 44 |
-
const [showSettings, setShowSettings] = useState(false);
|
| 45 |
const [isCollapsed, setIsCollapsed] = useState(false);
|
| 46 |
const [isCreatingNewChat, setIsCreatingNewChat] = useState(false);
|
| 47 |
-
const [showClearAllConfirm, setShowClearAllConfirm] = useState(false);
|
| 48 |
-
const [isClearingAll, setIsClearingAll] = useState(false);
|
| 49 |
|
| 50 |
useEffect(() => {
|
| 51 |
if (authToken) {
|
|
@@ -159,28 +180,6 @@ const Sidebar = ({
|
|
| 159 |
}
|
| 160 |
};
|
| 161 |
|
| 162 |
-
const handleClearAllChats = async () => {
|
| 163 |
-
setIsClearingAll(true);
|
| 164 |
-
try {
|
| 165 |
-
const response = await fetch(`${process.env.REACT_APP_API_URL}/api/chat-sessions`, {
|
| 166 |
-
method: 'DELETE',
|
| 167 |
-
headers: {
|
| 168 |
-
'Authorization': `Bearer ${authToken}`,
|
| 169 |
-
'Content-Type': 'application/json'
|
| 170 |
-
}
|
| 171 |
-
});
|
| 172 |
-
if (response.ok) {
|
| 173 |
-
setChatSessions([]);
|
| 174 |
-
onCurrentSessionDeleted?.();
|
| 175 |
-
}
|
| 176 |
-
} catch (error) {
|
| 177 |
-
console.error('Error clearing all chat sessions:', error);
|
| 178 |
-
} finally {
|
| 179 |
-
setIsClearingAll(false);
|
| 180 |
-
setShowClearAllConfirm(false);
|
| 181 |
-
}
|
| 182 |
-
};
|
| 183 |
-
|
| 184 |
const toggleSidebar = () => {
|
| 185 |
setIsCollapsed(!isCollapsed);
|
| 186 |
// Close user menu when collapsing
|
|
@@ -214,8 +213,17 @@ const Sidebar = ({
|
|
| 214 |
<>
|
| 215 |
<div className="user-section">
|
| 216 |
<div className="user-info">
|
| 217 |
-
<div
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
</div>
|
| 220 |
<div className="user-details">
|
| 221 |
<span className="user-name">{user.firstName} {user.lastName}</span>
|
|
@@ -243,12 +251,21 @@ const Sidebar = ({
|
|
| 243 |
|
| 244 |
{showUserMenu && (
|
| 245 |
<div className="user-menu">
|
| 246 |
-
<button
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
>
|
| 250 |
-
|
| 251 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
</button>
|
| 253 |
<button className="user-menu-item sign-out" onClick={onSignOut}>
|
| 254 |
<LogOut size={16} />
|
|
@@ -260,34 +277,6 @@ const Sidebar = ({
|
|
| 260 |
</div>
|
| 261 |
</div>
|
| 262 |
|
| 263 |
-
<div className="new-chat-row">
|
| 264 |
-
<button
|
| 265 |
-
className="new-chat-button"
|
| 266 |
-
onClick={handleNewChat}
|
| 267 |
-
disabled={isCreatingNewChat}
|
| 268 |
-
>
|
| 269 |
-
<Plus size={16} />
|
| 270 |
-
<span>{isCreatingNewChat ? 'Creating...' : 'New Chat'}</span>
|
| 271 |
-
</button>
|
| 272 |
-
<button
|
| 273 |
-
className="clear-all-chats-btn"
|
| 274 |
-
onClick={() => setShowClearAllConfirm(true)}
|
| 275 |
-
disabled={isClearingAll || chatSessions.length === 0}
|
| 276 |
-
title="Clear all chats"
|
| 277 |
-
aria-label="Clear all chats"
|
| 278 |
-
>
|
| 279 |
-
<Trash2 size={16} />
|
| 280 |
-
</button>
|
| 281 |
-
</div>
|
| 282 |
-
|
| 283 |
-
<button
|
| 284 |
-
className="sidebar-canvas-btn"
|
| 285 |
-
onClick={onNavigateToCanvas}
|
| 286 |
-
title={canvasLabel}
|
| 287 |
-
>
|
| 288 |
-
<FileText size={18} />
|
| 289 |
-
{!isCollapsed && <span>{canvasLabel}</span>}
|
| 290 |
-
</button>
|
| 291 |
</>
|
| 292 |
)}
|
| 293 |
|
|
@@ -309,14 +298,6 @@ const Sidebar = ({
|
|
| 309 |
>
|
| 310 |
<SquarePen size={20} />
|
| 311 |
</button>
|
| 312 |
-
<button
|
| 313 |
-
className="sidebar-canvas-btn"
|
| 314 |
-
onClick={onNavigateToCanvas}
|
| 315 |
-
title={canvasLabel}
|
| 316 |
-
>
|
| 317 |
-
<FileText size={20} />
|
| 318 |
-
{!isCollapsed && <span>{canvasLabel}</span>}
|
| 319 |
-
</button>
|
| 320 |
</div>
|
| 321 |
)}
|
| 322 |
</div>
|
|
@@ -328,24 +309,187 @@ const Sidebar = ({
|
|
| 328 |
<Search size={16} className="search-icon" />
|
| 329 |
<input
|
| 330 |
type="text"
|
| 331 |
-
placeholder=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
value={searchTerm}
|
| 333 |
onChange={(e) => setSearchTerm(e.target.value)}
|
| 334 |
className="search-input"
|
| 335 |
/>
|
| 336 |
</div>
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
| 345 |
</div>
|
| 346 |
)}
|
| 347 |
|
| 348 |
-
{/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
<div className="chat-sessions">
|
| 350 |
{isLoading ? (
|
| 351 |
<div className="loading-sessions">
|
|
@@ -398,15 +542,8 @@ const Sidebar = ({
|
|
| 398 |
</div>
|
| 399 |
)}
|
| 400 |
</div>
|
| 401 |
-
|
| 402 |
-
{appVersion && (
|
| 403 |
-
<div
|
| 404 |
-
className="sidebar-version"
|
| 405 |
-
title={`Version ${appVersion}`}
|
| 406 |
-
>
|
| 407 |
-
{isCollapsed ? `v${appVersion}` : `Version ${appVersion}`}
|
| 408 |
-
</div>
|
| 409 |
)}
|
|
|
|
| 410 |
{/* Footer */}
|
| 411 |
<div className={`sidebar-footer ${isCollapsed ? 'collapsed' : ''}`}>
|
| 412 |
<a
|
|
@@ -423,34 +560,23 @@ const Sidebar = ({
|
|
| 423 |
</div>
|
| 424 |
</div>
|
| 425 |
|
| 426 |
-
{
|
| 427 |
-
<
|
| 428 |
-
|
| 429 |
-
|
|
|
|
|
|
|
| 430 |
/>
|
| 431 |
)}
|
| 432 |
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
confirmLabel={isClearingAll ? 'Clearing…' : 'Clear all chats'}
|
| 438 |
-
cancelLabel="Cancel"
|
| 439 |
-
tone="danger"
|
| 440 |
-
onConfirm={handleClearAllChats}
|
| 441 |
-
onCancel={() => setShowClearAllConfirm(false)}
|
| 442 |
-
/>
|
| 443 |
-
{showSettings && (
|
| 444 |
-
<SettingsModal
|
| 445 |
-
user={user}
|
| 446 |
-
authToken={authToken}
|
| 447 |
-
onUserUpdate={onUserUpdate}
|
| 448 |
-
onSignOut={onSignOut}
|
| 449 |
-
onClose={() => setShowSettings(false)}
|
| 450 |
/>
|
| 451 |
)}
|
| 452 |
</>
|
| 453 |
);
|
| 454 |
};
|
| 455 |
|
| 456 |
-
export default Sidebar;
|
|
|
|
| 1 |
import React, { useState, useEffect } from 'react';
|
| 2 |
import {
|
| 3 |
MessageSquare,
|
|
|
|
| 4 |
SquarePen,
|
| 5 |
Search,
|
| 6 |
MoreVertical,
|
| 7 |
Trash2,
|
|
|
|
| 8 |
LogOut,
|
| 9 |
User,
|
| 10 |
+
UserCircle,
|
| 11 |
+
DatabaseZap,
|
| 12 |
+
KeyRound,
|
| 13 |
PanelLeft,
|
| 14 |
+
FileText,
|
| 15 |
+
ChevronRight,
|
| 16 |
+
Clock
|
| 17 |
} from 'lucide-react';
|
| 18 |
+
import * as LucideIcons from 'lucide-react';
|
| 19 |
import { useAppConfig } from '../contexts/AppConfigContext';
|
| 20 |
+
import UserAvatarPicker from './UserAvatarPicker';
|
| 21 |
import CopyrightNotice from './CopyrightNotice';
|
|
|
|
| 22 |
import '../styles/Sidebar.css';
|
| 23 |
|
| 24 |
const Sidebar = ({
|
|
|
|
| 27 |
onSelectSession,
|
| 28 |
onNewChat,
|
| 29 |
onSignOut,
|
|
|
|
| 30 |
authToken,
|
| 31 |
onSidebarToggle,
|
| 32 |
isMobileOpen = false,
|
| 33 |
onMobileToggle,
|
|
|
|
| 34 |
refreshTrigger,
|
| 35 |
+
onCurrentSessionDeleted,
|
| 36 |
+
pageContext = 'chat',
|
| 37 |
+
canvasItems = [],
|
| 38 |
+
canvasSubview = 'workspace',
|
| 39 |
+
widgetGroups = [],
|
| 40 |
+
deliverableProjects = [],
|
| 41 |
+
insightSections = [],
|
| 42 |
+
userAvatarId,
|
| 43 |
+
onAvatarChange,
|
| 44 |
+
onOpenProfile,
|
| 45 |
+
onOpenAccount,
|
| 46 |
+
onOpenClearData,
|
| 47 |
}) => {
|
| 48 |
const { config } = useAppConfig();
|
| 49 |
+
const isOnCanvas = pageContext === 'canvas';
|
| 50 |
+
const [showAvatarPicker, setShowAvatarPicker] = useState(false);
|
| 51 |
+
const avatarOptions = config?.app?.user_avatars || [];
|
| 52 |
+
const currentAvatar = avatarOptions.find(a => a.id === userAvatarId);
|
| 53 |
+
const AvatarIcon = currentAvatar ? (LucideIcons[currentAvatar.icon] || User) : User;
|
| 54 |
+
const [expanded, setExpanded] = useState(() => {
|
| 55 |
+
try { return JSON.parse(localStorage.getItem('sidebar-expanded-v1') || '{}'); } catch { return {}; }
|
| 56 |
+
});
|
| 57 |
+
const toggleExpanded = (key) => {
|
| 58 |
+
setExpanded(prev => {
|
| 59 |
+
const next = { ...prev, [key]: !prev[key] };
|
| 60 |
+
localStorage.setItem('sidebar-expanded-v1', JSON.stringify(next));
|
| 61 |
+
return next;
|
| 62 |
+
});
|
| 63 |
+
};
|
| 64 |
const [chatSessions, setChatSessions] = useState([]);
|
| 65 |
const [searchTerm, setSearchTerm] = useState('');
|
| 66 |
const [isLoading, setIsLoading] = useState(true);
|
| 67 |
const [showUserMenu, setShowUserMenu] = useState(false);
|
|
|
|
| 68 |
const [isCollapsed, setIsCollapsed] = useState(false);
|
| 69 |
const [isCreatingNewChat, setIsCreatingNewChat] = useState(false);
|
|
|
|
|
|
|
| 70 |
|
| 71 |
useEffect(() => {
|
| 72 |
if (authToken) {
|
|
|
|
| 180 |
}
|
| 181 |
};
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
const toggleSidebar = () => {
|
| 184 |
setIsCollapsed(!isCollapsed);
|
| 185 |
// Close user menu when collapsing
|
|
|
|
| 213 |
<>
|
| 214 |
<div className="user-section">
|
| 215 |
<div className="user-info">
|
| 216 |
+
<div
|
| 217 |
+
className="user-avatar"
|
| 218 |
+
onClick={() => onAvatarChange && setShowAvatarPicker(true)}
|
| 219 |
+
style={{
|
| 220 |
+
cursor: onAvatarChange ? 'pointer' : undefined,
|
| 221 |
+
backgroundColor: currentAvatar?.bg || undefined,
|
| 222 |
+
color: currentAvatar?.color || undefined,
|
| 223 |
+
}}
|
| 224 |
+
title={onAvatarChange ? 'Change avatar' : undefined}
|
| 225 |
+
>
|
| 226 |
+
<AvatarIcon size={20} />
|
| 227 |
</div>
|
| 228 |
<div className="user-details">
|
| 229 |
<span className="user-name">{user.firstName} {user.lastName}</span>
|
|
|
|
| 251 |
|
| 252 |
{showUserMenu && (
|
| 253 |
<div className="user-menu">
|
| 254 |
+
<button className="user-menu-item" onClick={() => { setShowUserMenu(false); setShowAvatarPicker(true); }}>
|
| 255 |
+
<User size={16} />
|
| 256 |
+
<span>Change Avatar</span>
|
| 257 |
+
</button>
|
| 258 |
+
<button className="user-menu-item" onClick={() => { setShowUserMenu(false); if (onOpenProfile) onOpenProfile(); }}>
|
| 259 |
+
<UserCircle size={16} />
|
| 260 |
+
<span>Profile</span>
|
| 261 |
+
</button>
|
| 262 |
+
<button className="user-menu-item" onClick={() => { setShowUserMenu(false); if (onOpenAccount) onOpenAccount(); }}>
|
| 263 |
+
<KeyRound size={16} />
|
| 264 |
+
<span>Account</span>
|
| 265 |
+
</button>
|
| 266 |
+
<button className="user-menu-item" onClick={() => { setShowUserMenu(false); if (onOpenClearData) onOpenClearData(); }}>
|
| 267 |
+
<DatabaseZap size={16} />
|
| 268 |
+
<span>Clear User Data</span>
|
| 269 |
</button>
|
| 270 |
<button className="user-menu-item sign-out" onClick={onSignOut}>
|
| 271 |
<LogOut size={16} />
|
|
|
|
| 277 |
</div>
|
| 278 |
</div>
|
| 279 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
</>
|
| 281 |
)}
|
| 282 |
|
|
|
|
| 298 |
>
|
| 299 |
<SquarePen size={20} />
|
| 300 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
</div>
|
| 302 |
)}
|
| 303 |
</div>
|
|
|
|
| 309 |
<Search size={16} className="search-icon" />
|
| 310 |
<input
|
| 311 |
type="text"
|
| 312 |
+
placeholder={
|
| 313 |
+
isOnCanvas
|
| 314 |
+
? (canvasSubview === 'deliverables' ? 'Search drafts...'
|
| 315 |
+
: canvasSubview === 'insights' ? 'Search sections...'
|
| 316 |
+
: 'Search widgets...')
|
| 317 |
+
: 'Search chats...'
|
| 318 |
+
}
|
| 319 |
value={searchTerm}
|
| 320 |
onChange={(e) => setSearchTerm(e.target.value)}
|
| 321 |
className="search-input"
|
| 322 |
/>
|
| 323 |
</div>
|
| 324 |
+
{!isOnCanvas && (
|
| 325 |
+
<button
|
| 326 |
+
className="new-chat-icon-btn"
|
| 327 |
+
onClick={handleNewChat}
|
| 328 |
+
disabled={isCreatingNewChat}
|
| 329 |
+
title={isCreatingNewChat ? 'Creating...' : 'New Chat'}
|
| 330 |
+
>
|
| 331 |
+
<SquarePen size={18} />
|
| 332 |
+
</button>
|
| 333 |
+
)}
|
| 334 |
</div>
|
| 335 |
)}
|
| 336 |
|
| 337 |
+
{/* Canvas sidebar — subview-aware (Insights / Workspace / Deliverables) */}
|
| 338 |
+
{isOnCanvas ? (
|
| 339 |
+
<div className="canvas-sidebar-menu">
|
| 340 |
+
{!isCollapsed && (() => {
|
| 341 |
+
const q = searchTerm.toLowerCase();
|
| 342 |
+
|
| 343 |
+
// ---------- DELIVERABLES: project list with expandable section dropdown ----------
|
| 344 |
+
if (canvasSubview === 'deliverables') {
|
| 345 |
+
const projects = deliverableProjects.filter(p =>
|
| 346 |
+
!q || p.name.toLowerCase().includes(q) || p.sections.some(s => s.name.toLowerCase().includes(q))
|
| 347 |
+
);
|
| 348 |
+
if (projects.length === 0) {
|
| 349 |
+
return (
|
| 350 |
+
<div className="no-sessions">
|
| 351 |
+
{searchTerm ? 'No drafts match' : 'No drafts yet — create one in Documents'}
|
| 352 |
+
</div>
|
| 353 |
+
);
|
| 354 |
+
}
|
| 355 |
+
return projects.map(p => {
|
| 356 |
+
const open = expanded[`p-${p.id}`] ?? p.isActive;
|
| 357 |
+
const totalWords = p.sections.reduce((s, x) => s + x.wc, 0);
|
| 358 |
+
return (
|
| 359 |
+
<div key={p.id} className={`csm-project ${p.isActive ? 'active' : ''}`}>
|
| 360 |
+
<button
|
| 361 |
+
className="csm-project-head"
|
| 362 |
+
onClick={() => toggleExpanded(`p-${p.id}`)}
|
| 363 |
+
>
|
| 364 |
+
<ChevronRight size={12} className={`csm-chevron ${open ? 'open' : ''}`}/>
|
| 365 |
+
<FileText size={13}/>
|
| 366 |
+
<span className="csm-project-name">{p.name}</span>
|
| 367 |
+
{p.versions > 0 && (
|
| 368 |
+
<span className="csm-versions" title={`${p.versions} version${p.versions === 1 ? '' : 's'} saved`}>
|
| 369 |
+
<Clock size={10}/>{p.versions}
|
| 370 |
+
</span>
|
| 371 |
+
)}
|
| 372 |
+
</button>
|
| 373 |
+
{open && (
|
| 374 |
+
<div className="csm-project-body">
|
| 375 |
+
<button className="csm-row" onClick={p.onOpen}>
|
| 376 |
+
<span className="csm-row-icon">📂</span>
|
| 377 |
+
<span>Open editor</span>
|
| 378 |
+
</button>
|
| 379 |
+
{p.sections.map(s => (
|
| 380 |
+
<button key={s.id} className="csm-row csm-row-section" onClick={s.onClick}>
|
| 381 |
+
<span className="csm-row-bullet"/>
|
| 382 |
+
<span className="csm-row-label">{s.name}</span>
|
| 383 |
+
{s.wc > 0 && <span className="csm-row-meta">{s.wc}</span>}
|
| 384 |
+
</button>
|
| 385 |
+
))}
|
| 386 |
+
<div className="csm-row csm-row-foot">
|
| 387 |
+
<Clock size={11}/>
|
| 388 |
+
<span>{p.versions} version{p.versions === 1 ? '' : 's'} · auto-saved</span>
|
| 389 |
+
</div>
|
| 390 |
+
<div className="csm-row csm-row-foot">
|
| 391 |
+
<span style={{ color: 'var(--text-tertiary, #9CA3AF)' }}>{totalWords} words total</span>
|
| 392 |
+
</div>
|
| 393 |
+
</div>
|
| 394 |
+
)}
|
| 395 |
+
</div>
|
| 396 |
+
);
|
| 397 |
+
});
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
// ---------- WORKSPACE: widgets grouped by category ----------
|
| 401 |
+
if (canvasSubview === 'workspace') {
|
| 402 |
+
const groups = widgetGroups
|
| 403 |
+
.map(g => ({
|
| 404 |
+
...g,
|
| 405 |
+
items: g.items.filter(it => !q || it.label.toLowerCase().includes(q)),
|
| 406 |
+
}))
|
| 407 |
+
.filter(g => g.items.length > 0);
|
| 408 |
+
if (groups.length === 0) {
|
| 409 |
+
return (
|
| 410 |
+
<div className="no-sessions">
|
| 411 |
+
{searchTerm ? 'No widgets match' : 'Workspace is empty — add widgets'}
|
| 412 |
+
</div>
|
| 413 |
+
);
|
| 414 |
+
}
|
| 415 |
+
return groups.map(g => {
|
| 416 |
+
const open = expanded[`g-${g.id}`] ?? true;
|
| 417 |
+
return (
|
| 418 |
+
<div key={g.id} className="csm-group">
|
| 419 |
+
<button className="csm-group-head" onClick={() => toggleExpanded(`g-${g.id}`)}>
|
| 420 |
+
<ChevronRight size={11} className={`csm-chevron ${open ? 'open' : ''}`}/>
|
| 421 |
+
<span className="csm-group-name">{g.label}</span>
|
| 422 |
+
<span className="csm-group-count">{g.items.length}</span>
|
| 423 |
+
</button>
|
| 424 |
+
{open && (
|
| 425 |
+
<div className="csm-group-body">
|
| 426 |
+
{g.items.map(it => (
|
| 427 |
+
<button key={it.id} className={`csm-row ${it.critic ? 'critic' : ''}`} onClick={it.onClick}>
|
| 428 |
+
<span className="csm-row-bullet"/>
|
| 429 |
+
<span className="csm-row-label">{it.label}</span>
|
| 430 |
+
</button>
|
| 431 |
+
))}
|
| 432 |
+
</div>
|
| 433 |
+
)}
|
| 434 |
+
</div>
|
| 435 |
+
);
|
| 436 |
+
});
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
// ---------- INSIGHTS: section list with confidence badges ----------
|
| 440 |
+
if (canvasSubview === 'insights') {
|
| 441 |
+
const sections = insightSections.filter(s => !q || s.name.toLowerCase().includes(q));
|
| 442 |
+
if (sections.length === 0) {
|
| 443 |
+
return <div className="no-sessions">{searchTerm ? 'No sections match' : 'No insights yet'}</div>;
|
| 444 |
+
}
|
| 445 |
+
return (
|
| 446 |
+
<div className="csm-group">
|
| 447 |
+
<div className="csm-group-head" style={{ cursor: 'default' }}>
|
| 448 |
+
<span className="csm-group-name">Sections</span>
|
| 449 |
+
<span className="csm-group-count">{sections.length}</span>
|
| 450 |
+
</div>
|
| 451 |
+
<div className="csm-group-body">
|
| 452 |
+
{sections.map(s => {
|
| 453 |
+
const complete = s.taskCount > 0 && s.doneCount === s.taskCount;
|
| 454 |
+
return (
|
| 455 |
+
<button key={s.id} className={`csm-row ${complete ? 'csm-row-done' : ''}`} onClick={s.onClick}>
|
| 456 |
+
<span className="csm-row-bullet"/>
|
| 457 |
+
<span className="csm-row-label">{s.name}</span>
|
| 458 |
+
{s.taskCount > 0 && (
|
| 459 |
+
<span className="csm-row-meta">{s.doneCount}/{s.taskCount}</span>
|
| 460 |
+
)}
|
| 461 |
+
</button>
|
| 462 |
+
);
|
| 463 |
+
})}
|
| 464 |
+
</div>
|
| 465 |
+
</div>
|
| 466 |
+
);
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
// ---------- Fallback: flat list (legacy) ----------
|
| 470 |
+
const items = canvasItems.filter(it => !q || it.label.toLowerCase().includes(q));
|
| 471 |
+
if (items.length === 0) {
|
| 472 |
+
return <div className="no-sessions">{searchTerm ? 'No matches' : 'Nothing here yet'}</div>;
|
| 473 |
+
}
|
| 474 |
+
return (
|
| 475 |
+
<div className="sessions-list">
|
| 476 |
+
{items.map((it) => (
|
| 477 |
+
<div key={it.id} className="session-item" onClick={it.onClick}>
|
| 478 |
+
<div className="session-content">
|
| 479 |
+
<div className="session-icon"><FileText size={16}/></div>
|
| 480 |
+
<div className="session-details">
|
| 481 |
+
<div className="session-title">{it.label}</div>
|
| 482 |
+
{it.sub && <div className="session-meta"><span>{it.sub}</span></div>}
|
| 483 |
+
</div>
|
| 484 |
+
</div>
|
| 485 |
+
</div>
|
| 486 |
+
))}
|
| 487 |
+
</div>
|
| 488 |
+
);
|
| 489 |
+
})()}
|
| 490 |
+
</div>
|
| 491 |
+
) : (
|
| 492 |
+
/* Chat Sessions */
|
| 493 |
<div className="chat-sessions">
|
| 494 |
{isLoading ? (
|
| 495 |
<div className="loading-sessions">
|
|
|
|
| 542 |
</div>
|
| 543 |
)}
|
| 544 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
)}
|
| 546 |
+
|
| 547 |
{/* Footer */}
|
| 548 |
<div className={`sidebar-footer ${isCollapsed ? 'collapsed' : ''}`}>
|
| 549 |
<a
|
|
|
|
| 560 |
</div>
|
| 561 |
</div>
|
| 562 |
|
| 563 |
+
{showAvatarPicker && (
|
| 564 |
+
<UserAvatarPicker
|
| 565 |
+
options={avatarOptions}
|
| 566 |
+
currentId={userAvatarId}
|
| 567 |
+
onSelect={(id) => { onAvatarChange?.(id); setShowAvatarPicker(false); }}
|
| 568 |
+
onClose={() => setShowAvatarPicker(false)}
|
| 569 |
/>
|
| 570 |
)}
|
| 571 |
|
| 572 |
+
{isMobileOpen && (
|
| 573 |
+
<div
|
| 574 |
+
className="mobile-sidebar-overlay visible"
|
| 575 |
+
onClick={() => onMobileToggle(false)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
/>
|
| 577 |
)}
|
| 578 |
</>
|
| 579 |
);
|
| 580 |
};
|
| 581 |
|
| 582 |
+
export default Sidebar;
|
phd-advisor-frontend/src/components/Signup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
-
import { Eye, EyeOff, Mail, Lock, User, ArrowRight,
|
| 3 |
import { useAppConfig } from '../contexts/AppConfigContext';
|
| 4 |
import '../styles/Signup.css';
|
| 5 |
|
|
@@ -19,14 +19,22 @@ const Signup = ({ onNavigateToLogin, onNavigateToHome }) => {
|
|
| 19 |
const [isLoading, setIsLoading] = useState(false);
|
| 20 |
const [errors, setErrors] = useState({});
|
| 21 |
|
| 22 |
-
const
|
| 23 |
-
? config.login.
|
| 24 |
-
:
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
const handleInputChange = (e) => {
|
| 32 |
const { name, value } = e.target;
|
|
@@ -75,7 +83,7 @@ const Signup = ({ onNavigateToLogin, onNavigateToHome }) => {
|
|
| 75 |
}
|
| 76 |
|
| 77 |
if (!formData.academicStage) {
|
| 78 |
-
newErrors.academicStage = 'Please select your
|
| 79 |
}
|
| 80 |
|
| 81 |
setErrors(newErrors);
|
|
@@ -140,7 +148,7 @@ const Signup = ({ onNavigateToLogin, onNavigateToHome }) => {
|
|
| 140 |
{/* Header */}
|
| 141 |
<div className="signup-header">
|
| 142 |
<div className="logo-container">
|
| 143 |
-
<
|
| 144 |
</div>
|
| 145 |
<h1 className="signup-title">Join Our Community</h1>
|
| 146 |
<p className="signup-subtitle">
|
|
@@ -285,13 +293,13 @@ const Signup = ({ onNavigateToLogin, onNavigateToHome }) => {
|
|
| 285 |
</div>
|
| 286 |
</div>
|
| 287 |
|
| 288 |
-
{/*
|
| 289 |
<div className="form-group">
|
| 290 |
<label htmlFor="academicStage" className="form-label">
|
| 291 |
-
|
| 292 |
</label>
|
| 293 |
<div className="input-container">
|
| 294 |
-
<
|
| 295 |
<select
|
| 296 |
id="academicStage"
|
| 297 |
name="academicStage"
|
|
@@ -300,7 +308,7 @@ const Signup = ({ onNavigateToLogin, onNavigateToHome }) => {
|
|
| 300 |
className={`form-select ${errors.academicStage ? 'error' : ''}`}
|
| 301 |
disabled={isLoading}
|
| 302 |
>
|
| 303 |
-
{
|
| 304 |
<option key={stage.value} value={stage.value}>
|
| 305 |
{stage.label}
|
| 306 |
</option>
|
|
@@ -312,23 +320,27 @@ const Signup = ({ onNavigateToLogin, onNavigateToHome }) => {
|
|
| 312 |
)}
|
| 313 |
</div>
|
| 314 |
|
| 315 |
-
{/*
|
| 316 |
<div className="form-group">
|
| 317 |
<label htmlFor="researchArea" className="form-label">
|
| 318 |
-
|
| 319 |
</label>
|
| 320 |
<div className="input-container">
|
| 321 |
-
<
|
| 322 |
-
<
|
| 323 |
-
type="text"
|
| 324 |
id="researchArea"
|
| 325 |
name="researchArea"
|
| 326 |
value={formData.researchArea}
|
| 327 |
onChange={handleInputChange}
|
| 328 |
-
className="form-
|
| 329 |
-
placeholder="e.g., Computer Science, Biology, Psychology..."
|
| 330 |
disabled={isLoading}
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
</div>
|
| 333 |
</div>
|
| 334 |
|
|
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
+
import { Eye, EyeOff, Mail, Lock, User, ArrowRight, Shield, Phone, Globe } from 'lucide-react';
|
| 3 |
import { useAppConfig } from '../contexts/AppConfigContext';
|
| 4 |
import '../styles/Signup.css';
|
| 5 |
|
|
|
|
| 19 |
const [isLoading, setIsLoading] = useState(false);
|
| 20 |
const [errors, setErrors] = useState({});
|
| 21 |
|
| 22 |
+
const knowledgeLevels = config?.login?.knowledge_levels?.length
|
| 23 |
+
? config.login.knowledge_levels
|
| 24 |
+
: config?.login?.academic_stages?.length
|
| 25 |
+
? config.login.academic_stages
|
| 26 |
+
: [
|
| 27 |
+
{ value: '', label: 'Select your cybersecurity knowledge level' },
|
| 28 |
+
{ value: 'newcomer', label: 'New to cybersecurity' },
|
| 29 |
+
{ value: 'foundational', label: 'Foundational' },
|
| 30 |
+
{ value: 'practitioner', label: 'Practitioner' },
|
| 31 |
+
{ value: 'experienced', label: 'Experienced' },
|
| 32 |
+
{ value: 'expert', label: 'Expert / specialist' },
|
| 33 |
+
];
|
| 34 |
+
|
| 35 |
+
const timezones = config?.login?.timezones?.length
|
| 36 |
+
? config.login.timezones
|
| 37 |
+
: [{ value: '', label: 'Select timezone (optional)' }];
|
| 38 |
|
| 39 |
const handleInputChange = (e) => {
|
| 40 |
const { name, value } = e.target;
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
if (!formData.academicStage) {
|
| 86 |
+
newErrors.academicStage = 'Please select your cybersecurity knowledge level';
|
| 87 |
}
|
| 88 |
|
| 89 |
setErrors(newErrors);
|
|
|
|
| 148 |
{/* Header */}
|
| 149 |
<div className="signup-header">
|
| 150 |
<div className="logo-container">
|
| 151 |
+
<Shield className="logo-icon" />
|
| 152 |
</div>
|
| 153 |
<h1 className="signup-title">Join Our Community</h1>
|
| 154 |
<p className="signup-subtitle">
|
|
|
|
| 293 |
</div>
|
| 294 |
</div>
|
| 295 |
|
| 296 |
+
{/* Cybersecurity knowledge level */}
|
| 297 |
<div className="form-group">
|
| 298 |
<label htmlFor="academicStage" className="form-label">
|
| 299 |
+
Cybersecurity knowledge level
|
| 300 |
</label>
|
| 301 |
<div className="input-container">
|
| 302 |
+
<Shield className="input-icon" />
|
| 303 |
<select
|
| 304 |
id="academicStage"
|
| 305 |
name="academicStage"
|
|
|
|
| 308 |
className={`form-select ${errors.academicStage ? 'error' : ''}`}
|
| 309 |
disabled={isLoading}
|
| 310 |
>
|
| 311 |
+
{knowledgeLevels.map(stage => (
|
| 312 |
<option key={stage.value} value={stage.value}>
|
| 313 |
{stage.label}
|
| 314 |
</option>
|
|
|
|
| 320 |
)}
|
| 321 |
</div>
|
| 322 |
|
| 323 |
+
{/* Time zone (optional) */}
|
| 324 |
<div className="form-group">
|
| 325 |
<label htmlFor="researchArea" className="form-label">
|
| 326 |
+
Time zone <span className="optional">(Optional)</span>
|
| 327 |
</label>
|
| 328 |
<div className="input-container">
|
| 329 |
+
<Globe className="input-icon" />
|
| 330 |
+
<select
|
|
|
|
| 331 |
id="researchArea"
|
| 332 |
name="researchArea"
|
| 333 |
value={formData.researchArea}
|
| 334 |
onChange={handleInputChange}
|
| 335 |
+
className="form-select"
|
|
|
|
| 336 |
disabled={isLoading}
|
| 337 |
+
>
|
| 338 |
+
{timezones.map(tz => (
|
| 339 |
+
<option key={tz.value} value={tz.value}>
|
| 340 |
+
{tz.label}
|
| 341 |
+
</option>
|
| 342 |
+
))}
|
| 343 |
+
</select>
|
| 344 |
</div>
|
| 345 |
</div>
|
| 346 |
|
phd-advisor-frontend/src/components/ThinkingIndicator.js
CHANGED
|
@@ -14,57 +14,41 @@ const ThinkingIndicator = ({ advisorId }) => {
|
|
| 14 |
|
| 15 |
return (
|
| 16 |
<div className="thinking-container">
|
| 17 |
-
<div
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
</div>
|
| 23 |
-
<div
|
| 24 |
className="thinking-bubble"
|
| 25 |
-
style={{
|
| 26 |
backgroundColor: colors.bgColor,
|
| 27 |
-
borderColor: colors.color + '40'
|
| 28 |
}}
|
| 29 |
>
|
| 30 |
<div className="thinking-header">
|
| 31 |
-
<h4
|
| 32 |
-
className="advisor-name"
|
| 33 |
-
style={{ color: colors.color }}
|
| 34 |
-
>
|
| 35 |
{advisor.name}
|
| 36 |
</h4>
|
| 37 |
</div>
|
| 38 |
<div className="thinking-dots">
|
| 39 |
-
<div
|
| 40 |
-
className="thinking-dot"
|
| 41 |
-
style={{
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
}}
|
| 52 |
-
></div>
|
| 53 |
-
<div
|
| 54 |
-
className="thinking-dot"
|
| 55 |
-
style={{
|
| 56 |
-
backgroundColor: colors.color,
|
| 57 |
-
animationDelay: '300ms'
|
| 58 |
-
}}
|
| 59 |
-
></div>
|
| 60 |
</div>
|
| 61 |
-
<p
|
| 62 |
-
className="thinking-text"
|
| 63 |
-
style={{
|
| 64 |
-
color: colors.color,
|
| 65 |
-
opacity: 0.8
|
| 66 |
-
}}
|
| 67 |
-
>
|
| 68 |
thinking...
|
| 69 |
</p>
|
| 70 |
</div>
|
|
|
|
| 14 |
|
| 15 |
return (
|
| 16 |
<div className="thinking-container">
|
| 17 |
+
<div className="advisor-message-avatar-ring" style={{ width: 44, height: 44 }}>
|
| 18 |
+
{Icon ? (
|
| 19 |
+
<Icon
|
| 20 |
+
className="advisor-message-avatar-icon"
|
| 21 |
+
style={{ color: colors.color, width: 23, height: 23 }}
|
| 22 |
+
/>
|
| 23 |
+
) : null}
|
| 24 |
</div>
|
| 25 |
+
<div
|
| 26 |
className="thinking-bubble"
|
| 27 |
+
style={{
|
| 28 |
backgroundColor: colors.bgColor,
|
| 29 |
+
borderColor: colors.color + '40',
|
| 30 |
}}
|
| 31 |
>
|
| 32 |
<div className="thinking-header">
|
| 33 |
+
<h4 className="advisor-name" style={{ color: colors.color }}>
|
|
|
|
|
|
|
|
|
|
| 34 |
{advisor.name}
|
| 35 |
</h4>
|
| 36 |
</div>
|
| 37 |
<div className="thinking-dots">
|
| 38 |
+
<div
|
| 39 |
+
className="thinking-dot"
|
| 40 |
+
style={{ backgroundColor: colors.color, animationDelay: '0ms' }}
|
| 41 |
+
/>
|
| 42 |
+
<div
|
| 43 |
+
className="thinking-dot"
|
| 44 |
+
style={{ backgroundColor: colors.color, animationDelay: '150ms' }}
|
| 45 |
+
/>
|
| 46 |
+
<div
|
| 47 |
+
className="thinking-dot"
|
| 48 |
+
style={{ backgroundColor: colors.color, animationDelay: '300ms' }}
|
| 49 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
</div>
|
| 51 |
+
<p className="thinking-text" style={{ color: colors.color, opacity: 0.8 }}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
thinking...
|
| 53 |
</p>
|
| 54 |
</div>
|
phd-advisor-frontend/src/data/userGuide.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
-
// User Guide content
|
| 2 |
-
//
|
| 3 |
-
// Use {{appName}} as a placeholder. It gets replaced at render time.
|
| 4 |
|
| 5 |
export const userGuideTopics = [
|
| 6 |
{
|
|
@@ -9,30 +8,30 @@ export const userGuideTopics = [
|
|
| 9 |
icon: 'Sparkles',
|
| 10 |
content: `# Welcome to {{appName}}
|
| 11 |
|
| 12 |
-
{{appName}} is your AI-powered
|
| 13 |
|
| 14 |
## Your first steps
|
| 15 |
1. **Start a new chat** using the pencil icon next to the search bar
|
| 16 |
-
2. **Type a question
|
| 17 |
-
3. **Read multiple advisor responses
|
| 18 |
-
4. **Reply to a specific advisor** to
|
| 19 |
|
| 20 |
## Need help?
|
| 21 |
-
|
| 22 |
},
|
| 23 |
{
|
| 24 |
id: 'advisors',
|
| 25 |
title: 'Your Advisors',
|
| 26 |
-
icon: '
|
| 27 |
content: `# Your Advisors
|
| 28 |
|
| 29 |
-
{{appName}}
|
| 30 |
|
| 31 |
## Available advisors
|
| 32 |
{{advisorList}}
|
| 33 |
|
| 34 |
## Seeing who's available
|
| 35 |
-
Click the **
|
| 36 |
},
|
| 37 |
{
|
| 38 |
id: 'conversations',
|
|
@@ -41,18 +40,15 @@ Click the **"X Advisors"** dropdown in the top right of the chat to see all of y
|
|
| 41 |
content: `# Conversations & Replies
|
| 42 |
|
| 43 |
## Asking a question
|
| 44 |
-
Type into the chat box at the bottom. All advisors
|
| 45 |
|
| 46 |
## Replying to a specific advisor
|
| 47 |
-
Click
|
| 48 |
-
|
| 49 |
-
## Expanding a response
|
| 50 |
-
Some responses include an **"Expand"** action to ask the advisor to elaborate further with more detail.
|
| 51 |
|
| 52 |
## Tips
|
| 53 |
-
-
|
| 54 |
-
-
|
| 55 |
-
- Different advisors
|
| 56 |
},
|
| 57 |
{
|
| 58 |
id: 'documents',
|
|
@@ -60,21 +56,18 @@ Some responses include an **"Expand"** action to ask the advisor to elaborate fu
|
|
| 60 |
icon: 'Paperclip',
|
| 61 |
content: `# Uploading Documents
|
| 62 |
|
| 63 |
-
|
| 64 |
|
| 65 |
## How it works
|
| 66 |
1. Click the paperclip icon in the chat input
|
| 67 |
2. Select your file
|
| 68 |
-
3. Wait for
|
| 69 |
-
4. Ask a question
|
| 70 |
-
|
| 71 |
-
## What can it handle?
|
| 72 |
-
- Research papers (PDF)
|
| 73 |
-
- Drafts and chapters (DOCX, TXT)
|
| 74 |
-
- Notes and outlines
|
| 75 |
|
| 76 |
-
##
|
| 77 |
-
|
|
|
|
|
|
|
| 78 |
},
|
| 79 |
{
|
| 80 |
id: 'sessions',
|
|
@@ -82,44 +75,24 @@ Documents are processed using **RAG (retrieval-augmented generation)**. The syst
|
|
| 82 |
icon: 'MessagesSquare',
|
| 83 |
content: `# Sessions & History
|
| 84 |
|
| 85 |
-
Every conversation is
|
| 86 |
-
|
| 87 |
-
## Finding past chats
|
| 88 |
-
Use the **search bar** in the sidebar to filter your past sessions by title.
|
| 89 |
-
|
| 90 |
-
## Switching between sessions
|
| 91 |
-
Click any session in the sidebar to return to it. Your full context is preserved.
|
| 92 |
-
|
| 93 |
-
## Starting a new chat
|
| 94 |
-
Click the pencil/edit icon next to the search bar to start a fresh conversation.
|
| 95 |
-
|
| 96 |
-
## Renaming or deleting
|
| 97 |
-
Hover any session to reveal the **menu**. From there you can rename or delete.`,
|
| 98 |
},
|
| 99 |
{
|
| 100 |
id: 'canvas',
|
| 101 |
-
title: '
|
| 102 |
icon: 'BarChart3',
|
| 103 |
content: `# {{appName}} Canvas
|
| 104 |
|
| 105 |
-
The Canvas is a **structured
|
| 106 |
|
| 107 |
-
-
|
| 108 |
-
-
|
| 109 |
-
-
|
| 110 |
-
-
|
| 111 |
-
-
|
| 112 |
-
-
|
| 113 |
-
- Career Development
|
| 114 |
-
- Literature Review
|
| 115 |
-
- Data Analysis
|
| 116 |
-
- Motivation & Mindset
|
| 117 |
|
| 118 |
-
|
| 119 |
-
Click the **{{appName}} Canvas** button in the sidebar.
|
| 120 |
-
|
| 121 |
-
## Exporting
|
| 122 |
-
You can print or download the Canvas as a snapshot of your progress.`,
|
| 123 |
},
|
| 124 |
{
|
| 125 |
id: 'tips',
|
|
@@ -127,17 +100,13 @@ You can print or download the Canvas as a snapshot of your progress.`,
|
|
| 127 |
icon: 'Sparkles',
|
| 128 |
content: `# Tips & Shortcuts
|
| 129 |
|
| 130 |
-
## Get better answers
|
| 131 |
-
- **Provide context.** Mention your field, your stage, your specific concern.
|
| 132 |
-
- **Quote your work.** Paste a paragraph from your draft for targeted feedback.
|
| 133 |
-
- **Use multiple advisors.** Ask one for theory, another for practical next steps.
|
| 134 |
-
|
| 135 |
## Useful workflows
|
| 136 |
-
- **
|
| 137 |
-
- **
|
| 138 |
-
- **
|
|
|
|
| 139 |
|
| 140 |
## Theme
|
| 141 |
-
Switch
|
| 142 |
},
|
| 143 |
];
|
|
|
|
| 1 |
+
// User Guide content for Cybersecurity Advisor.
|
| 2 |
+
// Use {{appName}} as a placeholder — replaced at render time.
|
|
|
|
| 3 |
|
| 4 |
export const userGuideTopics = [
|
| 5 |
{
|
|
|
|
| 8 |
icon: 'Sparkles',
|
| 9 |
content: `# Welcome to {{appName}}
|
| 10 |
|
| 11 |
+
{{appName}} is your AI-powered cybersecurity guidance system. A panel of specialized advisors gives you diverse perspectives on threats, controls, incidents, compliance, architecture, and career growth.
|
| 12 |
|
| 13 |
## Your first steps
|
| 14 |
1. **Start a new chat** using the pencil icon next to the search bar
|
| 15 |
+
2. **Type a question** about security risks, tools, policies, incidents, or your career path
|
| 16 |
+
3. **Read multiple advisor responses** — each persona brings a different lens
|
| 17 |
+
4. **Reply to a specific advisor** to go deeper on their angle
|
| 18 |
|
| 19 |
## Need help?
|
| 20 |
+
Return to this guide anytime via the **?** icon in the header.`,
|
| 21 |
},
|
| 22 |
{
|
| 23 |
id: 'advisors',
|
| 24 |
title: 'Your Advisors',
|
| 25 |
+
icon: 'Shield',
|
| 26 |
content: `# Your Advisors
|
| 27 |
|
| 28 |
+
{{appName}} includes {{advisorCount}} specialized cybersecurity personas, powered by Neon BrainForge Security (4090 x1-3) with GPT-5.4 fallback when needed.
|
| 29 |
|
| 30 |
## Available advisors
|
| 31 |
{{advisorList}}
|
| 32 |
|
| 33 |
## Seeing who's available
|
| 34 |
+
Click the **advisors** dropdown in the top right of the chat to see the full panel.`,
|
| 35 |
},
|
| 36 |
{
|
| 37 |
id: 'conversations',
|
|
|
|
| 40 |
content: `# Conversations & Replies
|
| 41 |
|
| 42 |
## Asking a question
|
| 43 |
+
Type into the chat box at the bottom. All advisors respond with their unique perspective.
|
| 44 |
|
| 45 |
## Replying to a specific advisor
|
| 46 |
+
Click an advisor's response to **reply directly to them** and continue one-on-one.
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
## Tips
|
| 49 |
+
- Include environment context (cloud, on-prem, SaaS, regulated industry)
|
| 50 |
+
- Paste log snippets, policy excerpts, or architecture notes for sharper advice
|
| 51 |
+
- Different advisors may disagree — use that tension to stress-test decisions`,
|
| 52 |
},
|
| 53 |
{
|
| 54 |
id: 'documents',
|
|
|
|
| 56 |
icon: 'Paperclip',
|
| 57 |
content: `# Uploading Documents
|
| 58 |
|
| 59 |
+
Attach **PDFs, Word documents, and text files** so advisors can reference your materials.
|
| 60 |
|
| 61 |
## How it works
|
| 62 |
1. Click the paperclip icon in the chat input
|
| 63 |
2. Select your file
|
| 64 |
+
3. Wait for processing
|
| 65 |
+
4. Ask a question — advisors use **RAG** to pull relevant sections
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
+
## Good uploads
|
| 68 |
+
- Incident reports and postmortems
|
| 69 |
+
- Architecture diagrams (exported as PDF)
|
| 70 |
+
- Policy drafts, audit findings, pen-test summaries`,
|
| 71 |
},
|
| 72 |
{
|
| 73 |
id: 'sessions',
|
|
|
|
| 75 |
icon: 'MessagesSquare',
|
| 76 |
content: `# Sessions & History
|
| 77 |
|
| 78 |
+
Every conversation is saved as a session. Use the sidebar search to find past chats, switch sessions, or start a new chat with the pencil icon.`,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
},
|
| 80 |
{
|
| 81 |
id: 'canvas',
|
| 82 |
+
title: 'Security Canvas',
|
| 83 |
icon: 'BarChart3',
|
| 84 |
content: `# {{appName}} Canvas
|
| 85 |
|
| 86 |
+
The Canvas is a **structured workspace** for your security program. Insights from chats can inform widgets such as:
|
| 87 |
|
| 88 |
+
- Threat landscape
|
| 89 |
+
- Controls posture
|
| 90 |
+
- Open incidents & IR actions
|
| 91 |
+
- Compliance gaps
|
| 92 |
+
- Architecture decisions
|
| 93 |
+
- Skill development & certifications
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
+
Open Canvas from the sidebar. Layout and widgets auto-save in your browser.`,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
},
|
| 97 |
{
|
| 98 |
id: 'tips',
|
|
|
|
| 100 |
icon: 'Sparkles',
|
| 101 |
content: `# Tips & Shortcuts
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
## Useful workflows
|
| 104 |
+
- **Incident triage:** Incident Response Lead + Threat Modeling Analyst
|
| 105 |
+
- **Audit prep:** Compliance Advisor + Security Architect
|
| 106 |
+
- **Career planning:** Jerry Huaute Advisor + Security Career Mentor
|
| 107 |
+
- **Red-team mindset:** Use anti-yes-man Canvas widgets for challenge and scope checks
|
| 108 |
|
| 109 |
## Theme
|
| 110 |
+
Switch light/dark mode from the toggle in the header.`,
|
| 111 |
},
|
| 112 |
];
|
phd-advisor-frontend/src/pages/CanvasPage.js
CHANGED
|
@@ -1,574 +1,1072 @@
|
|
| 1 |
-
import React, { useState, useEffect } from 'react';
|
| 2 |
-
import {
|
| 3 |
-
|
| 4 |
-
RefreshCw,
|
| 5 |
-
Download,
|
| 6 |
-
Calendar,
|
| 7 |
-
TrendingUp,
|
| 8 |
-
Target,
|
| 9 |
-
BookOpen,
|
| 10 |
-
Lightbulb,
|
| 11 |
-
AlertTriangle,
|
| 12 |
-
Users,
|
| 13 |
-
BarChart3,
|
| 14 |
-
Heart,
|
| 15 |
-
ArrowLeft,
|
| 16 |
-
Printer,
|
| 17 |
-
Trash2,
|
| 18 |
-
MessageCircle,
|
| 19 |
-
ArrowRight
|
| 20 |
-
} from 'lucide-react';
|
| 21 |
import { useAppConfig } from '../contexts/AppConfigContext';
|
| 22 |
-
import
|
| 23 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
import '../styles/CanvasPage.css';
|
| 25 |
|
| 26 |
-
|
| 27 |
-
const
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
const CanvasSection = ({ section, sectionKey, isExpanded, onToggle }) => {
|
| 41 |
-
const IconComponent = sectionIcons[sectionKey] || Lightbulb;
|
| 42 |
-
|
| 43 |
return (
|
| 44 |
-
<div
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
>
|
| 49 |
-
<
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
<div className="section-meta">
|
| 57 |
-
<span className="insight-count">{section.insights.length} insights</span>
|
| 58 |
-
<div className={`expand-arrow ${isExpanded ? 'expanded' : ''}`}>
|
| 59 |
-
▼
|
| 60 |
-
</div>
|
| 61 |
</div>
|
| 62 |
</div>
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
{section.insights.length === 0 ? (
|
| 67 |
-
<div className="empty-section">
|
| 68 |
-
<Lightbulb className="empty-icon" />
|
| 69 |
-
<p>No insights yet. Keep chatting with your advisors to build this section!</p>
|
| 70 |
-
</div>
|
| 71 |
-
) : (
|
| 72 |
-
<div className="insights-grid">
|
| 73 |
-
{section.insights.map((insight, index) => (
|
| 74 |
-
<div key={index} className="insight-card">
|
| 75 |
-
<div className="insight-content">
|
| 76 |
-
{insight.content}
|
| 77 |
-
</div>
|
| 78 |
-
<div className="insight-footer">
|
| 79 |
-
<span className="insight-source">{insight.source_persona}</span>
|
| 80 |
-
<span className="insight-confidence">
|
| 81 |
-
{Math.round(insight.confidence_score * 100)}% confidence
|
| 82 |
-
</span>
|
| 83 |
-
</div>
|
| 84 |
-
</div>
|
| 85 |
-
))}
|
| 86 |
-
</div>
|
| 87 |
-
)}
|
| 88 |
-
</div>
|
| 89 |
-
)}
|
| 90 |
</div>
|
| 91 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
};
|
|
|
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
const [isRefreshing, setIsRefreshing] = useState(false);
|
| 105 |
-
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
| 106 |
-
const [isClearing, setIsClearing] = useState(false);
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
// Cleanup on unmount
|
| 124 |
-
return () => {
|
| 125 |
-
if (pollInterval) {
|
| 126 |
-
clearInterval(pollInterval);
|
| 127 |
-
}
|
| 128 |
-
};
|
| 129 |
-
}, []);
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
headers: {
|
| 135 |
-
'Authorization': `Bearer ${authToken}`,
|
| 136 |
-
'Content-Type': 'application/json'
|
| 137 |
-
}
|
| 138 |
-
});
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
const sectionsToExpand = {};
|
| 146 |
-
Object.entries(data.sections).forEach(([key, section]) => {
|
| 147 |
-
if (section.insights.length > 0) {
|
| 148 |
-
sectionsToExpand[key] = true;
|
| 149 |
-
}
|
| 150 |
-
});
|
| 151 |
-
setExpandedSections(sectionsToExpand);
|
| 152 |
-
} else {
|
| 153 |
-
console.error('Failed to fetch canvas');
|
| 154 |
-
}
|
| 155 |
-
} catch (error) {
|
| 156 |
-
console.error('Error fetching canvas:', error);
|
| 157 |
-
} finally {
|
| 158 |
-
setIsLoading(false);
|
| 159 |
-
}
|
| 160 |
};
|
| 161 |
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
console.error('Error fetching stats:', error);
|
| 177 |
-
}
|
| 178 |
};
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
const response = await fetch(`${process.env.REACT_APP_API_URL}/api/chat-sessions/count`, {
|
| 187 |
-
headers: {
|
| 188 |
-
'Authorization': `Bearer ${authToken}`,
|
| 189 |
-
'Content-Type': 'application/json'
|
| 190 |
-
}
|
| 191 |
-
});
|
| 192 |
-
|
| 193 |
-
if (response.ok) {
|
| 194 |
-
const { count } = await response.json();
|
| 195 |
-
if (count > 0) {
|
| 196 |
-
console.log(`User has ${count} chats but empty canvas. Triggering full refresh.`);
|
| 197 |
-
await handleFullRefresh();
|
| 198 |
-
}
|
| 199 |
-
} else {
|
| 200 |
-
console.error('Failed to fetch chat sessions count:', response.status);
|
| 201 |
-
// Don't trigger refresh if count fails
|
| 202 |
-
return;
|
| 203 |
-
}
|
| 204 |
-
}
|
| 205 |
-
} catch (error) {
|
| 206 |
-
console.error('Error checking for empty canvas with chats:', error);
|
| 207 |
-
// Stop the polling if there's an error
|
| 208 |
-
return;
|
| 209 |
-
}
|
| 210 |
};
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
'Authorization': `Bearer ${authToken}`,
|
| 218 |
-
'Content-Type': 'application/json'
|
| 219 |
-
}
|
| 220 |
-
});
|
| 221 |
-
|
| 222 |
-
if (response.ok) {
|
| 223 |
-
const result = await response.json();
|
| 224 |
-
|
| 225 |
-
// If this is a first-time canvas update, show appropriate message
|
| 226 |
-
if (result.type === 'full_update') {
|
| 227 |
-
console.log('First-time canvas detected. Processing all your chats...');
|
| 228 |
-
|
| 229 |
-
// Show loading state
|
| 230 |
-
setIsProcessingFirstTime(true);
|
| 231 |
-
setIsUpdating(true);
|
| 232 |
-
|
| 233 |
-
// Poll for updates every 10 seconds for up to 3 minutes
|
| 234 |
-
let attempts = 0;
|
| 235 |
-
const maxAttempts = 18; // 3 minutes / 10 seconds
|
| 236 |
-
|
| 237 |
-
const pollForUpdates = setInterval(async () => {
|
| 238 |
-
attempts++;
|
| 239 |
-
|
| 240 |
-
try {
|
| 241 |
-
await fetchCanvas();
|
| 242 |
-
|
| 243 |
-
// If canvas now has insights, stop polling
|
| 244 |
-
if (canvasData && canvasData.total_insights > 0) {
|
| 245 |
-
clearInterval(pollForUpdates);
|
| 246 |
-
setIsUpdating(false);
|
| 247 |
-
setIsProcessingFirstTime(false);
|
| 248 |
-
console.log('Canvas successfully populated with insights!');
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
// Stop polling after max attempts
|
| 252 |
-
if (attempts >= maxAttempts) {
|
| 253 |
-
clearInterval(pollForUpdates);
|
| 254 |
-
setIsUpdating(false);
|
| 255 |
-
setIsProcessingFirstTime(false);
|
| 256 |
-
}
|
| 257 |
-
} catch (error) {
|
| 258 |
-
console.error('Error polling for updates:', error);
|
| 259 |
-
}
|
| 260 |
-
}, 10000);
|
| 261 |
-
}
|
| 262 |
-
}
|
| 263 |
-
} catch (error) {
|
| 264 |
-
console.error('Error triggering auto-update:', error);
|
| 265 |
-
}
|
| 266 |
};
|
| 267 |
|
| 268 |
-
const
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
const response = await fetch(`${process.env.REACT_APP_API_URL}/api/phd-canvas`, {
|
| 272 |
-
method: 'DELETE',
|
| 273 |
-
headers: {
|
| 274 |
-
'Authorization': `Bearer ${authToken}`,
|
| 275 |
-
'Content-Type': 'application/json'
|
| 276 |
-
}
|
| 277 |
-
});
|
| 278 |
-
if (response.ok) {
|
| 279 |
-
setCanvasData(null);
|
| 280 |
-
await fetchCanvas();
|
| 281 |
-
}
|
| 282 |
-
} catch (error) {
|
| 283 |
-
console.error('Error clearing canvas:', error);
|
| 284 |
-
} finally {
|
| 285 |
-
setIsClearing(false);
|
| 286 |
-
setShowClearConfirm(false);
|
| 287 |
-
}
|
| 288 |
};
|
| 289 |
|
| 290 |
-
const
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
method: 'GET',
|
| 303 |
-
headers: {
|
| 304 |
-
'Authorization': `Bearer ${authToken}`,
|
| 305 |
-
'Content-Type': 'application/json'
|
| 306 |
-
}
|
| 307 |
-
});
|
| 308 |
-
|
| 309 |
-
if (response.ok) {
|
| 310 |
-
const result = await response.json();
|
| 311 |
-
console.log('Full refresh initiated:', result);
|
| 312 |
-
|
| 313 |
-
// Poll for updates
|
| 314 |
-
setTimeout(() => {
|
| 315 |
-
fetchCanvas();
|
| 316 |
-
fetchStats();
|
| 317 |
-
}, 5000);
|
| 318 |
-
|
| 319 |
-
setTimeout(() => {
|
| 320 |
-
setIsUpdating(false);
|
| 321 |
-
setIsRefreshing(false);
|
| 322 |
-
}, 10000);
|
| 323 |
-
}
|
| 324 |
-
} catch (error) {
|
| 325 |
-
console.error('Error refreshing canvas:', error);
|
| 326 |
-
setIsUpdating(false);
|
| 327 |
-
setIsRefreshing(false);
|
| 328 |
-
}
|
| 329 |
};
|
| 330 |
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
}
|
| 337 |
-
|
| 338 |
-
setIsRefreshing(true);
|
| 339 |
-
setIsUpdating(true);
|
| 340 |
-
|
| 341 |
-
try {
|
| 342 |
-
const response = await fetch(`${process.env.REACT_APP_API_URL}/api/phd-canvas/refresh`, {
|
| 343 |
-
method: 'GET',
|
| 344 |
-
headers: {
|
| 345 |
-
'Authorization': `Bearer ${authToken}`,
|
| 346 |
-
'Content-Type': 'application/json'
|
| 347 |
-
}
|
| 348 |
-
});
|
| 349 |
-
|
| 350 |
-
if (response.ok) {
|
| 351 |
-
const result = await response.json();
|
| 352 |
-
console.log('Full refresh initiated:', result);
|
| 353 |
-
|
| 354 |
-
// Poll for updates
|
| 355 |
-
setTimeout(() => {
|
| 356 |
-
fetchCanvas();
|
| 357 |
-
fetchStats();
|
| 358 |
-
}, 5000);
|
| 359 |
-
|
| 360 |
-
setTimeout(() => {
|
| 361 |
-
setIsUpdating(false);
|
| 362 |
-
setIsRefreshing(false);
|
| 363 |
-
}, 10000);
|
| 364 |
-
}
|
| 365 |
-
} catch (error) {
|
| 366 |
-
console.error('Error refreshing canvas:', error);
|
| 367 |
-
setIsUpdating(false);
|
| 368 |
-
setIsRefreshing(false);
|
| 369 |
-
}
|
| 370 |
};
|
| 371 |
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
}));
|
|
|
|
|
|
|
| 377 |
};
|
| 378 |
|
| 379 |
-
const
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
|
| 392 |
-
|
|
|
|
| 393 |
return (
|
| 394 |
-
<
|
| 395 |
-
<div className="
|
| 396 |
-
|
| 397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
);
|
| 399 |
}
|
| 400 |
|
| 401 |
-
// Sort sections by priority and insights count
|
| 402 |
-
const sortedSections = Object.entries(canvasData?.sections || {})
|
| 403 |
-
.sort(([, a], [, b]) => {
|
| 404 |
-
// First by priority (lower number = higher priority)
|
| 405 |
-
if (a.priority !== b.priority) {
|
| 406 |
-
return a.priority - b.priority;
|
| 407 |
-
}
|
| 408 |
-
// Then by insights count (more insights first)
|
| 409 |
-
return b.insights.length - a.insights.length;
|
| 410 |
-
});
|
| 411 |
-
|
| 412 |
return (
|
| 413 |
-
<
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
<
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
>
|
| 421 |
-
<ArrowLeft size={20} />
|
| 422 |
-
Back to Chat
|
| 423 |
-
</button>
|
| 424 |
-
|
| 425 |
-
<div className="header-actions">
|
| 426 |
-
<button
|
| 427 |
-
onClick={handleRefreshCanvas}
|
| 428 |
-
disabled={isRefreshing || isUpdating}
|
| 429 |
-
className={`canvas-icon-btn refresh-button ${(isRefreshing || isUpdating) ? 'disabled' : ''}`}
|
| 430 |
-
title={(isRefreshing || isUpdating) ? 'Refreshing…' : 'Refresh Canvas'}
|
| 431 |
-
aria-label="Refresh canvas"
|
| 432 |
-
>
|
| 433 |
-
<RefreshCw className={`refresh-icon ${(isRefreshing || isUpdating) ? 'spinning' : ''}`} />
|
| 434 |
-
</button>
|
| 435 |
-
|
| 436 |
-
<button
|
| 437 |
-
className="canvas-icon-btn print-button"
|
| 438 |
-
onClick={handlePrint}
|
| 439 |
-
title="Print"
|
| 440 |
-
aria-label="Print canvas"
|
| 441 |
-
>
|
| 442 |
-
<Printer className="action-icon" />
|
| 443 |
-
</button>
|
| 444 |
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
</
|
|
|
|
|
|
|
| 454 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
</div>
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
<
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
</
|
| 462 |
-
<
|
| 463 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
</div>
|
| 465 |
|
| 466 |
-
{/*
|
| 467 |
-
<div className="
|
| 468 |
-
<
|
| 469 |
-
<
|
| 470 |
-
|
| 471 |
-
<
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
</div>
|
| 480 |
-
<
|
| 481 |
-
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
</span>
|
| 484 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
</div>
|
| 486 |
-
|
| 487 |
|
| 488 |
-
{/*
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
<
|
| 512 |
-
<div className="empty-canvas-actions">
|
| 513 |
<button
|
| 514 |
-
className="
|
| 515 |
-
|
|
|
|
|
|
|
| 516 |
>
|
| 517 |
-
<
|
| 518 |
-
<span
|
| 519 |
-
|
| 520 |
</button>
|
| 521 |
-
<
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
</div>
|
| 529 |
-
|
| 530 |
-
)}
|
| 531 |
</div>
|
| 532 |
-
)
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
</div>
|
| 544 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
</div>
|
|
|
|
|
|
|
|
|
|
| 546 |
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
</div>
|
| 558 |
-
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
</div>
|
| 571 |
);
|
| 572 |
};
|
| 573 |
|
| 574 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
| 2 |
+
import { HelpCircle } from 'lucide-react';
|
| 3 |
+
import { useTheme } from '../contexts/ThemeContext';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import { useAppConfig } from '../contexts/AppConfigContext';
|
| 5 |
+
import Sidebar from '../components/Sidebar';
|
| 6 |
+
import AppHeader from '../components/AppHeader';
|
| 7 |
+
import Icon from '../components/canvas/CanvasIcon';
|
| 8 |
+
import {
|
| 9 |
+
INSIGHTS, WIDGET_CATALOG, DEFAULT_LAYOUT, EMPTY_STATE, WORKSPACE_PRESETS,
|
| 10 |
+
} from '../components/canvas/canvasData';
|
| 11 |
+
import {
|
| 12 |
+
BibliographyWidget, KanbanWidget, PomodoroWidget, WritingWidget,
|
| 13 |
+
DeadlinesWidget, BudgetWidget, ReadingQueueWidget, NotesWidget,
|
| 14 |
+
HabitsWidget, GoalsWidget, MeetingsWidget,
|
| 15 |
+
OutlineWidget, HighlightsWidget, LatexWidget,
|
| 16 |
+
CalendarWidget, DocumenterWidget, ActivityWidget,
|
| 17 |
+
PhdJourneyWidget, PhdResourcesWidget,
|
| 18 |
+
StubWidget,
|
| 19 |
+
} from '../components/canvas/CanvasWidgets';
|
| 20 |
+
import {
|
| 21 |
+
Reviewer2Widget, DevilsAdvocateWidget, ScopeRealismWidget,
|
| 22 |
+
ReviewerModal, DevilsModal, ScopeModal,
|
| 23 |
+
} from '../components/canvas/CanvasCriticWidgets';
|
| 24 |
+
import {
|
| 25 |
+
AddCitationModal, AddTaskModal, AddDeadlineModal, LogWordsModal,
|
| 26 |
+
ConfirmRemoveModal, ReadingPaperModal, BudgetItemModal,
|
| 27 |
+
NoteModal, HabitModal, GoalModal, MeetingModal,
|
| 28 |
+
PaletteModal, CommandPaletteModal, GlobalSearchModal,
|
| 29 |
+
} from '../components/canvas/CanvasModals';
|
| 30 |
+
import CanvasWelcomeTour from '../components/canvas/CanvasWelcomeTour';
|
| 31 |
+
import DeliverablesView, { TEMPLATES as DELIVERABLE_TEMPLATES } from '../components/canvas/CanvasDeliverables';
|
| 32 |
+
import { MOD } from '../components/canvas/platform';
|
| 33 |
import '../styles/CanvasPage.css';
|
| 34 |
|
| 35 |
+
const LAYOUT_KEY = 'canvas-layout-v2';
|
| 36 |
+
const STATES_KEY = 'canvas-states-v2';
|
| 37 |
+
const VIEW_KEY = 'canvas-view-v2';
|
| 38 |
+
|
| 39 |
+
function renderWidget(type, state, setState, openModal, allStates) {
|
| 40 |
+
const props = { state, setState, openModal, allStates };
|
| 41 |
+
switch (type) {
|
| 42 |
+
case 'bibliography': return <BibliographyWidget {...props}/>;
|
| 43 |
+
case 'kanban': return <KanbanWidget {...props}/>;
|
| 44 |
+
case 'pomodoro': return <PomodoroWidget {...props}/>;
|
| 45 |
+
case 'writing': return <WritingWidget {...props}/>;
|
| 46 |
+
case 'deadlines': return <DeadlinesWidget {...props}/>;
|
| 47 |
+
case 'budget': return <BudgetWidget {...props}/>;
|
| 48 |
+
case 'reading-queue': return <ReadingQueueWidget {...props}/>;
|
| 49 |
+
case 'notes': return <NotesWidget {...props}/>;
|
| 50 |
+
case 'habits': return <HabitsWidget {...props}/>;
|
| 51 |
+
case 'goals': return <GoalsWidget {...props}/>;
|
| 52 |
+
case 'meeting-log': return <MeetingsWidget {...props}/>;
|
| 53 |
+
case 'reviewer-2': return <Reviewer2Widget {...props}/>;
|
| 54 |
+
case 'devils-advocate': return <DevilsAdvocateWidget {...props}/>;
|
| 55 |
+
case 'scope-realism': return <ScopeRealismWidget {...props}/>;
|
| 56 |
+
case 'outline': return <OutlineWidget {...props}/>;
|
| 57 |
+
case 'highlights': return <HighlightsWidget {...props}/>;
|
| 58 |
+
case 'latex': return <LatexWidget {...props}/>;
|
| 59 |
+
case 'calendar': return <CalendarWidget {...props}/>;
|
| 60 |
+
case 'documenter': return <DocumenterWidget {...props}/>;
|
| 61 |
+
case 'activity': return <ActivityWidget {...props}/>;
|
| 62 |
+
case 'phd-journey': return <PhdJourneyWidget {...props}/>;
|
| 63 |
+
case 'phd-resources': return <PhdResourcesWidget {...props}/>;
|
| 64 |
+
default: {
|
| 65 |
+
const meta = WIDGET_CATALOG.find(w => w.type === type);
|
| 66 |
+
return <StubWidget meta={meta}/>;
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function CanvasWidget({ widget, isDragging, isDragOver, onDragStart, onDragOver, onDragEnd, onDrop, state, setState, onRemove, onResize, openModal, allStates }) {
|
| 72 |
+
const meta = WIDGET_CATALOG.find(w => w.type === widget.type);
|
| 73 |
+
if (!meta) return null;
|
| 74 |
+
const sizes = ['S', 'M', 'L'];
|
| 75 |
+
const cycleSize = () => onResize(widget.id, sizes[(sizes.indexOf(widget.size) + 1) % sizes.length]);
|
| 76 |
|
|
|
|
|
|
|
|
|
|
| 77 |
return (
|
| 78 |
+
<div
|
| 79 |
+
className={`widget size-${widget.size} ${meta.critic ? 'critic' : ''} ${isDragging ? 'dragging' : ''} ${isDragOver ? 'drag-over' : ''}`}
|
| 80 |
+
data-widget-id={widget.id}
|
| 81 |
+
data-widget-type={widget.type}
|
| 82 |
+
onDragOver={(e) => { e.preventDefault(); onDragOver(widget.id); }}
|
| 83 |
+
onDrop={(e) => { e.preventDefault(); onDrop(widget.id); }}
|
| 84 |
+
>
|
| 85 |
+
<div
|
| 86 |
+
className="widget-head"
|
| 87 |
+
draggable
|
| 88 |
+
onDragStart={(e) => { onDragStart(widget.id); e.dataTransfer.effectAllowed = 'move'; }}
|
| 89 |
+
onDragEnd={onDragEnd}
|
| 90 |
>
|
| 91 |
+
<span className="drag-grip"><Icon name="grip" size={14}/></span>
|
| 92 |
+
<div className="widget-icon"><Icon name={meta.icon} size={14}/></div>
|
| 93 |
+
<div className="widget-title">{meta.name}</div>
|
| 94 |
+
{meta.critic && <span className="widget-tag">wedge</span>}
|
| 95 |
+
<span className="size-pill" onClick={cycleSize} title="Cycle size S → M → L">{widget.size}</span>
|
| 96 |
+
<div className="widget-actions">
|
| 97 |
+
<button className="icon-btn" onClick={() => onRemove(widget.id, meta.name)} title="Remove"><Icon name="trash" size={13}/></button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
</div>
|
| 99 |
</div>
|
| 100 |
+
<div className="widget-body">
|
| 101 |
+
{renderWidget(widget.type, state, setState, openModal, allStates)}
|
| 102 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
</div>
|
| 104 |
);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// ============================================================================
|
| 108 |
+
// Insights view — AI-synthesized highlights with stats bar, filters, and
|
| 109 |
+
// click-to-expand source quotes.
|
| 110 |
+
// ============================================================================
|
| 111 |
+
const INSIGHT_CATEGORIES = [
|
| 112 |
+
{ id: 'all', label: 'All' },
|
| 113 |
+
{ id: 'open', label: 'Open' },
|
| 114 |
+
{ id: 'in-progress', label: 'In progress' },
|
| 115 |
+
{ id: 'completed', label: 'Completed' },
|
| 116 |
+
{ id: 'abandoned', label: 'Abandoned' },
|
| 117 |
+
{ id: 'pinned', label: 'Pinned' },
|
| 118 |
+
{ id: 'high', label: 'High confidence' },
|
| 119 |
+
{ id: 'progress', label: 'Progress' },
|
| 120 |
+
{ id: 'theory', label: 'Theory' },
|
| 121 |
+
{ id: 'literature', label: 'Literature' },
|
| 122 |
+
{ id: 'action', label: 'Actions' },
|
| 123 |
+
{ id: 'risk', label: 'Risks' },
|
| 124 |
+
];
|
| 125 |
+
const CATEGORY_TINT = {
|
| 126 |
+
progress: 'rgba(16, 185, 129, 0.12)',
|
| 127 |
+
theory: 'rgba(99, 102, 241, 0.12)',
|
| 128 |
+
literature: 'rgba(245, 158, 11, 0.12)',
|
| 129 |
+
action: 'rgba(59, 130, 246, 0.12)',
|
| 130 |
+
risk: 'rgba(220, 38, 38, 0.12)',
|
| 131 |
+
};
|
| 132 |
+
const CATEGORY_FG = {
|
| 133 |
+
progress: '#10B981',
|
| 134 |
+
theory: '#818CF8',
|
| 135 |
+
literature: '#F59E0B',
|
| 136 |
+
action: '#3B82F6',
|
| 137 |
+
risk: '#DC2626',
|
| 138 |
};
|
| 139 |
+
const confidenceTier = (c) => c >= 75 ? 'high' : c >= 60 ? 'med' : 'low';
|
| 140 |
|
| 141 |
+
// Task statuses live on each individual bullet within an insight, not on the
|
| 142 |
+
// whole card — Daniel's feedback: "each card is a discrete task, not a set of tasks".
|
| 143 |
+
const TASK_STATUSES = [
|
| 144 |
+
{ id: 'open', label: 'Open', color: 'var(--canvas-text-3)', icon: 'sparkles' },
|
| 145 |
+
{ id: 'in-progress', label: 'In progress', color: '#3B82F6', icon: 'graph' },
|
| 146 |
+
{ id: 'completed', label: 'Completed', color: '#10B981', icon: 'check' },
|
| 147 |
+
{ id: 'abandoned', label: 'Abandoned', color: 'var(--canvas-text-4)', icon: 'x' },
|
| 148 |
+
];
|
| 149 |
+
const TASK_STATUS_KEY = 'canvas-task-status-v1';
|
| 150 |
+
const taskKey = (insId, idx) => `${insId}::${idx}`;
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
+
function InsightsView({ widgetStates, setWidgetStates, onNavigateToChat }) {
|
| 153 |
+
const [pinned, setPinned] = useState(() => new Set(INSIGHTS.filter(i => i.pinned).map(i => i.id)));
|
| 154 |
+
const [taskStatuses, setTaskStatuses] = useState(() => {
|
| 155 |
+
try { return JSON.parse(localStorage.getItem(TASK_STATUS_KEY) || '{}'); } catch { return {}; }
|
| 156 |
+
});
|
| 157 |
+
const [filter, setFilter] = useState('all');
|
| 158 |
+
const [sortBy, setSortBy] = useState('confidence');
|
| 159 |
+
const [expanded, setExpanded] = useState(new Set());
|
| 160 |
+
const [refreshing, setRefreshing] = useState(false);
|
| 161 |
+
const [openStatusMenu, setOpenStatusMenu] = useState(null);
|
| 162 |
+
// 'cards' = current cards-of-tasks layout, 'tasks' = flat task list per Daniel's
|
| 163 |
+
// "Sections in sidebar, Tasks in the main view" suggestion.
|
| 164 |
+
const [viewMode, setViewMode] = useState(() => localStorage.getItem('canvas-insights-view') || 'cards');
|
| 165 |
+
useEffect(() => { localStorage.setItem('canvas-insights-view', viewMode); }, [viewMode]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
+
useEffect(() => {
|
| 168 |
+
localStorage.setItem(TASK_STATUS_KEY, JSON.stringify(taskStatuses));
|
| 169 |
+
}, [taskStatuses]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
+
const taskStatusOf = (insId, idx) => taskStatuses[taskKey(insId, idx)] || 'open';
|
| 172 |
+
const setTaskStatus = (insId, idx, status) => {
|
| 173 |
+
setTaskStatuses(prev => ({ ...prev, [taskKey(insId, idx)]: status }));
|
| 174 |
+
const lbl = TASK_STATUSES.find(s => s.id === status)?.label || status;
|
| 175 |
+
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg: `Task marked ${lbl}`, kind: 'success' } }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
};
|
| 177 |
|
| 178 |
+
// Roll up to a card-level status: completed if all tasks done, abandoned if all abandoned,
|
| 179 |
+
// in-progress if any are in-progress, otherwise open.
|
| 180 |
+
const insightRollup = (ins) => {
|
| 181 |
+
const states = ins.bullets.map((_, idx) => taskStatusOf(ins.id, idx));
|
| 182 |
+
const total = states.length;
|
| 183 |
+
if (total === 0) return { state: 'open', done: 0, total: 0, pct: 0 };
|
| 184 |
+
const done = states.filter(s => s === 'completed').length;
|
| 185 |
+
const inProg = states.filter(s => s === 'in-progress').length;
|
| 186 |
+
const abandoned = states.filter(s => s === 'abandoned').length;
|
| 187 |
+
let state = 'open';
|
| 188 |
+
if (done === total) state = 'completed';
|
| 189 |
+
else if (abandoned === total) state = 'abandoned';
|
| 190 |
+
else if (inProg > 0 || done > 0) state = 'in-progress';
|
| 191 |
+
return { state, done, total, inProg, abandoned, pct: Math.round((done / total) * 100) };
|
|
|
|
|
|
|
| 192 |
};
|
| 193 |
|
| 194 |
+
const togglePin = (id) => {
|
| 195 |
+
setPinned(prev => {
|
| 196 |
+
const n = new Set(prev);
|
| 197 |
+
if (n.has(id)) n.delete(id); else n.add(id);
|
| 198 |
+
return n;
|
| 199 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
};
|
| 201 |
+
const toggleExpand = (id) => {
|
| 202 |
+
setExpanded(prev => {
|
| 203 |
+
const n = new Set(prev);
|
| 204 |
+
if (n.has(id)) n.delete(id); else n.add(id);
|
| 205 |
+
return n;
|
| 206 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
};
|
| 208 |
|
| 209 |
+
const insightToTaskTitle = (ins) => {
|
| 210 |
+
const plain = (ins.bullets[0] || ins.summary || ins.title).replace(/<[^>]+>/g, '');
|
| 211 |
+
return plain.length > 80 ? plain.slice(0, 77) + '…' : plain;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
};
|
| 213 |
|
| 214 |
+
const sendToKanban = (ins) => {
|
| 215 |
+
if (!setWidgetStates) return;
|
| 216 |
+
const kanban = widgetStates.kanban || EMPTY_STATE.kanban;
|
| 217 |
+
const card = {
|
| 218 |
+
id: 'k' + Date.now(),
|
| 219 |
+
col: 'todo',
|
| 220 |
+
title: insightToTaskTitle(ins),
|
| 221 |
+
priority: 'med',
|
| 222 |
+
meta: `from Insights · ${ins.title}`,
|
| 223 |
+
};
|
| 224 |
+
setWidgetStates(s => ({ ...s, kanban: { ...kanban, cards: [...kanban.cards, card] } }));
|
| 225 |
+
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg: 'Sent to Kanban (To Do)', kind: 'success' } }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
};
|
| 227 |
|
| 228 |
+
// TODO(LLM): real refresh hits the orchestrator and re-synthesizes insights.
|
| 229 |
+
const handleRefresh = () => {
|
| 230 |
+
setRefreshing(true);
|
| 231 |
+
setTimeout(() => setRefreshing(false), 900);
|
| 232 |
+
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg: 'Refreshing insights… (stub)', kind: 'success' } }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
};
|
| 234 |
|
| 235 |
+
// "Ask follow-up": stash a draft prompt + insight context that the chat page
|
| 236 |
+
// can pick up on next navigation. Backend hookup TODO: include source-conversation IDs.
|
| 237 |
+
const askFollowUp = (ins) => {
|
| 238 |
+
const plain = (ins.bullets[0] || ins.summary || '').replace(/<[^>]+>/g, '');
|
| 239 |
+
const prompt = `Follow up on the insight "${ins.title}": ${plain}`;
|
| 240 |
+
try {
|
| 241 |
+
localStorage.setItem('canvas-chat-handoff', JSON.stringify({
|
| 242 |
+
at: Date.now(),
|
| 243 |
+
prompt,
|
| 244 |
+
insightId: ins.id,
|
| 245 |
+
insightTitle: ins.title,
|
| 246 |
+
}));
|
| 247 |
+
} catch { /* ignore */ }
|
| 248 |
+
window.dispatchEvent(new CustomEvent('canvas-toast', {
|
| 249 |
+
detail: { msg: `Follow-up drafted: "${ins.title}" — opening chat`, kind: 'success' },
|
| 250 |
}));
|
| 251 |
+
// Use the existing navigation prop if present; falls back to the global event.
|
| 252 |
+
if (onNavigateToChat) onNavigateToChat();
|
| 253 |
};
|
| 254 |
|
| 255 |
+
const filtered = useMemo(() => {
|
| 256 |
+
let r = INSIGHTS;
|
| 257 |
+
if (filter === 'pinned') r = r.filter(i => pinned.has(i.id));
|
| 258 |
+
else if (filter === 'high') r = r.filter(i => i.confidence >= 75);
|
| 259 |
+
else if (['open', 'in-progress', 'completed', 'abandoned'].includes(filter)) r = r.filter(i => insightRollup(i).state === filter);
|
| 260 |
+
else if (filter !== 'all') r = r.filter(i => i.category === filter);
|
| 261 |
+
if (sortBy === 'confidence') r = [...r].sort((a, b) => b.confidence - a.confidence);
|
| 262 |
+
if (sortBy === 'recent') r = [...r].sort((a, b) => (a.updatedMinutesAgo || 0) - (b.updatedMinutesAgo || 0));
|
| 263 |
+
if (sortBy === 'progress') r = [...r].sort((a, b) => insightRollup(b).pct - insightRollup(a).pct);
|
| 264 |
+
return r;
|
| 265 |
+
}, [filter, sortBy, pinned, taskStatuses]); // eslint-disable-line react-hooks/exhaustive-deps
|
| 266 |
|
| 267 |
+
// Aggregate stats over individual TASKS, not insights (Daniel's framing)
|
| 268 |
+
const stats = useMemo(() => {
|
| 269 |
+
const allTasks = INSIGHTS.flatMap(i => i.bullets.map((_, idx) => taskStatusOf(i.id, idx)));
|
| 270 |
+
const taskTotal = allTasks.length;
|
| 271 |
+
const completed = allTasks.filter(s => s === 'completed').length;
|
| 272 |
+
const inProgress = allTasks.filter(s => s === 'in-progress').length;
|
| 273 |
+
const abandoned = allTasks.filter(s => s === 'abandoned').length;
|
| 274 |
+
const open = taskTotal - completed - inProgress - abandoned;
|
| 275 |
+
const totalSources = INSIGHTS.reduce((s, i) => s + (i.sources || 0), 0);
|
| 276 |
+
const avgConf = Math.round(INSIGHTS.reduce((s, i) => s + i.confidence, 0) / INSIGHTS.length);
|
| 277 |
+
return {
|
| 278 |
+
sections: INSIGHTS.length, taskTotal, completed, inProgress, abandoned, open,
|
| 279 |
+
totalSources, avgConf, pinnedCount: pinned.size,
|
| 280 |
+
};
|
| 281 |
+
}, [pinned, taskStatuses]); // eslint-disable-line react-hooks/exhaustive-deps
|
| 282 |
+
|
| 283 |
+
const lastUpdated = Math.min(...INSIGHTS.map(i => i.updatedMinutesAgo || 0));
|
| 284 |
|
| 285 |
+
// Empty state — defensive (current data is hardcoded but a real backend could send [])
|
| 286 |
+
if (INSIGHTS.length === 0) {
|
| 287 |
return (
|
| 288 |
+
<>
|
| 289 |
+
<div className="page-header">
|
| 290 |
+
<div>
|
| 291 |
+
<h1 className="page-title">Insights</h1>
|
| 292 |
+
<div className="page-sub">AI-synthesized from your research conversations.</div>
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
<div className="empty-cell">
|
| 296 |
+
<Icon name="sparkles" size={32} style={{ color: 'var(--canvas-text-4)' }}/>
|
| 297 |
+
<div style={{ fontSize: 14, color: 'var(--canvas-text-2)', fontWeight: 500 }}>No insights yet</div>
|
| 298 |
+
<div>Have a conversation with your advisors and insights will appear here.</div>
|
| 299 |
+
</div>
|
| 300 |
+
</>
|
| 301 |
);
|
| 302 |
}
|
| 303 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
return (
|
| 305 |
+
<>
|
| 306 |
+
<div className="page-header">
|
| 307 |
+
<div>
|
| 308 |
+
<h1 className="page-title">Insights</h1>
|
| 309 |
+
<div className="page-sub">AI-synthesized from your research conversations.</div>
|
| 310 |
+
</div>
|
| 311 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
+
{/* Stats bar */}
|
| 314 |
+
<div className="insights-stats">
|
| 315 |
+
<div className="insights-stat">
|
| 316 |
+
<span className="insights-stat-value">{stats.completed}/{stats.taskTotal}</span>
|
| 317 |
+
<span className="insights-stat-label">tasks done</span>
|
| 318 |
+
</div>
|
| 319 |
+
<div className="insights-stat insights-stat-progress">
|
| 320 |
+
<div className="insights-progress-bar">
|
| 321 |
+
<i className="ip-completed" style={{ width: `${(stats.completed / Math.max(1, stats.taskTotal)) * 100}%` }}/>
|
| 322 |
+
<i className="ip-inprogress" style={{ width: `${(stats.inProgress / Math.max(1, stats.taskTotal)) * 100}%` }}/>
|
| 323 |
+
<i className="ip-abandoned" style={{ width: `${(stats.abandoned / Math.max(1, stats.taskTotal)) * 100}%` }}/>
|
| 324 |
</div>
|
| 325 |
+
<span className="insights-stat-label">
|
| 326 |
+
{stats.completed} done · {stats.inProgress} in progress · {stats.open} open
|
| 327 |
+
{stats.abandoned > 0 && ` · ${stats.abandoned} abandoned`}
|
| 328 |
+
</span>
|
| 329 |
</div>
|
| 330 |
+
<div className="insights-stat">
|
| 331 |
+
<span className="insights-stat-value">{stats.sections}</span>
|
| 332 |
+
<span className="insights-stat-label">sections</span>
|
| 333 |
+
</div>
|
| 334 |
+
<div className="insights-stat">
|
| 335 |
+
<span className="insights-stat-value">{stats.avgConf}%</span>
|
| 336 |
+
<span className="insights-stat-label">avg confidence</span>
|
| 337 |
</div>
|
| 338 |
+
<span style={{ flex: 1 }}/>
|
| 339 |
+
<span className="insights-stat-update">
|
| 340 |
+
<span className="dot"/>
|
| 341 |
+
updated {lastUpdated} min ago
|
| 342 |
+
</span>
|
| 343 |
+
<button className="btn btn-ghost" onClick={handleRefresh} disabled={refreshing}>
|
| 344 |
+
{refreshing ? <div className="spinner"/> : <Icon name="refresh" size={13}/>}
|
| 345 |
+
Refresh
|
| 346 |
+
</button>
|
| 347 |
</div>
|
| 348 |
|
| 349 |
+
{/* View toggle: Cards (sections with their tasks) vs Tasks (flat list) */}
|
| 350 |
+
<div className="insights-view-toggle">
|
| 351 |
+
<button className={viewMode === 'cards' ? 'active' : ''} onClick={() => setViewMode('cards')}>
|
| 352 |
+
<Icon name="layout" size={12}/>Cards
|
| 353 |
+
</button>
|
| 354 |
+
<button className={viewMode === 'tasks' ? 'active' : ''} onClick={() => setViewMode('tasks')}>
|
| 355 |
+
<Icon name="task" size={12}/>Tasks
|
| 356 |
+
</button>
|
| 357 |
+
</div>
|
| 358 |
+
|
| 359 |
+
{/* Filter + sort */}
|
| 360 |
+
<div className="insights-filters">
|
| 361 |
+
<div className="palette-cats" style={{ marginBottom: 0 }}>
|
| 362 |
+
{INSIGHT_CATEGORIES.map(c => {
|
| 363 |
+
const count =
|
| 364 |
+
c.id === 'all' ? INSIGHTS.length :
|
| 365 |
+
c.id === 'pinned' ? pinned.size :
|
| 366 |
+
c.id === 'high' ? INSIGHTS.filter(i => i.confidence >= 75).length :
|
| 367 |
+
['open', 'in-progress', 'completed', 'abandoned'].includes(c.id) ? INSIGHTS.filter(i => insightRollup(i).state === c.id).length :
|
| 368 |
+
INSIGHTS.filter(i => i.category === c.id).length;
|
| 369 |
+
if (count === 0 && c.id !== 'all') return null;
|
| 370 |
+
return (
|
| 371 |
+
<button key={c.id}
|
| 372 |
+
className={`palette-cat ${filter === c.id ? 'active' : ''}`}
|
| 373 |
+
onClick={() => setFilter(c.id)}>
|
| 374 |
+
{c.label}<span style={{ marginLeft: 6, opacity: 0.6 }}>{count}</span>
|
| 375 |
+
</button>
|
| 376 |
+
);
|
| 377 |
+
})}
|
| 378 |
</div>
|
| 379 |
+
<select className="select" style={{ width: 'auto', padding: '4px 8px', fontSize: 11, fontFamily: 'var(--canvas-mono)' }}
|
| 380 |
+
value={sortBy} onChange={e => setSortBy(e.target.value)}>
|
| 381 |
+
<option value="confidence">↓ confidence</option>
|
| 382 |
+
<option value="recent">↓ recent</option>
|
| 383 |
+
<option value="progress">↓ progress</option>
|
| 384 |
+
</select>
|
| 385 |
+
</div>
|
| 386 |
+
|
| 387 |
+
{/* Pinned strip — only when there are pins and we're not already filtering by pinned */}
|
| 388 |
+
{pinned.size > 0 && filter !== 'pinned' && (
|
| 389 |
+
<div className="insights-pinned-strip">
|
| 390 |
+
<span style={{ fontSize: 11, color: 'var(--canvas-text-4)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>
|
| 391 |
+
Pinned
|
| 392 |
</span>
|
| 393 |
+
{INSIGHTS.filter(i => pinned.has(i.id)).map(ins => (
|
| 394 |
+
<button key={ins.id} className="insights-pinned-pill" onClick={() => {
|
| 395 |
+
const el = document.getElementById(`insight-${ins.id}`);
|
| 396 |
+
if (el) {
|
| 397 |
+
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
| 398 |
+
el.style.boxShadow = '0 0 0 2px var(--canvas-accent), 0 0 24px var(--canvas-accent-glow)';
|
| 399 |
+
setTimeout(() => { el.style.boxShadow = ''; }, 1400);
|
| 400 |
+
}
|
| 401 |
+
}}>
|
| 402 |
+
<Icon name={ins.icon} size={11}/>{ins.title}
|
| 403 |
+
</button>
|
| 404 |
+
))}
|
| 405 |
</div>
|
| 406 |
+
)}
|
| 407 |
|
| 408 |
+
{/* TASKS view — flat list of every bullet across insights */}
|
| 409 |
+
{viewMode === 'tasks' && (() => {
|
| 410 |
+
const allTasks = filtered.flatMap(ins => ins.bullets.map((b, idx) => ({
|
| 411 |
+
ins, idx, text: b, status: taskStatusOf(ins.id, idx),
|
| 412 |
+
})));
|
| 413 |
+
const statusOrder = { open: 0, 'in-progress': 1, completed: 2, abandoned: 3 };
|
| 414 |
+
const sorted = [...allTasks].sort((a, b) => statusOrder[a.status] - statusOrder[b.status]);
|
| 415 |
+
if (sorted.length === 0) {
|
| 416 |
+
return (
|
| 417 |
+
<div className="empty-cell">
|
| 418 |
+
<Icon name="task" size={28} style={{ color: 'var(--canvas-text-4)' }}/>
|
| 419 |
+
<div style={{ fontSize: 14, color: 'var(--canvas-text-2)', fontWeight: 500 }}>No tasks match this filter</div>
|
| 420 |
+
<button className="btn btn-ghost" onClick={() => setFilter('all')}>Show all</button>
|
| 421 |
+
</div>
|
| 422 |
+
);
|
| 423 |
+
}
|
| 424 |
+
return (
|
| 425 |
+
<div className="insights-tasks-list">
|
| 426 |
+
{sorted.map(({ ins, idx, text, status }) => {
|
| 427 |
+
const meta = TASK_STATUSES.find(s => s.id === status);
|
| 428 |
+
const menuKey = `tv::${ins.id}::${idx}`;
|
| 429 |
+
const menuOpen = openStatusMenu === menuKey;
|
| 430 |
+
return (
|
| 431 |
+
<div key={menuKey} className={`insights-task-row task-${status}`}>
|
|
|
|
| 432 |
<button
|
| 433 |
+
className="insight-task-check"
|
| 434 |
+
style={{ color: meta.color, borderColor: meta.color + '60' }}
|
| 435 |
+
onClick={() => setOpenStatusMenu(menuOpen ? null : menuKey)}
|
| 436 |
+
title={`Status: ${meta.label}`}
|
| 437 |
>
|
| 438 |
+
{status === 'completed' && <Icon name="check" size={11}/>}
|
| 439 |
+
{status === 'in-progress' && <span className="task-check-dot"/>}
|
| 440 |
+
{status === 'abandoned' && <Icon name="x" size={10}/>}
|
| 441 |
</button>
|
| 442 |
+
<div className="insights-task-body">
|
| 443 |
+
<button
|
| 444 |
+
className="insights-task-section"
|
| 445 |
+
onClick={() => {
|
| 446 |
+
setViewMode('cards');
|
| 447 |
+
setTimeout(() => {
|
| 448 |
+
const el = document.getElementById(`insight-${ins.id}`);
|
| 449 |
+
if (el) el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
| 450 |
+
}, 60);
|
| 451 |
+
}}
|
| 452 |
+
title="Jump to this section"
|
| 453 |
+
>
|
| 454 |
+
<Icon name={ins.icon} size={10}/>{ins.title}
|
| 455 |
+
</button>
|
| 456 |
+
<span className="insights-task-text" dangerouslySetInnerHTML={{ __html: text }}/>
|
| 457 |
+
</div>
|
| 458 |
+
<button className="chip" onClick={() => sendToKanban(ins)} title="Send this section's open tasks to Kanban">
|
| 459 |
+
<Icon name="task" size={11}/>Kanban
|
| 460 |
</button>
|
| 461 |
+
{menuOpen && (
|
| 462 |
+
<div className="insight-status-menu" onMouseLeave={() => setOpenStatusMenu(null)}>
|
| 463 |
+
{TASK_STATUSES.map(s => (
|
| 464 |
+
<button key={s.id}
|
| 465 |
+
className={status === s.id ? 'active' : ''}
|
| 466 |
+
onClick={() => { setTaskStatus(ins.id, idx, s.id); setOpenStatusMenu(null); }}>
|
| 467 |
+
<Icon name={s.icon} size={11} style={{ color: s.color }}/>
|
| 468 |
+
{s.label}
|
| 469 |
+
</button>
|
| 470 |
+
))}
|
| 471 |
+
</div>
|
| 472 |
+
)}
|
| 473 |
</div>
|
| 474 |
+
);
|
| 475 |
+
})}
|
| 476 |
</div>
|
| 477 |
+
);
|
| 478 |
+
})()}
|
| 479 |
+
|
| 480 |
+
{/* CARDS view (default) */}
|
| 481 |
+
{viewMode === 'cards' && (filtered.length === 0 ? (
|
| 482 |
+
<div className="empty-cell">
|
| 483 |
+
<Icon name="search" size={28} style={{ color: 'var(--canvas-text-4)' }}/>
|
| 484 |
+
<div style={{ fontSize: 14, color: 'var(--canvas-text-2)', fontWeight: 500 }}>No insights match this filter</div>
|
| 485 |
+
<button className="btn btn-ghost" onClick={() => setFilter('all')}>Show all</button>
|
| 486 |
+
</div>
|
| 487 |
+
) : (
|
| 488 |
+
<div className="insight-grid">
|
| 489 |
+
{filtered.map(ins => {
|
| 490 |
+
const isExpanded = expanded.has(ins.id);
|
| 491 |
+
const isPinned = pinned.has(ins.id);
|
| 492 |
+
const tier = confidenceTier(ins.confidence);
|
| 493 |
+
const tint = CATEGORY_TINT[ins.category] || 'var(--canvas-surface-2)';
|
| 494 |
+
const fg = CATEGORY_FG[ins.category] || 'var(--canvas-accent)';
|
| 495 |
+
const rollup = insightRollup(ins);
|
| 496 |
+
return (
|
| 497 |
+
<div
|
| 498 |
+
key={ins.id}
|
| 499 |
+
id={`insight-${ins.id}`}
|
| 500 |
+
className={`insight ${isPinned ? 'is-pinned' : ''} tier-${tier} status-${rollup.state}`}
|
| 501 |
+
>
|
| 502 |
+
<div className="insight-head">
|
| 503 |
+
<div className="insight-icon" style={{ background: tint, color: fg }}>
|
| 504 |
+
<Icon name={ins.icon} size={16}/>
|
| 505 |
+
</div>
|
| 506 |
+
<div className="insight-title">{ins.title}</div>
|
| 507 |
+
<ConfidenceRing value={ins.confidence}/>
|
| 508 |
+
</div>
|
| 509 |
+
|
| 510 |
+
{/* Per-card progress (rolls up the bullet tasks) */}
|
| 511 |
+
{rollup.total > 0 && (
|
| 512 |
+
<div className="insight-progress">
|
| 513 |
+
<div className="insight-progress-meta">
|
| 514 |
+
<span className="insight-progress-count">{rollup.done}/{rollup.total} tasks</span>
|
| 515 |
+
{rollup.state === 'completed' && <span className="insight-progress-badge done">✓ Resolved</span>}
|
| 516 |
+
{rollup.state === 'abandoned' && <span className="insight-progress-badge abandoned">Abandoned</span>}
|
| 517 |
+
{rollup.state === 'in-progress' && <span className="insight-progress-badge inprog">In progress</span>}
|
| 518 |
+
</div>
|
| 519 |
+
<div className="insight-progress-bar">
|
| 520 |
+
<i className="ip-completed" style={{ width: `${(rollup.done / rollup.total) * 100}%` }}/>
|
| 521 |
+
{rollup.inProg > 0 && <i className="ip-inprogress" style={{ width: `${(rollup.inProg / rollup.total) * 100}%` }}/>}
|
| 522 |
+
{rollup.abandoned > 0 && <i className="ip-abandoned" style={{ width: `${(rollup.abandoned / rollup.total) * 100}%` }}/>}
|
| 523 |
+
</div>
|
| 524 |
+
</div>
|
| 525 |
+
)}
|
| 526 |
+
|
| 527 |
+
<div className="insight-body">
|
| 528 |
+
<div>{ins.summary}</div>
|
| 529 |
+
<ul className="insight-tasks">
|
| 530 |
+
{ins.bullets.map((b, idx) => {
|
| 531 |
+
const status = taskStatusOf(ins.id, idx);
|
| 532 |
+
const meta = TASK_STATUSES.find(s => s.id === status);
|
| 533 |
+
const menuOpen = openStatusMenu === `${ins.id}::${idx}`;
|
| 534 |
+
return (
|
| 535 |
+
<li key={idx} className={`insight-task task-${status}`}>
|
| 536 |
+
<button
|
| 537 |
+
className="insight-task-check"
|
| 538 |
+
style={{ color: meta.color, borderColor: meta.color + '60' }}
|
| 539 |
+
onClick={() => setOpenStatusMenu(menuOpen ? null : `${ins.id}::${idx}`)}
|
| 540 |
+
title={`Status: ${meta.label}`}
|
| 541 |
+
>
|
| 542 |
+
{status === 'completed' && <Icon name="check" size={10}/>}
|
| 543 |
+
{status === 'in-progress' && <span className="task-check-dot"/>}
|
| 544 |
+
{status === 'abandoned' && <Icon name="x" size={9}/>}
|
| 545 |
+
</button>
|
| 546 |
+
<span className="insight-task-text" dangerouslySetInnerHTML={{ __html: b }}/>
|
| 547 |
+
{menuOpen && (
|
| 548 |
+
<div className="insight-status-menu" onMouseLeave={() => setOpenStatusMenu(null)}>
|
| 549 |
+
{TASK_STATUSES.map(s => (
|
| 550 |
+
<button key={s.id}
|
| 551 |
+
className={status === s.id ? 'active' : ''}
|
| 552 |
+
onClick={() => { setTaskStatus(ins.id, idx, s.id); setOpenStatusMenu(null); }}>
|
| 553 |
+
<Icon name={s.icon} size={11} style={{ color: s.color }}/>
|
| 554 |
+
{s.label}
|
| 555 |
+
</button>
|
| 556 |
+
))}
|
| 557 |
+
</div>
|
| 558 |
+
)}
|
| 559 |
+
</li>
|
| 560 |
+
);
|
| 561 |
+
})}
|
| 562 |
+
</ul>
|
| 563 |
+
</div>
|
| 564 |
+
|
| 565 |
+
{/* Detail panel — quotes from sources, only when expanded */}
|
| 566 |
+
{isExpanded && ins.quotes && (
|
| 567 |
+
<div className="insight-detail">
|
| 568 |
+
<div className="insight-detail-head">Source quotes · {ins.sources} {ins.sources === 1 ? 'source' : 'sources'}</div>
|
| 569 |
+
{ins.quotes.map((q, i) => (
|
| 570 |
+
<div key={i} className="insight-quote">{q}</div>
|
| 571 |
+
))}
|
| 572 |
+
</div>
|
| 573 |
+
)}
|
| 574 |
+
|
| 575 |
+
<div className="insight-foot">
|
| 576 |
+
<span className="insight-foot-meta">
|
| 577 |
+
<Icon name="message" size={10}/> {ins.sources}
|
| 578 |
+
<span className="insight-dot"/>
|
| 579 |
+
updated {ins.updatedMinutesAgo}m ago
|
| 580 |
+
</span>
|
| 581 |
+
</div>
|
| 582 |
+
|
| 583 |
+
<div className="insight-actions">
|
| 584 |
+
<button className="chip" onClick={() => askFollowUp(ins)} title="Open this insight in a new chat session">
|
| 585 |
+
<Icon name="message" size={11}/>Ask follow-up
|
| 586 |
+
</button>
|
| 587 |
+
<button className="chip" onClick={() => sendToKanban(ins)} title="Add all open tasks from this insight to your Kanban (To Do)">
|
| 588 |
+
<Icon name="task" size={11}/>Add to Kanban
|
| 589 |
+
</button>
|
| 590 |
+
<button className="chip" onClick={() => toggleExpand(ins.id)}>
|
| 591 |
+
<Icon name="expand" size={11}/>{isExpanded ? 'Collapse' : 'Source quotes'}
|
| 592 |
+
</button>
|
| 593 |
+
<button className={`chip ${isPinned ? 'pinned' : ''}`} onClick={() => togglePin(ins.id)}>
|
| 594 |
+
<Icon name="pin" size={11}/>{isPinned ? 'Pinned' : 'Pin'}
|
| 595 |
+
</button>
|
| 596 |
+
</div>
|
| 597 |
+
</div>
|
| 598 |
+
);
|
| 599 |
+
})}
|
| 600 |
+
</div>
|
| 601 |
+
))}
|
| 602 |
+
</>
|
| 603 |
+
);
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
// Small SVG ring used for the confidence indicator
|
| 607 |
+
function ConfidenceRing({ value }) {
|
| 608 |
+
const r = 14;
|
| 609 |
+
const c = 2 * Math.PI * r;
|
| 610 |
+
const dash = c * (1 - value / 100);
|
| 611 |
+
const tier = confidenceTier(value);
|
| 612 |
+
const color = tier === 'high' ? '#10B981' : tier === 'med' ? '#F59E0B' : '#DC2626';
|
| 613 |
+
return (
|
| 614 |
+
<div className={`confidence-ring tier-${tier}`} title={`${value}% confidence (${tier})`}>
|
| 615 |
+
<svg width="32" height="32" viewBox="0 0 32 32">
|
| 616 |
+
<circle cx="16" cy="16" r={r} fill="none" stroke="var(--canvas-surface-3)" strokeWidth="3"/>
|
| 617 |
+
<circle cx="16" cy="16" r={r} fill="none" stroke={color} strokeWidth="3"
|
| 618 |
+
strokeDasharray={c} strokeDashoffset={dash} strokeLinecap="round"
|
| 619 |
+
transform="rotate(-90 16 16)"/>
|
| 620 |
+
</svg>
|
| 621 |
+
<span style={{ color }}>{value}</span>
|
| 622 |
+
</div>
|
| 623 |
+
);
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
function PresetPicker({ onPick }) {
|
| 627 |
+
return (
|
| 628 |
+
<div className="canvas-presets">
|
| 629 |
+
<div className="canvas-presets-head">
|
| 630 |
+
<div className="canvas-presets-title">Start from a preset</div>
|
| 631 |
+
<div className="canvas-presets-sub">Or skip and add widgets one at a time.</div>
|
| 632 |
+
</div>
|
| 633 |
+
<div className="canvas-presets-grid">
|
| 634 |
+
{WORKSPACE_PRESETS.map(p => (
|
| 635 |
+
<button key={p.id} className="canvas-preset-card" onClick={() => onPick(p)}>
|
| 636 |
+
<div className="canvas-preset-icon"><Icon name={p.icon} size={18}/></div>
|
| 637 |
+
<div className="canvas-preset-content">
|
| 638 |
+
<div className="canvas-preset-name">{p.name}</div>
|
| 639 |
+
<div className="canvas-preset-desc">{p.desc}</div>
|
| 640 |
+
<div className="canvas-preset-meta">{p.layout.length} widgets</div>
|
| 641 |
+
</div>
|
| 642 |
+
</button>
|
| 643 |
+
))}
|
| 644 |
+
</div>
|
| 645 |
+
</div>
|
| 646 |
+
);
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
function WorkspaceView({ openModal, layout, setLayout, widgetStates, setWidgetStates }) {
|
| 650 |
+
const [dragId, setDragId] = useState(null);
|
| 651 |
+
const [dragOverId, setDragOverId] = useState(null);
|
| 652 |
+
|
| 653 |
+
const onDragStart = (id) => setDragId(id);
|
| 654 |
+
const onDragOver = (id) => { if (id !== dragId) setDragOverId(id); };
|
| 655 |
+
const onDragEnd = () => { setDragId(null); setDragOverId(null); };
|
| 656 |
+
const onDrop = (targetId) => {
|
| 657 |
+
if (!dragId || dragId === targetId) { onDragEnd(); return; }
|
| 658 |
+
const next = [...layout];
|
| 659 |
+
const fromIdx = next.findIndex(w => w.id === dragId);
|
| 660 |
+
const toIdx = next.findIndex(w => w.id === targetId);
|
| 661 |
+
const [moved] = next.splice(fromIdx, 1);
|
| 662 |
+
next.splice(toIdx, 0, moved);
|
| 663 |
+
setLayout(next);
|
| 664 |
+
onDragEnd();
|
| 665 |
+
};
|
| 666 |
+
|
| 667 |
+
const setWState = (type) => (updater) => {
|
| 668 |
+
setWidgetStates(s => ({
|
| 669 |
+
...s,
|
| 670 |
+
[type]: typeof updater === 'function' ? updater(s[type]) : updater,
|
| 671 |
+
}));
|
| 672 |
+
};
|
| 673 |
+
|
| 674 |
+
const removeWidget = (id, label) => {
|
| 675 |
+
openModal('confirm-remove', {
|
| 676 |
+
label,
|
| 677 |
+
onConfirm: () => {
|
| 678 |
+
setLayout(l => l.filter(w => w.id !== id));
|
| 679 |
+
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg: label + ' removed', kind: 'success' } }));
|
| 680 |
+
},
|
| 681 |
+
});
|
| 682 |
+
};
|
| 683 |
+
|
| 684 |
+
const resizeWidget = (id, size) => setLayout(l => l.map(w => w.id === id ? { ...w, size } : w));
|
| 685 |
+
|
| 686 |
+
// Each widget starts from scratch — fresh empty state, no demo content.
|
| 687 |
+
const addWidget = (meta) => {
|
| 688 |
+
const id = 'w-' + Date.now();
|
| 689 |
+
setLayout(l => [...l, { id, type: meta.type, size: meta.defaultSize, critic: meta.critic }]);
|
| 690 |
+
if (EMPTY_STATE[meta.type]) {
|
| 691 |
+
setWidgetStates(s => ({ ...s, [meta.type]: JSON.parse(JSON.stringify(EMPTY_STATE[meta.type])) }));
|
| 692 |
+
}
|
| 693 |
+
};
|
| 694 |
+
|
| 695 |
+
const applyPreset = (preset) => {
|
| 696 |
+
setLayout(preset.layout.map(w => ({ ...w })));
|
| 697 |
+
// Seed empty state for any widget types not already present
|
| 698 |
+
const seeds = {};
|
| 699 |
+
preset.layout.forEach(w => {
|
| 700 |
+
if (!widgetStates[w.type] && EMPTY_STATE[w.type]) {
|
| 701 |
+
seeds[w.type] = JSON.parse(JSON.stringify(EMPTY_STATE[w.type]));
|
| 702 |
+
}
|
| 703 |
+
});
|
| 704 |
+
if (Object.keys(seeds).length) setWidgetStates(s => ({ ...s, ...seeds }));
|
| 705 |
+
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg: `${preset.name} preset loaded`, kind: 'success' } }));
|
| 706 |
+
};
|
| 707 |
+
|
| 708 |
+
const reset = () => {
|
| 709 |
+
if (!window.confirm('Reset workspace? All widgets and content will be cleared.')) return;
|
| 710 |
+
setLayout([]);
|
| 711 |
+
setWidgetStates({});
|
| 712 |
+
localStorage.removeItem(LAYOUT_KEY);
|
| 713 |
+
localStorage.removeItem(STATES_KEY);
|
| 714 |
+
};
|
| 715 |
+
|
| 716 |
+
return (
|
| 717 |
+
<>
|
| 718 |
+
<div className="page-header">
|
| 719 |
+
<div>
|
| 720 |
+
<h1 className="page-title">Workspace</h1>
|
| 721 |
+
<div className="page-sub">{layout.length} widgets · {layout.filter(w => w.critic).length} anti-yes-man · drag headers to reorder, click size pill to resize</div>
|
| 722 |
+
</div>
|
| 723 |
+
<div style={{ display: 'flex', gap: 8 }}>
|
| 724 |
+
<button className="btn btn-ghost" onClick={reset} title="Reset layout"><Icon name="reset" size={13}/>Reset</button>
|
| 725 |
+
<button className="btn btn-primary" onClick={() => openModal('palette', {
|
| 726 |
+
layout, onAdd: addWidget,
|
| 727 |
+
})}><Icon name="plus" size={13}/>Add widget</button>
|
| 728 |
+
</div>
|
| 729 |
+
</div>
|
| 730 |
+
{layout.length === 0 && (
|
| 731 |
+
<PresetPicker onPick={applyPreset}/>
|
| 732 |
+
)}
|
| 733 |
+
<div className="workspace">
|
| 734 |
+
{layout.length === 0 && (
|
| 735 |
+
<div className="empty-cell">
|
| 736 |
+
<Icon name="layout" size={28} style={{ color: 'var(--canvas-text-4)' }}/>
|
| 737 |
+
<div style={{ fontSize: 14, color: 'var(--canvas-text-2)', fontWeight: 500 }}>Or build from scratch</div>
|
| 738 |
+
<button className="btn btn-primary" onClick={() => openModal('palette', { layout, onAdd: addWidget })} style={{ marginTop: 6 }}>
|
| 739 |
+
<Icon name="plus" size={13}/>Add your first widget
|
| 740 |
+
</button>
|
| 741 |
</div>
|
| 742 |
)}
|
| 743 |
+
{layout.map((w) => (
|
| 744 |
+
<CanvasWidget
|
| 745 |
+
key={w.id}
|
| 746 |
+
widget={w}
|
| 747 |
+
isDragging={dragId === w.id}
|
| 748 |
+
isDragOver={dragOverId === w.id}
|
| 749 |
+
onDragStart={onDragStart}
|
| 750 |
+
onDragOver={onDragOver}
|
| 751 |
+
onDragEnd={onDragEnd}
|
| 752 |
+
onDrop={onDrop}
|
| 753 |
+
state={widgetStates[w.type] ?? (EMPTY_STATE[w.type] ?? {})}
|
| 754 |
+
setState={setWState(w.type)}
|
| 755 |
+
onRemove={removeWidget}
|
| 756 |
+
onResize={resizeWidget}
|
| 757 |
+
openModal={openModal}
|
| 758 |
+
allStates={widgetStates}
|
| 759 |
+
/>
|
| 760 |
+
))}
|
| 761 |
</div>
|
| 762 |
+
</>
|
| 763 |
+
);
|
| 764 |
+
}
|
| 765 |
|
| 766 |
+
function ModalRouter({ modal, onClose }) {
|
| 767 |
+
if (!modal) return null;
|
| 768 |
+
const handleBackdropClick = (e) => { if (e.target === e.currentTarget) onClose(); };
|
| 769 |
+
let content = null;
|
| 770 |
+
switch (modal.kind) {
|
| 771 |
+
case 'palette': content = <PaletteModal data={modal.data} onClose={onClose}/>; break;
|
| 772 |
+
case 'add-citation': content = <AddCitationModal data={modal.data} onClose={onClose}/>; break;
|
| 773 |
+
case 'add-task': content = <AddTaskModal data={modal.data} onClose={onClose}/>; break;
|
| 774 |
+
case 'add-deadline': content = <AddDeadlineModal data={modal.data} onClose={onClose}/>; break;
|
| 775 |
+
case 'log-words': content = <LogWordsModal data={modal.data} onClose={onClose}/>; break;
|
| 776 |
+
case 'confirm-remove': content = <ConfirmRemoveModal data={modal.data} onClose={onClose}/>; break;
|
| 777 |
+
case 'reviewer-2': content = <ReviewerModal data={modal.data} onClose={onClose}/>; break;
|
| 778 |
+
case 'devils-advocate': content = <DevilsModal data={modal.data} onClose={onClose}/>; break;
|
| 779 |
+
case 'scope-realism': content = <ScopeModal data={modal.data} onClose={onClose}/>; break;
|
| 780 |
+
case 'reading-paper': content = <ReadingPaperModal data={modal.data} onClose={onClose}/>; break;
|
| 781 |
+
case 'budget-item': content = <BudgetItemModal data={modal.data} onClose={onClose}/>; break;
|
| 782 |
+
case 'note': content = <NoteModal data={modal.data} onClose={onClose}/>; break;
|
| 783 |
+
case 'habit': content = <HabitModal data={modal.data} onClose={onClose}/>; break;
|
| 784 |
+
case 'goal': content = <GoalModal data={modal.data} onClose={onClose}/>; break;
|
| 785 |
+
case 'meeting': content = <MeetingModal data={modal.data} onClose={onClose}/>; break;
|
| 786 |
+
case 'command': content = <CommandPaletteModal data={modal.data} onClose={onClose}/>; break;
|
| 787 |
+
case 'global-search': content = <GlobalSearchModal data={modal.data} onClose={onClose}/>; break;
|
| 788 |
+
default: return null;
|
| 789 |
+
}
|
| 790 |
+
return <div className="canvas-modal-backdrop" onClick={handleBackdropClick}>{content}</div>;
|
| 791 |
+
}
|
| 792 |
|
| 793 |
+
function ToastStack() {
|
| 794 |
+
const [toasts, setToasts] = useState([]);
|
| 795 |
+
useEffect(() => {
|
| 796 |
+
const handler = (e) => {
|
| 797 |
+
const id = Date.now() + Math.random();
|
| 798 |
+
setToasts(t => [...t, { id, ...e.detail }]);
|
| 799 |
+
setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3500);
|
| 800 |
+
};
|
| 801 |
+
window.addEventListener('canvas-toast', handler);
|
| 802 |
+
return () => window.removeEventListener('canvas-toast', handler);
|
| 803 |
+
}, []);
|
| 804 |
+
return (
|
| 805 |
+
<div className="toast-stack">
|
| 806 |
+
{toasts.map(t => (
|
| 807 |
+
<div key={t.id} className={`toast ${t.kind || 'success'}`}>
|
| 808 |
+
<Icon name={t.kind === 'critic' ? 'gavel' : t.kind === 'danger' ? 'alert' : 'check'} size={14}/>
|
| 809 |
+
<span>{t.msg}</span>
|
| 810 |
</div>
|
| 811 |
+
))}
|
| 812 |
+
</div>
|
| 813 |
+
);
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
const CanvasPage = ({ user, authToken, onNavigateToHome, onNavigateToChat, onSignOut }) => {
|
| 818 |
+
const { theme, toggleTheme } = useTheme();
|
| 819 |
+
useAppConfig();
|
| 820 |
+
const [view, setView] = useState(() => localStorage.getItem(VIEW_KEY) || 'workspace');
|
| 821 |
+
const [modal, setModal] = useState(null);
|
| 822 |
+
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
| 823 |
+
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
| 824 |
+
const [tourForceShow, setTourForceShow] = useState(0);
|
| 825 |
+
|
| 826 |
+
const [layout, setLayout] = useState(() => {
|
| 827 |
+
try {
|
| 828 |
+
const saved = localStorage.getItem(LAYOUT_KEY);
|
| 829 |
+
return saved ? JSON.parse(saved) : DEFAULT_LAYOUT;
|
| 830 |
+
} catch { return DEFAULT_LAYOUT; }
|
| 831 |
+
});
|
| 832 |
+
const [widgetStates, setWidgetStates] = useState(() => {
|
| 833 |
+
try {
|
| 834 |
+
const saved = localStorage.getItem(STATES_KEY);
|
| 835 |
+
return saved ? JSON.parse(saved) : {};
|
| 836 |
+
} catch { return {}; }
|
| 837 |
+
});
|
| 838 |
+
|
| 839 |
+
useEffect(() => { localStorage.setItem(LAYOUT_KEY, JSON.stringify(layout)); }, [layout]);
|
| 840 |
+
useEffect(() => { localStorage.setItem(STATES_KEY, JSON.stringify(widgetStates)); }, [widgetStates]);
|
| 841 |
+
useEffect(() => { localStorage.setItem(VIEW_KEY, view); }, [view]);
|
| 842 |
+
|
| 843 |
+
// Apply canvas theme attribute on body for scoped styling
|
| 844 |
+
useEffect(() => {
|
| 845 |
+
document.body.dataset.canvasTheme = theme;
|
| 846 |
+
return () => { delete document.body.dataset.canvasTheme; };
|
| 847 |
+
}, [theme]);
|
| 848 |
+
|
| 849 |
+
const openModal = useCallback((kind, data = {}) => setModal({ kind, data }), []);
|
| 850 |
+
const closeModal = useCallback(() => setModal(null), []);
|
| 851 |
+
|
| 852 |
+
const exportWorkspace = useCallback(() => {
|
| 853 |
+
const data = { layout, states: widgetStates };
|
| 854 |
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
| 855 |
+
const a = document.createElement('a');
|
| 856 |
+
a.href = URL.createObjectURL(blob);
|
| 857 |
+
a.download = 'canvas-workspace.json';
|
| 858 |
+
a.click();
|
| 859 |
+
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg: 'Workspace exported as JSON', kind: 'success' } }));
|
| 860 |
+
}, [layout, widgetStates]);
|
| 861 |
+
|
| 862 |
+
const openCommandPalette = useCallback(() => {
|
| 863 |
+
openModal('command', {
|
| 864 |
+
layout,
|
| 865 |
+
onSetView: (v) => setView(v),
|
| 866 |
+
onAddWidget: (meta) => {
|
| 867 |
+
const id = 'w-' + Date.now();
|
| 868 |
+
setLayout(l => [...l, { id, type: meta.type, size: meta.defaultSize, critic: meta.critic }]);
|
| 869 |
+
if (EMPTY_STATE[meta.type]) {
|
| 870 |
+
setWidgetStates(s => ({ ...s, [meta.type]: JSON.parse(JSON.stringify(EMPTY_STATE[meta.type])) }));
|
| 871 |
+
}
|
| 872 |
+
},
|
| 873 |
+
onToggleTheme: toggleTheme,
|
| 874 |
+
onExport: exportWorkspace,
|
| 875 |
+
});
|
| 876 |
+
}, [openModal, layout, toggleTheme, exportWorkspace]);
|
| 877 |
+
|
| 878 |
+
const openGlobalSearch = useCallback(() => {
|
| 879 |
+
openModal('global-search', { states: widgetStates });
|
| 880 |
+
}, [openModal, widgetStates]);
|
| 881 |
+
|
| 882 |
+
// Critic widgets dispatch `canvas-open-in-chat` when the user wants real LLM history.
|
| 883 |
+
useEffect(() => {
|
| 884 |
+
const handler = () => onNavigateToChat && onNavigateToChat();
|
| 885 |
+
window.addEventListener('canvas-open-in-chat', handler);
|
| 886 |
+
return () => window.removeEventListener('canvas-open-in-chat', handler);
|
| 887 |
+
}, [onNavigateToChat]);
|
| 888 |
+
|
| 889 |
+
// Esc closes modal, ⌘K opens command palette, ⌘/ opens global content search,
|
| 890 |
+
// ? opens the welcome tour for help (matches the icon in the topbar).
|
| 891 |
+
useEffect(() => {
|
| 892 |
+
const k = (e) => {
|
| 893 |
+
if (e.key === 'Escape') closeModal();
|
| 894 |
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
| 895 |
+
e.preventDefault();
|
| 896 |
+
openCommandPalette();
|
| 897 |
+
}
|
| 898 |
+
if ((e.metaKey || e.ctrlKey) && e.key === '/') {
|
| 899 |
+
e.preventDefault();
|
| 900 |
+
openGlobalSearch();
|
| 901 |
+
}
|
| 902 |
+
// ? key (Shift+/) — only when the user isn't typing in an input
|
| 903 |
+
if (e.key === '?' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName)) {
|
| 904 |
+
e.preventDefault();
|
| 905 |
+
setTourForceShow(n => n + 1);
|
| 906 |
+
}
|
| 907 |
+
};
|
| 908 |
+
window.addEventListener('keydown', k);
|
| 909 |
+
return () => window.removeEventListener('keydown', k);
|
| 910 |
+
}, [closeModal, openCommandPalette, openGlobalSearch]);
|
| 911 |
+
|
| 912 |
+
// Highlight a widget when picked from the sidebar
|
| 913 |
+
const flashScrollTo = (selector) => {
|
| 914 |
+
const el = document.querySelector(selector);
|
| 915 |
+
if (!el) return;
|
| 916 |
+
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
| 917 |
+
el.style.boxShadow = '0 0 0 2px var(--canvas-accent), 0 0 24px var(--canvas-accent-glow)';
|
| 918 |
+
setTimeout(() => { el.style.boxShadow = ''; }, 1400);
|
| 919 |
+
};
|
| 920 |
+
|
| 921 |
+
// Workspace: group widgets by category for the new sidebar
|
| 922 |
+
const widgetGroups = useMemo(() => {
|
| 923 |
+
const groups = {};
|
| 924 |
+
layout.forEach(w => {
|
| 925 |
+
const meta = WIDGET_CATALOG.find(m => m.type === w.type);
|
| 926 |
+
if (!meta) return;
|
| 927 |
+
const cat = meta.cat;
|
| 928 |
+
(groups[cat] ||= { id: cat, label: cat, items: [] }).items.push({
|
| 929 |
+
id: w.id,
|
| 930 |
+
label: meta.name,
|
| 931 |
+
icon: meta.icon,
|
| 932 |
+
critic: meta.critic,
|
| 933 |
+
onClick: () => flashScrollTo(`[data-widget-id="${w.id}"]`),
|
| 934 |
+
});
|
| 935 |
+
});
|
| 936 |
+
// Order: critic last
|
| 937 |
+
const order = ['research', 'writing', 'project', 'wellness', 'career', 'data', 'practical', 'critic'];
|
| 938 |
+
return order.map(c => groups[c]).filter(Boolean);
|
| 939 |
+
}, [layout]);
|
| 940 |
+
|
| 941 |
+
// Insights: list of sections — Daniel's feedback said sidebar should show sections here
|
| 942 |
+
const insightSections = useMemo(() => {
|
| 943 |
+
let taskMap = {};
|
| 944 |
+
try { taskMap = JSON.parse(localStorage.getItem(TASK_STATUS_KEY) || '{}'); } catch { /* ignore */ }
|
| 945 |
+
return INSIGHTS.map(ins => {
|
| 946 |
+
const states = ins.bullets.map((_, idx) => taskMap[taskKey(ins.id, idx)] || 'open');
|
| 947 |
+
const done = states.filter(s => s === 'completed').length;
|
| 948 |
+
return {
|
| 949 |
+
id: ins.id,
|
| 950 |
+
name: ins.title,
|
| 951 |
+
icon: ins.icon,
|
| 952 |
+
category: ins.category,
|
| 953 |
+
confidence: ins.confidence,
|
| 954 |
+
taskCount: ins.bullets.length,
|
| 955 |
+
doneCount: done,
|
| 956 |
+
onClick: () => flashScrollTo(`#insight-${ins.id}`),
|
| 957 |
+
};
|
| 958 |
+
});
|
| 959 |
+
}, [view, layout, widgetStates]); // eslint-disable-line react-hooks/exhaustive-deps
|
| 960 |
+
|
| 961 |
+
// Deliverables: list of projects with sections + history actions
|
| 962 |
+
const deliverableProjects = useMemo(() => {
|
| 963 |
+
try {
|
| 964 |
+
const dStore = JSON.parse(localStorage.getItem('canvas-deliverables-v2') || '{}');
|
| 965 |
+
const projects = Object.values(dStore.projects || {});
|
| 966 |
+
return projects.map(p => {
|
| 967 |
+
const t = DELIVERABLE_TEMPLATES.find(x => x.id === p.templateId);
|
| 968 |
+
return {
|
| 969 |
+
id: p.id,
|
| 970 |
+
name: p.name,
|
| 971 |
+
icon: t?.icon || 'book',
|
| 972 |
+
versions: p.versions?.length || 0,
|
| 973 |
+
isActive: p.id === dStore.activeProjectId,
|
| 974 |
+
sections: (t?.sections || []).map(s => ({
|
| 975 |
+
id: s.id,
|
| 976 |
+
name: s.name,
|
| 977 |
+
wc: ((p.sections || {})[s.id] || '').trim().split(/\s+/).filter(Boolean).length,
|
| 978 |
+
onClick: () => {
|
| 979 |
+
if (p.id !== dStore.activeProjectId) {
|
| 980 |
+
// Open this project first; section scroll happens after a tick.
|
| 981 |
+
const next = { ...dStore, activeProjectId: p.id };
|
| 982 |
+
localStorage.setItem('canvas-deliverables-v2', JSON.stringify(next));
|
| 983 |
+
window.dispatchEvent(new Event('storage'));
|
| 984 |
+
}
|
| 985 |
+
setTimeout(() => flashScrollTo(`#notion-section-${s.id}`), 80);
|
| 986 |
+
},
|
| 987 |
+
})),
|
| 988 |
+
onOpen: () => {
|
| 989 |
+
const next = { ...dStore, activeProjectId: p.id };
|
| 990 |
+
localStorage.setItem('canvas-deliverables-v2', JSON.stringify(next));
|
| 991 |
+
window.dispatchEvent(new Event('storage'));
|
| 992 |
+
},
|
| 993 |
+
};
|
| 994 |
+
});
|
| 995 |
+
} catch { return []; }
|
| 996 |
+
// re-derive when view or layout changes (layout proxy for "user did something")
|
| 997 |
+
}, [view, layout, widgetStates]); // eslint-disable-line react-hooks/exhaustive-deps
|
| 998 |
|
| 999 |
+
return (
|
| 1000 |
+
<div className="canvas-page-with-sidebar" data-canvas-theme={theme}>
|
| 1001 |
+
<Sidebar
|
| 1002 |
+
user={user}
|
| 1003 |
+
authToken={authToken}
|
| 1004 |
+
onSignOut={onSignOut}
|
| 1005 |
+
onSidebarToggle={setIsSidebarCollapsed}
|
| 1006 |
+
isMobileOpen={isMobileMenuOpen}
|
| 1007 |
+
onMobileToggle={setIsMobileMenuOpen}
|
| 1008 |
+
onNavigateToCanvas={() => {}}
|
| 1009 |
+
onSelectSession={(id) => onNavigateToChat && onNavigateToChat(id)}
|
| 1010 |
+
onNewChat={() => onNavigateToChat && onNavigateToChat()}
|
| 1011 |
+
pageContext="canvas"
|
| 1012 |
+
canvasSubview={view}
|
| 1013 |
+
widgetGroups={widgetGroups}
|
| 1014 |
+
deliverableProjects={deliverableProjects}
|
| 1015 |
+
insightSections={insightSections}
|
| 1016 |
/>
|
| 1017 |
+
<div className={`canvas-main-area ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
|
| 1018 |
+
<div className="canvas-app-shell">
|
| 1019 |
+
<AppHeader
|
| 1020 |
+
currentPage={`canvas-${view}`}
|
| 1021 |
+
onNavigateToHome={onNavigateToHome}
|
| 1022 |
+
onNavigateToChat={onNavigateToChat}
|
| 1023 |
+
onNavigateToCanvas={(v) => setView(v || 'workspace')}
|
| 1024 |
+
onMobileMenu={() => setIsMobileMenuOpen(true)}
|
| 1025 |
+
>
|
| 1026 |
+
<button className="icon-btn" onClick={() => setTourForceShow(n => n + 1)} title="Show tour">
|
| 1027 |
+
<HelpCircle size={18}/>
|
| 1028 |
+
</button>
|
| 1029 |
+
<button className="icon-btn" onClick={openGlobalSearch} title={`Search canvas content (${MOD}+/)`}>
|
| 1030 |
+
<Icon name="search" size={16}/>
|
| 1031 |
+
</button>
|
| 1032 |
+
<button className="icon-btn" onClick={openCommandPalette} title={`Commands (${MOD}+K)`}>
|
| 1033 |
+
<Icon name="zap" size={16}/>
|
| 1034 |
+
</button>
|
| 1035 |
+
</AppHeader>
|
| 1036 |
+
<div className="canvas-content">
|
| 1037 |
+
{view === 'insights' && <InsightsView widgetStates={widgetStates} setWidgetStates={setWidgetStates} onNavigateToChat={onNavigateToChat}/>}
|
| 1038 |
+
{view === 'workspace' && <WorkspaceView openModal={openModal} layout={layout} setLayout={setLayout} widgetStates={widgetStates} setWidgetStates={setWidgetStates}/>}
|
| 1039 |
+
{view === 'deliverables' && <DeliverablesView allStates={widgetStates}/>}
|
| 1040 |
+
</div>
|
| 1041 |
+
</div>
|
| 1042 |
+
</div>
|
| 1043 |
+
<ModalRouter modal={modal} onClose={closeModal}/>
|
| 1044 |
+
<ToastStack/>
|
| 1045 |
+
<CanvasWelcomeTour key={tourForceShow} forceShow={tourForceShow > 0}/>
|
| 1046 |
+
<ShortcutHint/>
|
| 1047 |
</div>
|
| 1048 |
);
|
| 1049 |
};
|
| 1050 |
|
| 1051 |
+
// Subtle floating hint bar showing the most-used keyboard shortcuts.
|
| 1052 |
+
// Auto-hides on small screens and after the first 12s, until the user hovers.
|
| 1053 |
+
function ShortcutHint() {
|
| 1054 |
+
const [visible, setVisible] = useState(true);
|
| 1055 |
+
useEffect(() => {
|
| 1056 |
+
const t = setTimeout(() => setVisible(false), 12000);
|
| 1057 |
+
return () => clearTimeout(t);
|
| 1058 |
+
}, []);
|
| 1059 |
+
return (
|
| 1060 |
+
<div
|
| 1061 |
+
className={`canvas-shortcut-hint ${visible ? 'visible' : ''}`}
|
| 1062 |
+
onMouseEnter={() => setVisible(true)}
|
| 1063 |
+
onMouseLeave={() => setVisible(false)}
|
| 1064 |
+
>
|
| 1065 |
+
<span><kbd>{MOD}</kbd><kbd>K</kbd> commands</span>
|
| 1066 |
+
<span><kbd>{MOD}</kbd><kbd>/</kbd> search</span>
|
| 1067 |
+
<span><kbd>?</kbd> help</span>
|
| 1068 |
+
</div>
|
| 1069 |
+
);
|
| 1070 |
+
}
|
| 1071 |
+
|
| 1072 |
+
export default CanvasPage;
|
phd-advisor-frontend/src/pages/ChatPage.js
CHANGED
|
@@ -1,28 +1,31 @@
|
|
| 1 |
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
| 2 |
-
|
| 3 |
-
import { Home, MessageCircle, Reply, X, Sparkles, Users, Settings2, FileText, Menu, HelpCircle } from 'lucide-react';
|
| 4 |
-
|
| 5 |
import EnhancedChatInput from '../components/EnhancedChatInput';
|
| 6 |
import MessageBubble from '../components/MessageBubble';
|
| 7 |
import ThinkingIndicator from '../components/ThinkingIndicator';
|
| 8 |
import SuggestionsPanel from '../components/SuggestionsPanel';
|
| 9 |
-
import
|
| 10 |
-
import
|
| 11 |
import ExportButton from '../components/ExportButton';
|
| 12 |
import Sidebar from '../components/Sidebar';
|
| 13 |
import { useAppConfig } from '../contexts/AppConfigContext';
|
| 14 |
import { useTheme } from '../contexts/ThemeContext';
|
| 15 |
import '../styles/ChatPage.css';
|
| 16 |
import '../styles/EnhancedChatInput.css';
|
| 17 |
-
import AdvisorStatusDropdown from '../components/AdvisorStatusDropdown';
|
| 18 |
import AdvisorCarousel from '../components/AdvisorCarousel';
|
| 19 |
-
import
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
const
|
|
|
|
|
|
|
| 22 |
const { config, advisors, getAdvisorColors } = useAppConfig();
|
| 23 |
const [messages, setMessages] = useState([]);
|
| 24 |
const [isLoading, setIsLoading] = useState(false);
|
| 25 |
const [thinkingAdvisors, setThinkingAdvisors] = useState([]);
|
|
|
|
| 26 |
const [collectedInfo, setCollectedInfo] = useState({});
|
| 27 |
const [replyingTo, setReplyingTo] = useState(null);
|
| 28 |
const [currentProvider, setCurrentProvider] = useState('gemini');
|
|
@@ -38,7 +41,97 @@ const ChatPage = ({ user, authToken, onNavigateToHome, onNavigateToCanvas, onSig
|
|
| 38 |
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
| 39 |
const [sidebarRefreshTrigger, setSidebarRefreshTrigger] = useState(0);
|
| 40 |
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
const scrollToBottom = () => {
|
| 44 |
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
@@ -215,19 +308,20 @@ const loadChatSession = async (sessionId) => {
|
|
| 215 |
}
|
| 216 |
};
|
| 217 |
|
| 218 |
-
// Save a message to the current session
|
| 219 |
-
const saveMessageToSession = async (message) => {
|
| 220 |
-
|
|
|
|
| 221 |
|
| 222 |
try {
|
| 223 |
-
await fetch(`${process.env.REACT_APP_API_URL}/api/chat-sessions/${
|
| 224 |
method: 'POST',
|
| 225 |
headers: {
|
| 226 |
'Authorization': `Bearer ${authToken}`,
|
| 227 |
'Content-Type': 'application/json'
|
| 228 |
},
|
| 229 |
body: JSON.stringify({
|
| 230 |
-
session_id:
|
| 231 |
message: {
|
| 232 |
...message,
|
| 233 |
timestamp: message.timestamp.toISOString()
|
|
@@ -395,6 +489,10 @@ const handleNewChat = async (sessionId = null) => {
|
|
| 395 |
}
|
| 396 |
}
|
| 397 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
// Update session title if this is the first message and title is generic
|
| 399 |
if (messages.length === 0 && currentSessionTitle.includes('Chat ')) {
|
| 400 |
const newTitle = inputMessage.length > 30
|
|
@@ -405,6 +503,14 @@ const handleNewChat = async (sessionId = null) => {
|
|
| 405 |
|
| 406 |
// Set loading state
|
| 407 |
setIsLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
setThinkingAdvisors(['system']);
|
| 409 |
|
| 410 |
try {
|
|
@@ -417,7 +523,8 @@ const handleNewChat = async (sessionId = null) => {
|
|
| 417 |
body: JSON.stringify({
|
| 418 |
user_input: inputMessage,
|
| 419 |
response_length: 'medium',
|
| 420 |
-
chat_session_id:
|
|
|
|
| 421 |
}),
|
| 422 |
});
|
| 423 |
|
|
@@ -457,7 +564,7 @@ const handleNewChat = async (sessionId = null) => {
|
|
| 457 |
};
|
| 458 |
setMessages(prev => [...prev, msg]);
|
| 459 |
setThinkingAdvisors(prev => prev.filter(a => a !== d.persona_id));
|
| 460 |
-
await saveMessageToSession(msg);
|
| 461 |
break;
|
| 462 |
}
|
| 463 |
case 'clarification':
|
|
@@ -470,6 +577,15 @@ const handleNewChat = async (sessionId = null) => {
|
|
| 470 |
}]);
|
| 471 |
break;
|
| 472 |
case 'progress':
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
if (d.phase === 'complete') {
|
| 474 |
break;
|
| 475 |
}
|
|
@@ -753,94 +869,60 @@ const handleNewChat = async (sessionId = null) => {
|
|
| 753 |
const chatPlaceholder = config?.chat_page?.placeholder || "Ask your advisors anything...";
|
| 754 |
|
| 755 |
return (
|
| 756 |
-
<OnboardingTour>
|
| 757 |
<div className="chat-page-with-sidebar">
|
| 758 |
{/* Sidebar Component */}
|
| 759 |
-
<Sidebar
|
| 760 |
user={user}
|
| 761 |
currentSessionId={currentSessionId}
|
| 762 |
onSelectSession={handleSelectSession}
|
| 763 |
onNewChat={handleNewChat}
|
| 764 |
onCurrentSessionDeleted={handleCurrentSessionDeleted}
|
| 765 |
onSignOut={onSignOut}
|
| 766 |
-
onUserUpdate={onUserUpdate}
|
| 767 |
authToken={authToken}
|
| 768 |
onSidebarToggle={handleSidebarToggle}
|
| 769 |
isMobileOpen={isMobileMenuOpen}
|
| 770 |
onMobileToggle={setIsMobileMenuOpen}
|
| 771 |
onNavigateToCanvas={onNavigateToCanvas}
|
| 772 |
refreshTrigger={sidebarRefreshTrigger}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 773 |
/>
|
| 774 |
|
| 775 |
<div className={`main-chat-area ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
|
| 776 |
<div className="modern-chat-page">
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
<div className="header-controls">
|
| 809 |
-
{/* Add session title display */}
|
| 810 |
-
{currentSessionTitle && (
|
| 811 |
-
<div className="session-title-display">
|
| 812 |
-
<span>{currentSessionTitle}</span>
|
| 813 |
-
</div>
|
| 814 |
-
)}
|
| 815 |
-
|
| 816 |
-
{/* Export Button */}
|
| 817 |
-
<ExportButton
|
| 818 |
-
hasMessages={hasConversationMessages}
|
| 819 |
-
currentSessionId={currentSessionId}
|
| 820 |
-
authToken={authToken}
|
| 821 |
-
/>
|
| 822 |
-
|
| 823 |
-
{/* Provider Dropdown */}
|
| 824 |
-
<ProviderDropdown
|
| 825 |
-
currentProvider={currentProvider}
|
| 826 |
-
onProviderChange={handleProviderSwitch}
|
| 827 |
-
isLoading={isProviderSwitching}
|
| 828 |
-
/>
|
| 829 |
-
|
| 830 |
-
{/* Theme Toggle */}
|
| 831 |
-
<ThemeToggle />
|
| 832 |
-
|
| 833 |
-
{/* Help / User Guide */}
|
| 834 |
-
<button
|
| 835 |
-
className="header-help-btn"
|
| 836 |
-
onClick={() => window.dispatchEvent(new CustomEvent('open-user-guide'))}
|
| 837 |
-
title="Open user guide"
|
| 838 |
-
>
|
| 839 |
-
<HelpCircle />
|
| 840 |
-
</button>
|
| 841 |
-
</div>
|
| 842 |
-
</div>
|
| 843 |
-
</div>
|
| 844 |
|
| 845 |
{/* Main Content */}
|
| 846 |
<div className="chat-content">
|
|
@@ -1000,7 +1082,6 @@ const handleNewChat = async (sessionId = null) => {
|
|
| 1000 |
</div>
|
| 1001 |
</div>
|
| 1002 |
</div>
|
| 1003 |
-
</OnboardingTour>
|
| 1004 |
);
|
| 1005 |
};
|
| 1006 |
|
|
|
|
| 1 |
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
| 2 |
+
import { MessageCircle, Reply, X, Sparkles, Users, Settings2, FileText, HelpCircle } from 'lucide-react';
|
|
|
|
|
|
|
| 3 |
import EnhancedChatInput from '../components/EnhancedChatInput';
|
| 4 |
import MessageBubble from '../components/MessageBubble';
|
| 5 |
import ThinkingIndicator from '../components/ThinkingIndicator';
|
| 6 |
import SuggestionsPanel from '../components/SuggestionsPanel';
|
| 7 |
+
import AppHeader from '../components/AppHeader';
|
| 8 |
+
import AdvisorStatusDropdown from '../components/AdvisorStatusDropdown';
|
| 9 |
import ExportButton from '../components/ExportButton';
|
| 10 |
import Sidebar from '../components/Sidebar';
|
| 11 |
import { useAppConfig } from '../contexts/AppConfigContext';
|
| 12 |
import { useTheme } from '../contexts/ThemeContext';
|
| 13 |
import '../styles/ChatPage.css';
|
| 14 |
import '../styles/EnhancedChatInput.css';
|
|
|
|
| 15 |
import AdvisorCarousel from '../components/AdvisorCarousel';
|
| 16 |
+
import OnboardingChat from '../components/OnboardingChat';
|
| 17 |
+
import ProfileWalkthrough from '../components/ProfileWalkthrough';
|
| 18 |
+
import ClearDataModal from '../components/ClearDataModal';
|
| 19 |
+
import AccountModal from '../components/AccountModal';
|
| 20 |
|
| 21 |
+
const ACTIVE_ADVISORS_STORAGE_KEY = 'cybersecurityActiveAdvisorIds';
|
| 22 |
+
|
| 23 |
+
const ChatPage = ({ user, authToken, onNavigateToHome, onNavigateToCanvas, onSignOut }) => {
|
| 24 |
const { config, advisors, getAdvisorColors } = useAppConfig();
|
| 25 |
const [messages, setMessages] = useState([]);
|
| 26 |
const [isLoading, setIsLoading] = useState(false);
|
| 27 |
const [thinkingAdvisors, setThinkingAdvisors] = useState([]);
|
| 28 |
+
const [activeAdvisorIds, setActiveAdvisorIds] = useState([]);
|
| 29 |
const [collectedInfo, setCollectedInfo] = useState({});
|
| 30 |
const [replyingTo, setReplyingTo] = useState(null);
|
| 31 |
const [currentProvider, setCurrentProvider] = useState('gemini');
|
|
|
|
| 41 |
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
| 42 |
const [sidebarRefreshTrigger, setSidebarRefreshTrigger] = useState(0);
|
| 43 |
|
| 44 |
+
const [userAvatarId, setUserAvatarId] = useState(
|
| 45 |
+
() => localStorage.getItem('userAvatarId') || (user?.avatarId ?? null)
|
| 46 |
+
);
|
| 47 |
+
const avatarOptions = config?.app?.user_avatars || [];
|
| 48 |
+
|
| 49 |
+
const handleAvatarChange = async (id) => {
|
| 50 |
+
setUserAvatarId(id);
|
| 51 |
+
localStorage.setItem('userAvatarId', id);
|
| 52 |
+
try {
|
| 53 |
+
await fetch(`${process.env.REACT_APP_API_URL}/auth/me`, {
|
| 54 |
+
method: 'PATCH',
|
| 55 |
+
headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json' },
|
| 56 |
+
body: JSON.stringify({ avatarId: id }),
|
| 57 |
+
});
|
| 58 |
+
} catch (e) {
|
| 59 |
+
console.error('Failed to save avatar:', e);
|
| 60 |
+
}
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
const [showOnboarding, setShowOnboarding] = useState(false);
|
| 64 |
+
const [showProfileForm, setShowProfileForm] = useState(false);
|
| 65 |
+
const [showClearData, setShowClearData] = useState(false);
|
| 66 |
+
const [showAccount, setShowAccount] = useState(false);
|
| 67 |
+
const [userProfile, setUserProfile] = useState(null);
|
| 68 |
+
|
| 69 |
+
const loadProfile = async () => {
|
| 70 |
+
try {
|
| 71 |
+
const resp = await fetch(`${process.env.REACT_APP_API_URL}/api/users/me/profile`, {
|
| 72 |
+
headers: { Authorization: `Bearer ${authToken}` },
|
| 73 |
+
});
|
| 74 |
+
if (resp.ok) setUserProfile(await resp.json());
|
| 75 |
+
} catch (e) {
|
| 76 |
+
/* ignore */
|
| 77 |
+
}
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
useEffect(() => {
|
| 81 |
+
if (authToken) loadProfile();
|
| 82 |
+
}, [authToken]);
|
| 83 |
+
|
| 84 |
+
useEffect(() => {
|
| 85 |
+
const allIds = Object.keys(advisors || {});
|
| 86 |
+
if (allIds.length === 0) return;
|
| 87 |
+
|
| 88 |
+
setActiveAdvisorIds((prev) => {
|
| 89 |
+
let next = prev.filter((id) => allIds.includes(id));
|
| 90 |
+
if (next.length === 0) {
|
| 91 |
+
try {
|
| 92 |
+
const stored = JSON.parse(localStorage.getItem(ACTIVE_ADVISORS_STORAGE_KEY) || 'null');
|
| 93 |
+
if (Array.isArray(stored)) {
|
| 94 |
+
const valid = stored.filter((id) => allIds.includes(id));
|
| 95 |
+
if (valid.length > 0) next = valid;
|
| 96 |
+
}
|
| 97 |
+
} catch {
|
| 98 |
+
/* ignore */
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
if (next.length === 0) next = [...allIds];
|
| 102 |
+
if (
|
| 103 |
+
prev.length === next.length &&
|
| 104 |
+
prev.every((id, index) => id === next[index])
|
| 105 |
+
) {
|
| 106 |
+
return prev;
|
| 107 |
+
}
|
| 108 |
+
return next;
|
| 109 |
+
});
|
| 110 |
+
}, [advisors]);
|
| 111 |
+
|
| 112 |
+
const persistActiveAdvisorIds = (ids) => {
|
| 113 |
+
localStorage.setItem(ACTIVE_ADVISORS_STORAGE_KEY, JSON.stringify(ids));
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
const handleSetActiveAdvisors = (ids) => {
|
| 117 |
+
setActiveAdvisorIds(ids);
|
| 118 |
+
persistActiveAdvisorIds(ids);
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
const handleToggleAdvisor = (id) => {
|
| 122 |
+
setActiveAdvisorIds((prev) => {
|
| 123 |
+
const set = new Set(prev);
|
| 124 |
+
if (set.has(id)) {
|
| 125 |
+
if (set.size <= 1) return prev;
|
| 126 |
+
set.delete(id);
|
| 127 |
+
} else {
|
| 128 |
+
set.add(id);
|
| 129 |
+
}
|
| 130 |
+
const next = Array.from(set);
|
| 131 |
+
persistActiveAdvisorIds(next);
|
| 132 |
+
return next;
|
| 133 |
+
});
|
| 134 |
+
};
|
| 135 |
|
| 136 |
const scrollToBottom = () => {
|
| 137 |
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
| 308 |
}
|
| 309 |
};
|
| 310 |
|
| 311 |
+
// Save a message to the current session (optional sessionId when state is not updated yet)
|
| 312 |
+
const saveMessageToSession = async (message, sessionIdOverride) => {
|
| 313 |
+
const sid = sessionIdOverride || currentSessionId;
|
| 314 |
+
if (!sid || !authToken) return;
|
| 315 |
|
| 316 |
try {
|
| 317 |
+
await fetch(`${process.env.REACT_APP_API_URL}/api/chat-sessions/${sid}/messages`, {
|
| 318 |
method: 'POST',
|
| 319 |
headers: {
|
| 320 |
'Authorization': `Bearer ${authToken}`,
|
| 321 |
'Content-Type': 'application/json'
|
| 322 |
},
|
| 323 |
body: JSON.stringify({
|
| 324 |
+
session_id: sid,
|
| 325 |
message: {
|
| 326 |
...message,
|
| 327 |
timestamp: message.timestamp.toISOString()
|
|
|
|
| 489 |
}
|
| 490 |
}
|
| 491 |
|
| 492 |
+
saveMessageToSession(userMessage, sessionId).catch(err =>
|
| 493 |
+
console.error('Failed to persist user message:', err)
|
| 494 |
+
);
|
| 495 |
+
|
| 496 |
// Update session title if this is the first message and title is generic
|
| 497 |
if (messages.length === 0 && currentSessionTitle.includes('Chat ')) {
|
| 498 |
const newTitle = inputMessage.length > 30
|
|
|
|
| 503 |
|
| 504 |
// Set loading state
|
| 505 |
setIsLoading(true);
|
| 506 |
+
const advisorsForRequest = activeAdvisorIds.length > 0
|
| 507 |
+
? activeAdvisorIds
|
| 508 |
+
: Object.keys(advisors || {});
|
| 509 |
+
// Start with just the orchestrator's thinking bubble. The backend will
|
| 510 |
+
// emit a `progress { phase: 'selected', selected_advisors: [...] }` event
|
| 511 |
+
// naming the 3 advisors it picked, and that handler will add their
|
| 512 |
+
// ThinkingIndicators. This prevents the brief flash of thinking indicators
|
| 513 |
+
// for every advisor in the active pool before ranking has run.
|
| 514 |
setThinkingAdvisors(['system']);
|
| 515 |
|
| 516 |
try {
|
|
|
|
| 523 |
body: JSON.stringify({
|
| 524 |
user_input: inputMessage,
|
| 525 |
response_length: 'medium',
|
| 526 |
+
chat_session_id: sessionId,
|
| 527 |
+
active_advisors: advisorsForRequest,
|
| 528 |
}),
|
| 529 |
});
|
| 530 |
|
|
|
|
| 564 |
};
|
| 565 |
setMessages(prev => [...prev, msg]);
|
| 566 |
setThinkingAdvisors(prev => prev.filter(a => a !== d.persona_id));
|
| 567 |
+
await saveMessageToSession(msg, sessionId);
|
| 568 |
break;
|
| 569 |
}
|
| 570 |
case 'clarification':
|
|
|
|
| 577 |
}]);
|
| 578 |
break;
|
| 579 |
case 'progress':
|
| 580 |
+
if (d.phase === 'selected' && Array.isArray(d.selected_advisors)) {
|
| 581 |
+
setThinkingAdvisors(prev => {
|
| 582 |
+
const next = new Set(prev);
|
| 583 |
+
next.add('system');
|
| 584 |
+
d.selected_advisors.forEach(id => next.add(id));
|
| 585 |
+
return Array.from(next);
|
| 586 |
+
});
|
| 587 |
+
break;
|
| 588 |
+
}
|
| 589 |
if (d.phase === 'complete') {
|
| 590 |
break;
|
| 591 |
}
|
|
|
|
| 869 |
const chatPlaceholder = config?.chat_page?.placeholder || "Ask your advisors anything...";
|
| 870 |
|
| 871 |
return (
|
|
|
|
| 872 |
<div className="chat-page-with-sidebar">
|
| 873 |
{/* Sidebar Component */}
|
| 874 |
+
<Sidebar
|
| 875 |
user={user}
|
| 876 |
currentSessionId={currentSessionId}
|
| 877 |
onSelectSession={handleSelectSession}
|
| 878 |
onNewChat={handleNewChat}
|
| 879 |
onCurrentSessionDeleted={handleCurrentSessionDeleted}
|
| 880 |
onSignOut={onSignOut}
|
|
|
|
| 881 |
authToken={authToken}
|
| 882 |
onSidebarToggle={handleSidebarToggle}
|
| 883 |
isMobileOpen={isMobileMenuOpen}
|
| 884 |
onMobileToggle={setIsMobileMenuOpen}
|
| 885 |
onNavigateToCanvas={onNavigateToCanvas}
|
| 886 |
refreshTrigger={sidebarRefreshTrigger}
|
| 887 |
+
userAvatarId={userAvatarId}
|
| 888 |
+
onAvatarChange={handleAvatarChange}
|
| 889 |
+
onOpenProfile={() => setShowProfileForm(true)}
|
| 890 |
+
onOpenAccount={() => setShowAccount(true)}
|
| 891 |
+
onOpenClearData={() => setShowClearData(true)}
|
| 892 |
/>
|
| 893 |
|
| 894 |
<div className={`main-chat-area ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
|
| 895 |
<div className="modern-chat-page">
|
| 896 |
+
<AppHeader
|
| 897 |
+
currentPage="chat"
|
| 898 |
+
onNavigateToHome={onNavigateToHome}
|
| 899 |
+
onNavigateToChat={() => {}}
|
| 900 |
+
onNavigateToCanvas={onNavigateToCanvas}
|
| 901 |
+
onMobileMenu={handleMobileMenuToggle}
|
| 902 |
+
>
|
| 903 |
+
<AdvisorStatusDropdown
|
| 904 |
+
advisors={advisors}
|
| 905 |
+
activeAdvisorIds={activeAdvisorIds}
|
| 906 |
+
onToggleAdvisor={handleToggleAdvisor}
|
| 907 |
+
onSetActiveAdvisors={handleSetActiveAdvisors}
|
| 908 |
+
thinkingAdvisors={thinkingAdvisors}
|
| 909 |
+
getAdvisorColors={getAdvisorColors}
|
| 910 |
+
isDark={isDark}
|
| 911 |
+
/>
|
| 912 |
+
<ExportButton
|
| 913 |
+
hasMessages={hasConversationMessages}
|
| 914 |
+
currentSessionId={currentSessionId}
|
| 915 |
+
authToken={authToken}
|
| 916 |
+
/>
|
| 917 |
+
{/* User guide button (from main) — slotted into AppHeader's children */}
|
| 918 |
+
<button
|
| 919 |
+
className="icon-btn header-help-btn"
|
| 920 |
+
onClick={() => window.dispatchEvent(new CustomEvent('open-user-guide'))}
|
| 921 |
+
title="Open user guide"
|
| 922 |
+
>
|
| 923 |
+
<HelpCircle size={18} />
|
| 924 |
+
</button>
|
| 925 |
+
</AppHeader>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
|
| 927 |
{/* Main Content */}
|
| 928 |
<div className="chat-content">
|
|
|
|
| 1082 |
</div>
|
| 1083 |
</div>
|
| 1084 |
</div>
|
|
|
|
| 1085 |
);
|
| 1086 |
};
|
| 1087 |
|
phd-advisor-frontend/src/pages/HomePage.js
CHANGED
|
@@ -1,34 +1,21 @@
|
|
| 1 |
import React from 'react';
|
| 2 |
import { MessageCircle, ArrowRight } from 'lucide-react';
|
| 3 |
import AdvisorCard from '../components/AdvisorCard';
|
| 4 |
-
import
|
| 5 |
import CopyrightNotice from '../components/CopyrightNotice';
|
| 6 |
import { useAppConfig } from '../contexts/AppConfigContext';
|
| 7 |
|
| 8 |
-
const HomePage = ({ onNavigateToChat, isAuthenticated }) => {
|
| 9 |
const { config, advisors, resolveIcon } = useAppConfig();
|
| 10 |
|
| 11 |
-
const UsersIcon = resolveIcon('Users');
|
| 12 |
-
|
| 13 |
return (
|
| 14 |
<div className="homepage">
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
</div>
|
| 22 |
-
<div>
|
| 23 |
-
<h1 className="logo-title">{config.app.title}</h1>
|
| 24 |
-
<p className="logo-subtitle">{config.app.subtitle}</p>
|
| 25 |
-
</div>
|
| 26 |
-
</div>
|
| 27 |
-
<div className="header-right">
|
| 28 |
-
<ThemeToggle />
|
| 29 |
-
</div>
|
| 30 |
-
</div>
|
| 31 |
-
</header>
|
| 32 |
|
| 33 |
{/* Hero Section */}
|
| 34 |
<main className="main">
|
|
|
|
| 1 |
import React from 'react';
|
| 2 |
import { MessageCircle, ArrowRight } from 'lucide-react';
|
| 3 |
import AdvisorCard from '../components/AdvisorCard';
|
| 4 |
+
import AppHeader from '../components/AppHeader';
|
| 5 |
import CopyrightNotice from '../components/CopyrightNotice';
|
| 6 |
import { useAppConfig } from '../contexts/AppConfigContext';
|
| 7 |
|
| 8 |
+
const HomePage = ({ onNavigateToChat, isAuthenticated, onNavigateToHome, onNavigateToCanvas }) => {
|
| 9 |
const { config, advisors, resolveIcon } = useAppConfig();
|
| 10 |
|
|
|
|
|
|
|
| 11 |
return (
|
| 12 |
<div className="homepage">
|
| 13 |
+
<AppHeader
|
| 14 |
+
currentPage="home"
|
| 15 |
+
onNavigateToHome={onNavigateToHome}
|
| 16 |
+
onNavigateToChat={onNavigateToChat}
|
| 17 |
+
onNavigateToCanvas={onNavigateToCanvas}
|
| 18 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
{/* Hero Section */}
|
| 21 |
<main className="main">
|
phd-advisor-frontend/src/styles/CanvasPage.css
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
phd-advisor-frontend/src/styles/ChatPage.css
CHANGED
|
@@ -18,23 +18,18 @@
|
|
| 18 |
position: sticky;
|
| 19 |
top: 0;
|
| 20 |
z-index: 100;
|
| 21 |
-
background:
|
| 22 |
backdrop-filter: blur(20px);
|
| 23 |
-webkit-backdrop-filter: blur(20px);
|
| 24 |
-
border-bottom: 1px solid var(--
|
| 25 |
padding: 12px 24px;
|
| 26 |
display: flex;
|
| 27 |
align-items: center;
|
| 28 |
justify-content: space-between;
|
| 29 |
-
box-shadow:
|
| 30 |
flex-shrink: 0;
|
| 31 |
}
|
| 32 |
|
| 33 |
-
[data-theme="dark"] .floating-header {
|
| 34 |
-
background: rgba(31, 41, 55, 0.95);
|
| 35 |
-
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
.header-left {
|
| 39 |
display: flex;
|
| 40 |
align-items: center;
|
|
@@ -79,19 +74,25 @@
|
|
| 79 |
color: white;
|
| 80 |
}
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
.brand-text h1 {
|
| 83 |
font-size: 20px;
|
| 84 |
font-weight: 700;
|
| 85 |
color: var(--text-primary);
|
| 86 |
margin: 0;
|
| 87 |
-
line-height: 1;
|
| 88 |
}
|
| 89 |
|
| 90 |
.brand-text p {
|
| 91 |
font-size: 13px;
|
| 92 |
color: var(--text-secondary);
|
| 93 |
margin: 0;
|
| 94 |
-
line-height: 1;
|
| 95 |
}
|
| 96 |
|
| 97 |
.header-right {
|
|
@@ -678,34 +679,6 @@
|
|
| 678 |
margin-left: 70px; /* Width of sidebar when collapsed */
|
| 679 |
}
|
| 680 |
|
| 681 |
-
/* Session title display in header */
|
| 682 |
-
.session-title-display {
|
| 683 |
-
display: flex;
|
| 684 |
-
align-items: center;
|
| 685 |
-
padding: 6px 12px;
|
| 686 |
-
background: var(--bg-secondary, #f3f4f6);
|
| 687 |
-
border-radius: 6px;
|
| 688 |
-
margin-right: 8px;
|
| 689 |
-
}
|
| 690 |
-
|
| 691 |
-
.dark .session-title-display {
|
| 692 |
-
background: var(--bg-secondary-dark, #374151);
|
| 693 |
-
}
|
| 694 |
-
|
| 695 |
-
.session-title-display span {
|
| 696 |
-
font-size: 13px;
|
| 697 |
-
font-weight: 500;
|
| 698 |
-
color: var(--text-primary, #111827);
|
| 699 |
-
max-width: 150px;
|
| 700 |
-
white-space: nowrap;
|
| 701 |
-
overflow: hidden;
|
| 702 |
-
text-overflow: ellipsis;
|
| 703 |
-
}
|
| 704 |
-
|
| 705 |
-
.dark .session-title-display span {
|
| 706 |
-
color: var(--text-primary-dark, #f9fafb);
|
| 707 |
-
}
|
| 708 |
-
|
| 709 |
/* Optional header sign out button */
|
| 710 |
.header-signout-btn {
|
| 711 |
display: flex;
|
|
|
|
| 18 |
position: sticky;
|
| 19 |
top: 0;
|
| 20 |
z-index: 100;
|
| 21 |
+
background: var(--header-bg);
|
| 22 |
backdrop-filter: blur(20px);
|
| 23 |
-webkit-backdrop-filter: blur(20px);
|
| 24 |
+
border-bottom: 1px solid var(--header-border);
|
| 25 |
padding: 12px 24px;
|
| 26 |
display: flex;
|
| 27 |
align-items: center;
|
| 28 |
justify-content: space-between;
|
| 29 |
+
box-shadow: var(--header-shadow);
|
| 30 |
flex-shrink: 0;
|
| 31 |
}
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
.header-left {
|
| 34 |
display: flex;
|
| 35 |
align-items: center;
|
|
|
|
| 74 |
color: white;
|
| 75 |
}
|
| 76 |
|
| 77 |
+
.brand-text {
|
| 78 |
+
display: flex;
|
| 79 |
+
flex-direction: column;
|
| 80 |
+
gap: 6px;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
.brand-text h1 {
|
| 84 |
font-size: 20px;
|
| 85 |
font-weight: 700;
|
| 86 |
color: var(--text-primary);
|
| 87 |
margin: 0;
|
| 88 |
+
line-height: 1.2;
|
| 89 |
}
|
| 90 |
|
| 91 |
.brand-text p {
|
| 92 |
font-size: 13px;
|
| 93 |
color: var(--text-secondary);
|
| 94 |
margin: 0;
|
| 95 |
+
line-height: 1.35;
|
| 96 |
}
|
| 97 |
|
| 98 |
.header-right {
|
|
|
|
| 679 |
margin-left: 70px; /* Width of sidebar when collapsed */
|
| 680 |
}
|
| 681 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 682 |
/* Optional header sign out button */
|
| 683 |
.header-signout-btn {
|
| 684 |
display: flex;
|
phd-advisor-frontend/src/styles/Sidebar.css
CHANGED
|
@@ -272,21 +272,13 @@
|
|
| 272 |
background: #7f1d1d;
|
| 273 |
}
|
| 274 |
|
| 275 |
-
/* New Chat Row (button + clear-all icon) */
|
| 276 |
-
.new-chat-row {
|
| 277 |
-
display: flex;
|
| 278 |
-
align-items: stretch;
|
| 279 |
-
gap: 8px;
|
| 280 |
-
width: 100%;
|
| 281 |
-
}
|
| 282 |
-
|
| 283 |
/* New Chat Button */
|
| 284 |
.new-chat-button {
|
| 285 |
display: flex;
|
| 286 |
align-items: center;
|
| 287 |
justify-content: center;
|
| 288 |
gap: 8px;
|
| 289 |
-
|
| 290 |
padding: 12px 16px;
|
| 291 |
background: var(--primary-color, #3b82f6);
|
| 292 |
color: white;
|
|
@@ -297,47 +289,6 @@
|
|
| 297 |
transition: all 0.2s ease;
|
| 298 |
}
|
| 299 |
|
| 300 |
-
/* Clear All Chats Button — matches .new-chat-icon-btn dimensions */
|
| 301 |
-
.clear-all-chats-btn {
|
| 302 |
-
display: flex;
|
| 303 |
-
align-items: center;
|
| 304 |
-
justify-content: center;
|
| 305 |
-
width: 36px;
|
| 306 |
-
height: 36px;
|
| 307 |
-
flex-shrink: 0;
|
| 308 |
-
padding: 0;
|
| 309 |
-
background: transparent;
|
| 310 |
-
color: #ef4444;
|
| 311 |
-
border: 1px solid rgba(239, 68, 68, 0.35);
|
| 312 |
-
border-radius: 8px;
|
| 313 |
-
cursor: pointer;
|
| 314 |
-
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, transform 0.12s ease;
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
.clear-all-chats-btn:hover:not(:disabled) {
|
| 318 |
-
background: #ef4444;
|
| 319 |
-
color: #ffffff;
|
| 320 |
-
border-color: #ef4444;
|
| 321 |
-
transform: translateY(-1px);
|
| 322 |
-
}
|
| 323 |
-
|
| 324 |
-
.clear-all-chats-btn:disabled {
|
| 325 |
-
opacity: 0.4;
|
| 326 |
-
cursor: not-allowed;
|
| 327 |
-
transform: none;
|
| 328 |
-
}
|
| 329 |
-
|
| 330 |
-
[data-theme="dark"] .clear-all-chats-btn {
|
| 331 |
-
color: #f87171;
|
| 332 |
-
border-color: rgba(248, 113, 113, 0.4);
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
[data-theme="dark"] .clear-all-chats-btn:hover:not(:disabled) {
|
| 336 |
-
background: #ef4444;
|
| 337 |
-
color: #ffffff;
|
| 338 |
-
border-color: #ef4444;
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
.new-chat-button:hover {
|
| 342 |
background: var(--primary-color-hover, #2563eb);
|
| 343 |
transform: translateY(-1px);
|
|
@@ -902,24 +853,4 @@
|
|
| 902 |
.mobile-sidebar-overlay.visible {
|
| 903 |
display: block;
|
| 904 |
}
|
| 905 |
-
}
|
| 906 |
-
|
| 907 |
-
.sidebar-version {
|
| 908 |
-
padding: 8px 16px;
|
| 909 |
-
border-top: 1px solid var(--border-light, #e5e7eb);
|
| 910 |
-
font-size: 11px;
|
| 911 |
-
color: var(--text-secondary, #6b7280);
|
| 912 |
-
text-align: center;
|
| 913 |
-
user-select: none;
|
| 914 |
-
flex-shrink: 0;
|
| 915 |
-
}
|
| 916 |
-
|
| 917 |
-
.sidebar.collapsed .sidebar-version {
|
| 918 |
-
padding: 8px 4px;
|
| 919 |
-
font-size: 10px;
|
| 920 |
-
}
|
| 921 |
-
|
| 922 |
-
.dark .sidebar-version {
|
| 923 |
-
border-top-color: var(--border-dark, #374151);
|
| 924 |
-
color: var(--text-secondary-dark, #9ca3af);
|
| 925 |
}
|
|
|
|
| 272 |
background: #7f1d1d;
|
| 273 |
}
|
| 274 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
/* New Chat Button */
|
| 276 |
.new-chat-button {
|
| 277 |
display: flex;
|
| 278 |
align-items: center;
|
| 279 |
justify-content: center;
|
| 280 |
gap: 8px;
|
| 281 |
+
width: 100%;
|
| 282 |
padding: 12px 16px;
|
| 283 |
background: var(--primary-color, #3b82f6);
|
| 284 |
color: white;
|
|
|
|
| 289 |
transition: all 0.2s ease;
|
| 290 |
}
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
.new-chat-button:hover {
|
| 293 |
background: var(--primary-color-hover, #2563eb);
|
| 294 |
transform: translateY(-1px);
|
|
|
|
| 853 |
.mobile-sidebar-overlay.visible {
|
| 854 |
display: block;
|
| 855 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 856 |
}
|
phd-advisor-frontend/src/styles/components.css
CHANGED
|
@@ -42,6 +42,11 @@
|
|
| 42 |
--input-border: #D1D5DB;
|
| 43 |
--input-focus: #6366F1;
|
| 44 |
--input-focus-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
:root[data-theme="dark"] {
|
|
@@ -87,6 +92,11 @@
|
|
| 87 |
--input-border: #4B5563;
|
| 88 |
--input-focus: #818CF8;
|
| 89 |
--input-focus-shadow: 0 0 0 3px rgba(129, 140, 248, 0.2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
/* Theme Toggle Button */
|
|
@@ -527,13 +537,15 @@
|
|
| 527 |
.advisor-message-header {
|
| 528 |
display: flex;
|
| 529 |
align-items: center;
|
| 530 |
-
gap:
|
| 531 |
margin-bottom: 8px;
|
| 532 |
}
|
| 533 |
|
| 534 |
.advisor-message-name {
|
| 535 |
-
font-weight:
|
| 536 |
-
font-size:
|
|
|
|
|
|
|
| 537 |
}
|
| 538 |
|
| 539 |
.reply-badge {
|
|
@@ -550,9 +562,44 @@
|
|
| 550 |
}
|
| 551 |
|
| 552 |
.advisor-message-text {
|
|
|
|
| 553 |
line-height: 1.6;
|
| 554 |
}
|
| 555 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
.advisor-message-text > div:last-child p:last-child {
|
| 557 |
margin-bottom: 0; /* Remove bottom margin from last paragraph */
|
| 558 |
}
|
|
@@ -765,8 +812,9 @@
|
|
| 765 |
}
|
| 766 |
|
| 767 |
.advisor-name {
|
| 768 |
-
font-weight:
|
| 769 |
-
font-size:
|
|
|
|
| 770 |
margin: 0;
|
| 771 |
}
|
| 772 |
|
|
@@ -1184,7 +1232,11 @@
|
|
| 1184 |
}
|
| 1185 |
|
| 1186 |
.advisor-message-text {
|
| 1187 |
-
font-size:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1188 |
}
|
| 1189 |
|
| 1190 |
/* Basic ChatInput mobile styles */
|
|
|
|
| 42 |
--input-border: #D1D5DB;
|
| 43 |
--input-focus: #6366F1;
|
| 44 |
--input-focus-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
| 45 |
+
|
| 46 |
+
/* App header (floating bar) */
|
| 47 |
+
--header-bg: #EBF5FF;
|
| 48 |
+
--header-border: rgba(59, 130, 246, 0.2);
|
| 49 |
+
--header-shadow: 0 4px 20px rgba(37, 99, 235, 0.1);
|
| 50 |
}
|
| 51 |
|
| 52 |
:root[data-theme="dark"] {
|
|
|
|
| 92 |
--input-border: #4B5563;
|
| 93 |
--input-focus: #818CF8;
|
| 94 |
--input-focus-shadow: 0 0 0 3px rgba(129, 140, 248, 0.2);
|
| 95 |
+
|
| 96 |
+
/* App header (floating bar) */
|
| 97 |
+
--header-bg: rgba(23, 37, 64, 0.97);
|
| 98 |
+
--header-border: rgba(96, 165, 250, 0.18);
|
| 99 |
+
--header-shadow: 0 4px 20px rgba(0, 0, 0, 0.35);
|
| 100 |
}
|
| 101 |
|
| 102 |
/* Theme Toggle Button */
|
|
|
|
| 537 |
.advisor-message-header {
|
| 538 |
display: flex;
|
| 539 |
align-items: center;
|
| 540 |
+
gap: 10px;
|
| 541 |
margin-bottom: 8px;
|
| 542 |
}
|
| 543 |
|
| 544 |
.advisor-message-name {
|
| 545 |
+
font-weight: 600;
|
| 546 |
+
font-size: 1rem;
|
| 547 |
+
line-height: 1.6;
|
| 548 |
+
margin: 0;
|
| 549 |
}
|
| 550 |
|
| 551 |
.reply-badge {
|
|
|
|
| 562 |
}
|
| 563 |
|
| 564 |
.advisor-message-text {
|
| 565 |
+
font-size: 1rem;
|
| 566 |
line-height: 1.6;
|
| 567 |
}
|
| 568 |
|
| 569 |
+
/* Advisor icon in message header: circular badge, theme-aware fill */
|
| 570 |
+
.advisor-message-avatar-ring {
|
| 571 |
+
border-radius: 50%;
|
| 572 |
+
display: flex;
|
| 573 |
+
align-items: center;
|
| 574 |
+
justify-content: center;
|
| 575 |
+
flex-shrink: 0;
|
| 576 |
+
overflow: hidden;
|
| 577 |
+
background: #ffffff;
|
| 578 |
+
border: 1px solid rgba(15, 23, 42, 0.1);
|
| 579 |
+
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
[data-theme="dark"] .advisor-message-avatar-ring {
|
| 583 |
+
background: #1e293b;
|
| 584 |
+
border-color: rgba(248, 250, 252, 0.12);
|
| 585 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
.advisor-message-avatar-ring img {
|
| 589 |
+
width: 100%;
|
| 590 |
+
height: 100%;
|
| 591 |
+
object-fit: cover;
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
.advisor-message-avatar-icon {
|
| 595 |
+
flex-shrink: 0;
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
.advisor-message-avatar-initial {
|
| 599 |
+
font-weight: 700;
|
| 600 |
+
line-height: 1;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
.advisor-message-text > div:last-child p:last-child {
|
| 604 |
margin-bottom: 0; /* Remove bottom margin from last paragraph */
|
| 605 |
}
|
|
|
|
| 812 |
}
|
| 813 |
|
| 814 |
.advisor-name {
|
| 815 |
+
font-weight: 600;
|
| 816 |
+
font-size: 1rem;
|
| 817 |
+
line-height: 1.6;
|
| 818 |
margin: 0;
|
| 819 |
}
|
| 820 |
|
|
|
|
| 1232 |
}
|
| 1233 |
|
| 1234 |
.advisor-message-text {
|
| 1235 |
+
font-size: 1rem;
|
| 1236 |
+
}
|
| 1237 |
+
|
| 1238 |
+
.advisor-message-name {
|
| 1239 |
+
font-size: 1rem;
|
| 1240 |
}
|
| 1241 |
|
| 1242 |
/* Basic ChatInput mobile styles */
|