diff --git a/apps/interactive-journal/backend/app/__init__.py b/apps/interactive-journal/backend/app/__init__.py deleted file mode 100644 index 3db73c2..0000000 --- a/apps/interactive-journal/backend/app/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Memoir - Interactive Journaling App diff --git a/apps/interactive-journal/backend/app/routers/routes.py b/apps/interactive-journal/backend/app/routers/routes.py deleted file mode 100644 index 9aaa427..0000000 --- a/apps/interactive-journal/backend/app/routers/routes.py +++ /dev/null @@ -1,249 +0,0 @@ -import logging -from datetime import datetime, timedelta -from typing import Optional - -from bson import ObjectId -from fastapi import APIRouter, File, Form, UploadFile -from fastapi.responses import StreamingResponse - -from app.config import USER_ID, VECTOR_INDEX_NAME, VECTOR_NUM_CANDIDATES -from app.routers.helpers import ( - extract_and_save_memories, - get_conversation_history, - get_longest_streak, - get_mood_distribution, - get_themes, - get_total_entries, - image_to_base64, - retrieve_relevant_memories, - save_assistant_message, - save_image_file, - save_user_message, -) -from app.services.anthropic import ( - analyze_entry, - generate_journal_prompt, - generate_response, -) -from app.services.mongodb import get_database -from app.services.voyage import get_multimodal_embedding, get_text_embedding - -logger = logging.getLogger(__name__) - -router = APIRouter() - - -@router.post("/") -def create_entry(version: int = Form(1), entry_date: str = Form(...)): - db = get_database() - entry_dt = datetime.fromisoformat(entry_date) - entry_data = { - "user_id": USER_ID, - "title": entry_dt.strftime("%d/%m/%Y"), - "version": version, - "created_at": entry_dt, - } - result = db.entries.insert_one(entry_data) - logger.info(f"Created entry {result.inserted_id} for user {USER_ID}") - return {"_id": str(result.inserted_id)} - - -@router.post("/{entry_id}/messages") -def send_message( - entry_id: str, - content: Optional[str] = Form(None), - images: list[UploadFile] = File([]), - version: int = Form(1), - entry_date: Optional[str] = Form(None), -): - db = get_database() - is_v2 = version == 2 - msg_date = datetime.fromisoformat(entry_date) - - # Save image files to disk before streaming (file handles close after) - image_paths = [save_image_file(image) for image in images] - - # Build current message (text, images, or both) - messages = [] - if content: - messages.append({"type": "text", "text": content}) - for path in image_paths: - messages.append(image_to_base64(path)) - - # Get conversation history and add current message - conversation = get_conversation_history(db, entry_id) - if messages: - conversation.append({"role": "user", "content": messages}) - - # Retrieve relevant memories for context (V2 only) - memories = retrieve_relevant_memories(db, content) if is_v2 and content else [] - - def respond_and_save(): - # Stream response to user - response_text = [] - for chunk in generate_response(conversation, memories=memories): - response_text.append(chunk) - yield chunk - - # Save messages to DB after response completes - if content: - save_user_message(db, entry_id, content, version, msg_date) - for path in image_paths: - save_user_message(db, entry_id, path, version, msg_date) - save_assistant_message(db, entry_id, "".join(response_text), msg_date) - - return StreamingResponse(respond_and_save(), media_type="text/plain") - - -@router.get("/search") -def search_entries(q: str, version: int = 1): - """Search entries using vector search, grouped by entry.""" - db = get_database() - logger.info(f"Searching entries with query: {q[:50]}... (version={version})") - - # Use appropriate embedding based on version - if version == 2: - query_embedding = get_multimodal_embedding(q, mode="text", input_type="query") - else: - query_embedding = get_text_embedding(q, input_type="query") - - pipeline = [ - { - "$vectorSearch": { - "index": VECTOR_INDEX_NAME, - "path": "embedding", - "queryVector": query_embedding, - "numCandidates": VECTOR_NUM_CANDIDATES, - "limit": 20, - "filter": {"user_id": USER_ID, "version": version}, - } - }, - { - "$project": { - "entry_id": 1, - "content": 1, - "image": 1, - "created_at": 1, - "score": {"$meta": "vectorSearchScore"}, - } - }, - { - "$group": { - "_id": "$entry_id", - "content": {"$first": "$content"}, - "image": {"$first": "$image"}, - "created_at": {"$first": "$created_at"}, - "score": {"$max": "$score"}, - } - }, - {"$sort": {"score": -1}}, - {"$limit": 5}, - ] - - results = list(db.messages.aggregate(pipeline)) - for result in results: - result["_id"] = str(result["_id"]) - - logger.info(f"Search returned {len(results)} entries") - return results - - -@router.post("/{entry_id}/analyze") -def save_entry(entry_id: str, entry_date: str = Form(...)): - """Analyze entry for sentiment/themes and extract memories.""" - db = get_database() - conversation = get_conversation_history(db, entry_id, include_images=False) - - if not conversation: - return {"error": "No messages in entry"} - - # Analyze sentiment and themes - analysis = analyze_entry(conversation) - db.entries.update_one( - {"_id": ObjectId(entry_id)}, - {"$set": {"sentiment": analysis["sentiment"], "themes": analysis["themes"]}}, - ) - - # Extract memories from full conversation - extract_and_save_memories( - db, entry_id, conversation, datetime.fromisoformat(entry_date) - ) - - -@router.get("/") -def get_entries(version: int = 1): - db = get_database() - query = {"user_id": USER_ID, "version": version} - entries = list(db.entries.find(query).sort("created_at", -1)) - for entry in entries: - entry["_id"] = str(entry["_id"]) - return entries - - -@router.post("/generate-prompt") -def generate_prompt(entry_id: str = Form(...), entry_date: str = Form(...)): - """Generate a journal prompt based on the last month's memories.""" - db = get_database() - one_month_ago = datetime.now() - timedelta(days=30) - - memories = list( - db.memories.find( - {"user_id": USER_ID, "created_at": {"$gte": one_month_ago}}, - {"content": 1, "created_at": 1, "_id": 0}, - ) - ) - memory_contents = [ - f"Date: {m['created_at'].strftime('%Y-%m-%d')}, Memory: {m['content']}" - for m in memories - ] - logger.info(f"Found {len(memory_contents)} memories from the last month") - - prompt = generate_journal_prompt(memory_contents) - - # Save the prompt as an assistant message - msg_date = datetime.fromisoformat(entry_date) - prompt_msg = { - "entry_id": entry_id, - "role": "assistant", - "content": prompt, - "created_at": msg_date, - } - db.messages.insert_one(prompt_msg) - logger.info(f"Saved generated prompt for entry {entry_id}") - - return {"prompt": prompt} - - -@router.get("/{entry_id}/messages") -def get_messages(entry_id: str): - db = get_database() - messages = list(db.messages.find({"entry_id": entry_id}).sort("created_at", 1)) - for msg in messages: - msg["_id"] = str(msg["_id"]) - msg.pop("embedding", None) - return messages - - -@router.get("/insights") -def get_insights(): - """Get user insights: stats, mood distribution, and themes.""" - db = get_database() - return { - "total_entries": get_total_entries(db, USER_ID), - "longest_streak": get_longest_streak(db, USER_ID), - "mood": get_mood_distribution(db, USER_ID), - "themes": get_themes(db, USER_ID), - } - - -@router.delete("/{entry_id}") -def delete_entry(entry_id: str): - db = get_database() - db.entries.delete_one({"_id": ObjectId(entry_id)}) - messages = db.messages.delete_many({"entry_id": entry_id}) - memories = db.memories.delete_many({"entry_id": entry_id}) - logger.info( - f"Deleted entry {entry_id}: " - f"{messages.deleted_count} messages, {memories.deleted_count} memories" - ) - return {"deleted": True} diff --git a/apps/interactive-journal/backend/app/services/anthropic.py b/apps/interactive-journal/backend/app/services/anthropic.py deleted file mode 100644 index 3215f5d..0000000 --- a/apps/interactive-journal/backend/app/services/anthropic.py +++ /dev/null @@ -1,119 +0,0 @@ -import logging -from datetime import datetime -from typing import Literal, Optional - -import anthropic -from pydantic import BaseModel - -from app.config import ANTHROPIC_API_KEY, ANTHROPIC_MODEL -from app.services.prompts import ( - INSIGHTS_PROMPT, - JOURNAL_SYSTEM_PROMPT, - MEMORY_EXTRACTION_PROMPT, - PROMPT_GENERATOR, -) - -logger = logging.getLogger(__name__) - -client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY) - - -class MemoriesOutput(BaseModel): - memories: list[str] - - -class EntryAnalysis(BaseModel): - sentiment: Literal["positive", "negative", "neutral", "mixed"] - themes: list[str] - - -def extract_memories(user_message: str) -> list[str]: - """Extract memories/insights from a user's journal entry.""" - logger.info(f"Extracting memories using {ANTHROPIC_MODEL}") - - try: - response = client.beta.messages.parse( - model=ANTHROPIC_MODEL, - max_tokens=500, - temperature=0.8, - betas=["structured-outputs-2025-11-13"], - system=MEMORY_EXTRACTION_PROMPT, - messages=[{"role": "user", "content": user_message}], - output_format=MemoriesOutput, - ) - memories = response.parsed_output.memories - logger.info(f"Extracted {len(memories)} memories") - return memories - except Exception as e: - logger.error(f"Failed to extract memories: {e}") - return [] - - -def analyze_entry(conversation: list[dict]) -> dict: - """Analyze a journal entry for sentiment and themes.""" - logger.info(f"Analyzing entry with {len(conversation)} messages") - - content = "\n".join(f"{msg['role']}: {msg['content']}" for msg in conversation) - - try: - response = client.beta.messages.parse( - model=ANTHROPIC_MODEL, - max_tokens=200, - temperature=0.8, - betas=["structured-outputs-2025-11-13"], - system=INSIGHTS_PROMPT, - messages=[{"role": "user", "content": content}], - output_format=EntryAnalysis, - ) - result = { - "sentiment": response.parsed_output.sentiment, - "themes": response.parsed_output.themes, - } - logger.info(f"Entry analysis: {result}") - return result - except Exception as e: - logger.error(f"Failed to analyze entry: {e}") - return {"sentiment": "neutral", "themes": []} - - -def generate_response(messages: list[dict], memories: Optional[list[str]] = None): - """Generate a streaming response using Anthropic's Claude.""" - logger.info( - f"Generating response using {ANTHROPIC_MODEL} with {len(memories) if memories else 0} memories" - ) - - system_prompt = JOURNAL_SYSTEM_PROMPT - if memories: - memory_context = "\n".join(f"- {m}" for m in memories) - system_prompt += f"\n\nRelevant memories about this user:\n{memory_context}\n\nUse these memories to provide more personalized and contextual responses when relevant." - - with client.messages.stream( - model=ANTHROPIC_MODEL, - max_tokens=500, - temperature=0.8, - system=system_prompt, - messages=messages, - ) as stream: - yield from stream.text_stream - - -def generate_journal_prompt(memories: list[str]) -> str: - """Generate a reflective journal prompt based on past memories.""" - logger.info(f"Generating journal prompt from {len(memories)} memories") - - if not memories: - return "What's on your mind today?" - - today = datetime.now().strftime("%Y-%m-%d") - memory_context = "\n".join(f"- {m}" for m in memories) - user_content = f"Today's date: {today}\n\nMemories:\n{memory_context}" - - response = client.messages.create( - model=ANTHROPIC_MODEL, - max_tokens=150, - temperature=0.8, - system=PROMPT_GENERATOR, - messages=[{"role": "user", "content": user_content}], - ) - - return response.content[0].text diff --git a/apps/interactive-journal/backend/app/services/prompts.py b/apps/interactive-journal/backend/app/services/prompts.py deleted file mode 100644 index 459aadf..0000000 --- a/apps/interactive-journal/backend/app/services/prompts.py +++ /dev/null @@ -1,47 +0,0 @@ -JOURNAL_SYSTEM_PROMPT = """You are a thoughtful and empathetic AI journaling companion called Memoir. -Your role is to help users reflect on their thoughts, feelings, and experiences through conversation. - -Guidelines: -- Ask thoughtful follow-up questions to help users explore their thoughts deeper -- Be supportive and non-judgmental -- Help users identify patterns and insights in their reflections -- Keep responses concise but meaningful -- Encourage self-reflection without being preachy -- If users share something difficult, acknowledge their feelings first - -Remember: You're a journaling companion, not a therapist. Focus on reflection and exploration.""" - -MEMORY_EXTRACTION_PROMPT = """You are a memory extraction system. Analyze the user's journal entry and extract meaningful memories, insights, and facts about the user. - -Extract information such as: -- Personal facts (relationships, work, hobbies, preferences) -- Emotional patterns and feelings -- Goals, aspirations, and plans -- Significant events or experiences -- Insights and realizations - -Return a JSON array of memory strings. Each memory should be a concise, standalone statement ending in a period. -If no meaningful memories can be extracted, return an empty array. - -Example output: -["User has a sister named Sarah.", "User feels anxious about their job interview next week.", "User enjoys morning walks."]""" - -PROMPT_GENERATOR = """Based on the user's past memories, generate a thoughtful journaling prompt that encourages deeper reflection. - -Each memory includes its date. Use this to frame your prompt appropriately (e.g., "Last week you mentioned..." or "A few weeks ago you wrote about..."). Today's date is provided below. - -Pick one memory that seems meaningful and ask an open-ended question about it. Keep the prompt concise (1-2 sentences). - -Return only the prompt, nothing else.""" - -INSIGHTS_PROMPT = """Analyze this journal entry conversation and extract: -1. Overall sentiment (positive, negative, neutral, or mixed) -2. Key themes discussed (2-4 short themes) - -Return a JSON object with this structure: -{ - "sentiment": "positive" | "negative" | "neutral" | "mixed", - "themes": ["theme1", "theme2", ...] -} - -Keep themes concise (1-3 words each). Examples: "work stress", "family", "self-improvement", "gratitude".""" diff --git a/apps/interactive-journal/frontend/src/App.jsx b/apps/interactive-journal/frontend/src/App.jsx deleted file mode 100644 index 9f53848..0000000 --- a/apps/interactive-journal/frontend/src/App.jsx +++ /dev/null @@ -1,172 +0,0 @@ -import { useState, useEffect } from 'react' -import Sidebar from './components/Sidebar' -import Entry from './components/Entry' -import './App.css' - -const API_URL = 'http://localhost:8000/api' - -function App() { - const [entries, setEntries] = useState([]) - const [activeEntry, setActiveEntry] = useState(null) - const [messages, setMessages] = useState([]) - const [isV2, setIsV2] = useState(false) - const [activeSection, setActiveSection] = useState(null) - - useEffect(() => { - fetchEntries() - }, [isV2]) - - useEffect(() => { - if (activeEntry) { - fetchMessages(activeEntry) - } - }, [activeEntry]) - - const fetchEntries = async () => { - const version = isV2 ? 2 : 1 - const res = await fetch(`${API_URL}/entries/?version=${version}`) - const data = await res.json() - setEntries(data) - } - - const fetchMessages = async (entryId) => { - const res = await fetch(`${API_URL}/entries/${entryId}/messages`) - const data = await res.json() - setMessages(data) - } - - const createEntry = async (entryDate) => { - const version = isV2 ? 2 : 1 - const formData = new FormData() - formData.append('version', version) - formData.append('entry_date', entryDate) - const res = await fetch(`${API_URL}/entries/`, { - method: 'POST', - body: formData - }) - const data = await res.json() - await fetchEntries() - setActiveEntry(data._id) - setMessages([]) - setActiveSection(null) - } - - const deleteEntry = async (entryId) => { - await fetch(`${API_URL}/entries/${entryId}`, { method: 'DELETE' }) - await fetchEntries() - if (activeEntry === entryId) { - setActiveEntry(null) - setMessages([]) - } - } - - const toggleVersion = () => { - setIsV2(!isV2) - setActiveEntry(null) - setMessages([]) - } - - const sendMessage = async (content, images = []) => { - // Show user messages immediately (text and images separately) - const newMessages = [] - - if (content.trim()) { - newMessages.push({ - _id: Date.now().toString(), - role: 'user', - content - }) - } - - images.forEach((img, index) => { - newMessages.push({ - _id: Date.now().toString() + '-img-' + index, - role: 'user', - image: img.preview - }) - }) - - // Show user messages immediately - setMessages(prev => [...prev, ...newMessages]) - - // Send to backend using FormData - const formData = new FormData() - if (content) { - formData.append('content', content) - } - images.forEach(img => { - formData.append('images', img.file) - }) - formData.append('version', isV2 ? 2 : 1) - const activeEntryObj = entries.find(e => e._id === activeEntry) - if (activeEntryObj?.created_at) { - formData.append('entry_date', activeEntryObj.created_at) - } - - const res = await fetch(`${API_URL}/entries/${activeEntry}/messages`, { - method: 'POST', - body: formData - }) - - // Read the streaming response - const reader = res.body.getReader() - const decoder = new TextDecoder() - const aiMessageId = Date.now().toString() + '-ai' - let fullResponse = '' - let messageAdded = false - - while (true) { - const { done, value } = await reader.read() - if (done) break - - const chunk = decoder.decode(value, { stream: true }) - fullResponse += chunk - - // Add AI message on first chunk, then update - if (!messageAdded) { - setMessages(prev => [...prev, { _id: aiMessageId, role: 'assistant', content: fullResponse }]) - messageAdded = true - } else { - setMessages(prev => prev.map(msg => - msg._id === aiMessageId ? { ...msg, content: fullResponse } : msg - )) - } - } - } - - return ( -
- { - setActiveEntry(entryId) - if (isV2) setActiveSection('entries') - }} - onNewEntry={createEntry} - onDeleteEntry={deleteEntry} - isV2={isV2} - onToggleVersion={toggleVersion} - activeSection={activeSection} - onSectionChange={(section) => { - setActiveSection(section) - setActiveEntry(null) - setMessages([]) - }} - /> - activeEntry && fetchMessages(activeEntry)} - isV2={isV2} - activeSection={activeSection} - onSelectEntry={setActiveEntry} - /> -
- ) -} - -export default App diff --git a/apps/interactive-journal/frontend/src/index.css b/apps/interactive-journal/frontend/src/index.css deleted file mode 100644 index 363ca51..0000000 --- a/apps/interactive-journal/frontend/src/index.css +++ /dev/null @@ -1,24 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Sacramento&display=swap'); - -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -:root { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - line-height: 1.6; - font-weight: 400; - color: #1a1a1a; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - min-height: 100vh; -} - -#root { - height: 100vh; -} diff --git a/apps/interactive-journal/.gitignore b/apps/project-assistant/.gitignore similarity index 100% rename from apps/interactive-journal/.gitignore rename to apps/project-assistant/.gitignore diff --git a/apps/interactive-journal/README.md b/apps/project-assistant/README.md similarity index 100% rename from apps/interactive-journal/README.md rename to apps/project-assistant/README.md diff --git a/apps/interactive-journal/backend/.env.example b/apps/project-assistant/backend/.env.example similarity index 65% rename from apps/interactive-journal/backend/.env.example rename to apps/project-assistant/backend/.env.example index f210bd9..65ba48b 100644 --- a/apps/interactive-journal/backend/.env.example +++ b/apps/project-assistant/backend/.env.example @@ -2,7 +2,7 @@ MONGODB_URI=your-mongodb-connection-string-here # Database name -DATABASE_NAME=memoir +DATABASE_NAME=dev_assist # Anthropic API key ANTHROPIC_API_KEY=your-anthropic-api-key-here @@ -10,5 +10,7 @@ ANTHROPIC_API_KEY=your-anthropic-api-key-here # Anthropic model to use ANTHROPIC_MODEL=claude-sonnet-4-5 -# Voyage AI API key (for V2 semantic search) +# Voyage AI config (for V2 semantic search) VOYAGE_API_KEY=your-voyage-api-key-here +VOYAGE_MULTIMODAL_MODEL=voyage-multimodal-3.5 +VOYAGE_TEXT_MODEL=voyage-4 diff --git a/apps/project-assistant/backend/app/__init__.py b/apps/project-assistant/backend/app/__init__.py new file mode 100644 index 0000000..9b67d6b --- /dev/null +++ b/apps/project-assistant/backend/app/__init__.py @@ -0,0 +1 @@ +# DevAssist - Developer Productivity Assistant diff --git a/apps/interactive-journal/backend/app/config.py b/apps/project-assistant/backend/app/config.py similarity index 70% rename from apps/interactive-journal/backend/app/config.py rename to apps/project-assistant/backend/app/config.py index 72c40ab..fa327f6 100644 --- a/apps/interactive-journal/backend/app/config.py +++ b/apps/project-assistant/backend/app/config.py @@ -5,7 +5,7 @@ # MongoDB config MONGODB_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017") -DATABASE_NAME = os.getenv("DATABASE_NAME", "memoir") +DATABASE_NAME = os.getenv("DATABASE_NAME", "dev_assist") # Anthropic config ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") @@ -13,8 +13,8 @@ # Voyage AI config VOYAGE_API_KEY = os.getenv("VOYAGE_API_KEY") -VOYAGE_MULTIMODAL_MODEL = "voyage-multimodal-3.5" -VOYAGE_TEXT_MODEL = "voyage-3-large" +VOYAGE_MULTIMODAL_MODEL = os.getenv("VOYAGE_MULTIMODAL_MODEL", "voyage-multimodal-3.5") +VOYAGE_TEXT_MODEL = os.getenv("VOYAGE_TEXT_MODEL", "voyage-4") # Vector search config VECTOR_INDEX_NAME = "vector_index" diff --git a/apps/interactive-journal/backend/app/main.py b/apps/project-assistant/backend/app/main.py similarity index 71% rename from apps/interactive-journal/backend/app/main.py rename to apps/project-assistant/backend/app/main.py index ddd17b5..9330ad8 100644 --- a/apps/interactive-journal/backend/app/main.py +++ b/apps/project-assistant/backend/app/main.py @@ -20,18 +20,18 @@ @asynccontextmanager async def lifespan(app: FastAPI): # Startup - logger.info("Starting Memoir API...") + logger.info("Starting DevAssist API...") connect_db() - logger.info("Memoir API started successfully") + logger.info("DevAssist API started successfully") yield # Shutdown - logger.info("Shutting down Memoir API...") + logger.info("Shutting down DevAssist API...") close_db() app = FastAPI( - title="Memoir", - description="AI-powered interactive journaling application", + title="DevAssist", + description="AI-powered developer productivity assistant for project planning", version="1.0.0", lifespan=lifespan, ) @@ -46,12 +46,12 @@ async def lifespan(app: FastAPI): ) # Include routers -app.include_router(routes.router, prefix="/api/entries", tags=["entries"]) +app.include_router(routes.router, prefix="/api/projects", tags=["projects"]) @app.get("/") def root(): - return {"message": "Welcome to Memoir API"} + return {"message": "Welcome to DevAssist API"} @app.get("/health") diff --git a/apps/interactive-journal/backend/app/routers/__init__.py b/apps/project-assistant/backend/app/routers/__init__.py similarity index 100% rename from apps/interactive-journal/backend/app/routers/__init__.py rename to apps/project-assistant/backend/app/routers/__init__.py diff --git a/apps/interactive-journal/backend/app/routers/helpers.py b/apps/project-assistant/backend/app/routers/helpers.py similarity index 53% rename from apps/interactive-journal/backend/app/routers/helpers.py rename to apps/project-assistant/backend/app/routers/helpers.py index f4acd06..7e7167b 100644 --- a/apps/interactive-journal/backend/app/routers/helpers.py +++ b/apps/project-assistant/backend/app/routers/helpers.py @@ -1,7 +1,7 @@ import base64 import logging import uuid -from datetime import datetime, timedelta +from datetime import datetime from io import BytesIO from pathlib import Path @@ -21,12 +21,40 @@ UPLOADS_DIR.mkdir(parents=True, exist_ok=True) +def retrieve_relevant_memories(db, query: str) -> list[str]: + """Retrieve relevant procedural and semantic memories via vector search.""" + query_embedding = get_text_embedding(query, input_type="query") + pipeline = [ + { + "$vectorSearch": { + "index": VECTOR_INDEX_NAME, + "path": "embedding", + "queryVector": query_embedding, + "numCandidates": VECTOR_NUM_CANDIDATES, + "limit": 10, + "filter": {"user_id": USER_ID}, + } + }, + {"$project": {"content": 1, "score": {"$meta": "vectorSearchScore"}}}, + ] + results = list(db.memories.aggregate(pipeline)) + memories = [r["content"] for r in results] + logger.info(f"Retrieved {len(memories)} memories for context") + return memories + + def save_user_message( - db, entry_id: str, content: str | Path, version: int, msg_date: datetime + db, + project_id: str, + project_title: str, + content: str | Path, + version: int, + msg_date: datetime, ) -> None: """Save a user message (text or image) with its embedding.""" message = { - "entry_id": entry_id, + "project_id": project_id, + "project_title": project_title, "user_id": USER_ID, "role": "user", "version": version, @@ -48,60 +76,48 @@ def save_user_message( message["content"] = content db.messages.insert_one(message) - logger.info(f"Saved message for entry {entry_id}") + logger.info(f"Saved message for project {project_id}") def extract_and_save_memories( - db, entry_id: str, conversation: list[dict], entry_date: datetime + db, + project_id: str, + project_title: str, + conversation: list[dict], + created_at: datetime, ) -> None: - """Extract memories from conversation and save them.""" + """Extract memories from conversation: todos, preferences, and procedures.""" context = "\n".join(f"{msg['role']}: {msg['content']}" for msg in conversation) memories = extract_memories(context) if memories: - memory_docs = [ - { + memory_docs = [] + for memory in memories: + doc = { "user_id": USER_ID, - "entry_id": entry_id, - "content": memory_content, - "embedding": get_text_embedding(memory_content, input_type="document"), - "created_at": entry_date, + "project_id": project_id, + "project_title": project_title, + "type": memory["type"], + "content": memory["content"], + "created_at": created_at, } - for memory_content in memories - ] - db.memories.insert_many(memory_docs) - logger.info(f"Extracted and saved {len(memories)} memories: {memories}") - + if memory["type"] != "todo": + doc["embedding"] = get_text_embedding( + memory["content"], input_type="document" + ) + memory_docs.append(doc) -def retrieve_relevant_memories(db, query: str) -> list[str]: - """Retrieve relevant memories via vector search.""" - query_embedding = get_text_embedding(query, input_type="query") - pipeline = [ - { - "$vectorSearch": { - "index": VECTOR_INDEX_NAME, - "path": "embedding", - "queryVector": query_embedding, - "numCandidates": VECTOR_NUM_CANDIDATES, - "limit": 10, - "filter": {"user_id": USER_ID}, - } - }, - {"$project": {"content": 1, "score": {"$meta": "vectorSearchScore"}}}, - ] - results = list(db.memories.aggregate(pipeline)) - memories = [r["content"] for r in results] - logger.info(f"Retrieved {len(memories)} memories for context") - return memories + db.memories.insert_many(memory_docs) + logger.info(f"Extracted and saved {len(memories)} items") def get_conversation_history( - db, entry_id: str, include_images: bool = True + db, project_id: str, include_images: bool = True ) -> list[dict]: - """Get conversation history for an entry.""" + """Get conversation history for a project.""" history = list( db.messages.find( - {"entry_id": entry_id}, {"role": 1, "content": 1, "image": 1, "_id": 0} + {"project_id": project_id}, {"role": 1, "content": 1, "image": 1, "_id": 0} ).sort("created_at", 1) ) @@ -125,6 +141,9 @@ def image_to_base64(image_path: Path) -> dict: """Convert an image file to Claude's base64 format, resizing to fit limits.""" with Image.open(image_path) as img: img = img.resize(IMAGE_SIZE, Image.Resampling.LANCZOS) + # Convert RGBA to RGB (JPEG doesn't support transparency) + if img.mode == "RGBA": + img = img.convert("RGB") buffer = BytesIO() img.save(buffer, format="JPEG", quality=85) data = base64.standard_b64encode(buffer.getvalue()).decode("utf-8") @@ -135,17 +154,20 @@ def image_to_base64(image_path: Path) -> dict: } -def save_assistant_message(db, entry_id: str, content: str, msg_date: datetime) -> None: +def save_assistant_message( + db, project_id: str, project_title: str, content: str, msg_date: datetime +) -> None: """Save an assistant response message.""" db.messages.insert_one( { - "entry_id": entry_id, + "project_id": project_id, + "project_title": project_title, "role": "assistant", "content": content, "created_at": msg_date, } ) - logger.info(f"Saved AI response for entry {entry_id}") + logger.info(f"Saved AI response for project {project_id}") def save_image_file(image_file: UploadFile) -> Path: @@ -155,75 +177,3 @@ def save_image_file(image_file: UploadFile) -> Path: with open(image_path, "wb") as f: f.write(image_file.file.read()) return image_path - - -def get_monthly_filter(user_id: str) -> dict: - """Get common filter for monthly v2 entries.""" - thirty_days_ago = datetime.now() - timedelta(days=30) - return { - "user_id": user_id, - "version": 2, - "created_at": {"$gte": thirty_days_ago}, - } - - -def get_total_entries(db, user_id: str) -> int: - """Get total entries count for past 30 days.""" - return db.entries.count_documents(get_monthly_filter(user_id)) - - -def get_longest_streak(db, user_id: str) -> int: - """Get longest consecutive days streak in past 30 days.""" - pipeline = [ - {"$match": get_monthly_filter(user_id)}, - {"$project": {"date": {"$dateTrunc": {"date": "$created_at", "unit": "day"}}}}, - {"$group": {"_id": "$date"}}, - {"$sort": {"_id": 1}}, - ] - dates = [doc["_id"] for doc in db.entries.aggregate(pipeline)] - - if not dates: - return 0 - - longest = current = 1 - for i in range(1, len(dates)): - if (dates[i] - dates[i - 1]).days == 1: - current += 1 - longest = max(longest, current) - else: - current = 1 - - return longest - - -def get_mood_distribution(db, user_id: str) -> dict: - """Get sentiment distribution for past 30 days.""" - filter = get_monthly_filter(user_id) - filter["sentiment"] = {"$exists": True} - pipeline = [ - {"$match": filter}, - {"$group": {"_id": "$sentiment", "count": {"$sum": 1}}}, - ] - results = list(db.entries.aggregate(pipeline)) - counts = {r["_id"]: r["count"] for r in results} - total = sum(counts.values()) or 1 - return { - "positive": round(counts.get("positive", 0) / total * 100), - "neutral": round(counts.get("neutral", 0) / total * 100), - "mixed": round(counts.get("mixed", 0) / total * 100), - "negative": round(counts.get("negative", 0) / total * 100), - } - - -def get_themes(db, user_id: str) -> list[dict]: - """Get all themes with counts for past 30 days.""" - filter = get_monthly_filter(user_id) - filter["themes"] = {"$exists": True} - pipeline = [ - {"$match": filter}, - {"$unwind": "$themes"}, - {"$group": {"_id": "$themes", "count": {"$sum": 1}}}, - {"$sort": {"count": -1}}, - ] - results = list(db.entries.aggregate(pipeline)) - return [{"theme": r["_id"], "count": r["count"]} for r in results] diff --git a/apps/project-assistant/backend/app/routers/routes.py b/apps/project-assistant/backend/app/routers/routes.py new file mode 100644 index 0000000..a2faf42 --- /dev/null +++ b/apps/project-assistant/backend/app/routers/routes.py @@ -0,0 +1,196 @@ +import json +import logging +from datetime import datetime +from typing import Optional + +from bson import ObjectId +from fastapi import APIRouter, File, Form, UploadFile +from fastapi.responses import StreamingResponse + +from app.config import USER_ID, VECTOR_INDEX_NAME, VECTOR_NUM_CANDIDATES +from app.routers.helpers import ( + extract_and_save_memories, + get_conversation_history, + image_to_base64, + retrieve_relevant_memories, + save_assistant_message, + save_image_file, + save_user_message, +) +from app.services.anthropic import generate_response +from app.services.mongodb import get_database +from app.services.voyage import get_multimodal_embedding, get_text_embedding + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +@router.post("/{project_id}/messages") +def send_message( + project_id: str, + content: Optional[str] = Form(None), + images: list[UploadFile] = File([]), + version: int = Form(1), + project_date: Optional[str] = Form(None), + project_title: str = Form(...), +): + db = get_database() + is_v2 = version == 2 + msg_date = datetime.fromisoformat(project_date) + + # Save image files to disk + image_paths = [save_image_file(image) for image in images] + + # Build current message (text, images, or both) + messages = [] + if content: + messages.append({"type": "text", "text": content}) + for path in image_paths: + messages.append(image_to_base64(path)) + + # Get conversation history and add current message + conversation = get_conversation_history(db, project_id) + if messages: + conversation.append({"role": "user", "content": messages}) + + # Retrieve relevant memories for context (V2 only) + memories = retrieve_relevant_memories(db, content) if is_v2 and content else [] + + def stream_and_save(): + response_content = "" + for chunk in generate_response(conversation, memories=memories): + yield json.dumps(chunk) + "\n" + if chunk["type"] == "response": + response_content += chunk["content"] + + # Save messages to DB after streaming completes + if content: + save_user_message(db, project_id, project_title, content, version, msg_date) + for path in image_paths: + save_user_message(db, project_id, project_title, path, version, msg_date) + save_assistant_message( + db, project_id, project_title, response_content, msg_date + ) + + return StreamingResponse(stream_and_save(), media_type="application/x-ndjson") + + +@router.get("/search") +def search_projects(q: str, version: int = 1): + """Search projects using vector search.""" + db = get_database() + logger.info(f"Searching projects with query: {q[:50]}... (version={version})") + + # Use appropriate embedding based on version + if version == 2: + query_embedding = get_multimodal_embedding(q, mode="text", input_type="query") + else: + query_embedding = get_text_embedding(q, input_type="query") + + pipeline = [ + { + "$vectorSearch": { + "index": VECTOR_INDEX_NAME, + "path": "embedding", + "queryVector": query_embedding, + "numCandidates": VECTOR_NUM_CANDIDATES, + "limit": 20, + "filter": {"user_id": USER_ID, "version": version}, + } + }, + { + "$project": { + "project_id": 1, + "project_title": 1, + "content": 1, + "image": 1, + "created_at": 1, + "score": {"$meta": "vectorSearchScore"}, + } + }, + { + "$group": { + "_id": "$project_id", + "project_title": {"$first": "$project_title"}, + "content": {"$first": "$content"}, + "image": {"$first": "$image"}, + "created_at": {"$first": "$created_at"}, + "score": {"$max": "$score"}, + } + }, + {"$sort": {"score": -1}}, + {"$limit": 5}, + ] + + results = list(db.messages.aggregate(pipeline)) + for result in results: + result["_id"] = str(result["_id"]) + + logger.info(f"Search returned {len(results)} projects") + return results + + +@router.post("/") +def create_project(version: int = Form(1), title: str = Form(...)): + db = get_database() + project_data = { + "user_id": USER_ID, + "title": title, + "version": version, + "created_at": datetime.now(), + } + result = db.projects.insert_one(project_data) + logger.info(f"Created project '{title}' for user {USER_ID}") + return {"_id": str(result.inserted_id)} + + +@router.post("/{project_id}/save") +def save_project( + project_id: str, project_date: str = Form(...), project_title: str = Form(...) +): + """Extract and save memories from the conversation.""" + db = get_database() + conversation = get_conversation_history(db, project_id, include_images=False) + + if not conversation: + return {"error": "No messages in project"} + + extract_and_save_memories( + db, + project_id, + project_title, + conversation, + datetime.fromisoformat(project_date), + ) + + return {"success": True} + + +@router.get("/") +def get_projects(version: int = 1): + db = get_database() + query = {"user_id": USER_ID, "version": version} + projects = list(db.projects.find(query).sort("created_at", -1)) + for project in projects: + project["_id"] = str(project["_id"]) + return projects + + +@router.get("/{project_id}/messages") +def get_messages(project_id: str): + db = get_database() + messages = list(db.messages.find({"project_id": project_id}).sort("created_at", 1)) + for msg in messages: + msg["_id"] = str(msg["_id"]) + msg.pop("embedding", None) + return messages + + +@router.delete("/{project_id}") +def delete_project(project_id: str): + db = get_database() + db.projects.delete_one({"_id": ObjectId(project_id)}) + messages = db.messages.delete_many({"project_id": project_id}) + logger.info(f"Deleted project {project_id}: {messages.deleted_count} messages") + return {"deleted": True} diff --git a/apps/interactive-journal/backend/app/services/__init__.py b/apps/project-assistant/backend/app/services/__init__.py similarity index 100% rename from apps/interactive-journal/backend/app/services/__init__.py rename to apps/project-assistant/backend/app/services/__init__.py diff --git a/apps/project-assistant/backend/app/services/anthropic.py b/apps/project-assistant/backend/app/services/anthropic.py new file mode 100644 index 0000000..642fab7 --- /dev/null +++ b/apps/project-assistant/backend/app/services/anthropic.py @@ -0,0 +1,79 @@ +import logging +from typing import Literal, Optional + +import anthropic +from pydantic import BaseModel + +from app.config import ANTHROPIC_API_KEY, ANTHROPIC_MODEL +from app.services.prompts import ( + MEMORY_EXTRACTION_PROMPT, + SYSTEM_PROMPT, +) + +logger = logging.getLogger(__name__) + +client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY) + + +class MemoryItem(BaseModel): + type: Literal["semantic", "procedural"] + content: str + + +class MemoriesOutput(BaseModel): + memories: list[MemoryItem] + + +def extract_memories(user_message: str) -> list[dict]: + """Extract structured memories from a conversation.""" + logger.info(f"Extracting memories using {ANTHROPIC_MODEL}") + + try: + response = client.beta.messages.parse( + model=ANTHROPIC_MODEL, + max_tokens=2000, + temperature=1, + betas=["structured-outputs-2025-11-13"], + system=MEMORY_EXTRACTION_PROMPT, + messages=[{"role": "user", "content": user_message}], + output_format=MemoriesOutput, + ) + memories = [ + {"type": m.type, "content": m.content} + for m in response.parsed_output.memories + ] + logger.info(f"Extracted {len(memories)} memories: {memories}") + return memories + except Exception as e: + logger.error(f"Failed to extract memories: {e}") + return [] + + +def generate_response(messages: list[dict], memories: Optional[list[str]] = None): + """Generate a response with extended thinking.""" + logger.info( + f"Generating response using {ANTHROPIC_MODEL} with {len(memories) if memories else 0} memories" + ) + + system_prompt = SYSTEM_PROMPT + if memories: + memory_context = "\n".join(f"- {m}" for m in memories) + system_prompt += f"\n\nMemories about this user:\n{memory_context}" + + with client.messages.stream( + model=ANTHROPIC_MODEL, + max_tokens=16000, + temperature=1, + thinking={ + "type": "enabled", + "budget_tokens": 8000, + }, + system=system_prompt, + messages=messages, + ) as stream: + for event in stream: + if event.type == "content_block_delta": + if hasattr(event.delta, "thinking"): + yield {"type": "thinking", "content": event.delta.thinking} + elif hasattr(event.delta, "text"): + yield {"type": "response", "content": event.delta.text} diff --git a/apps/interactive-journal/backend/app/services/mongodb.py b/apps/project-assistant/backend/app/services/mongodb.py similarity index 97% rename from apps/interactive-journal/backend/app/services/mongodb.py rename to apps/project-assistant/backend/app/services/mongodb.py index feee948..ac3c232 100644 --- a/apps/interactive-journal/backend/app/services/mongodb.py +++ b/apps/project-assistant/backend/app/services/mongodb.py @@ -27,7 +27,7 @@ def connect_db(): def setup_collections(): - for name in ["entries", "messages", "memories"]: + for name in ["projects", "messages", "memories"]: try: db.create_collection(name) logger.info(f"Created collection: {name}") diff --git a/apps/project-assistant/backend/app/services/prompts.py b/apps/project-assistant/backend/app/services/prompts.py new file mode 100644 index 0000000..f4267ef --- /dev/null +++ b/apps/project-assistant/backend/app/services/prompts.py @@ -0,0 +1,44 @@ +MEMORY_EXTRACTION_PROMPT = """You are a developer context extraction system. Analyze the project planning conversation and extract structured information. + +Extract and categorize into these types: + +1. "semantic": User's technical preferences and decisions that apply broadly + - Technology choices, coding patterns, architectural preferences + - Example: "Prefers TypeScript over JavaScript", "Uses MongoDB for document storage" + +2. "procedural": A complete step-by-step implementation guide + - Synthesize the discussed approach into a reusable recipe + - Include a descriptive title followed by numbered steps + - Only create ONE procedural memory if a substantial implementation was discussed + - Should be comprehensive enough to guide similar future projects + +Return a JSON array of objects, each with "type" and "content" fields. +If nothing meaningful can be extracted, return an empty array. + +Example output: +[ + {"type": "semantic", "content": "Prefers Python with FastAPI for backend APIs"}, + {"type": "semantic", "content": "Uses environment variables for secrets"}, + {"type": "procedural", "content": "Building a Slack Webhook Integration:\\n1. Create a Slack app in the developer portal and enable incoming webhooks\\n2. Generate a webhook URL and store it securely in environment variables\\n3. Create an API endpoint that formats messages using Slack's Block Kit format\\n4. Implement retry logic with exponential backoff for failed deliveries\\n5. Add request signature verification using Slack's signing secret\\n6. Set up ngrok for local development testing\\n7. Configure event subscriptions for the specific events you need"} +]""" + + +SYSTEM_PROMPT = """You are an AI-powered developer productivity assistant. +Your role is to help developers plan and break down projects into actionable steps. + +IMPORTANT: When context about the user's preferences or past decisions is provided, reference them naturally to maintain consistency. + +Guidelines: +- Help break down projects into smaller, manageable tasks +- Ask clarifying questions about requirements, tech stack, and constraints +- Suggest best practices and potential approaches +- Identify dependencies and potential blockers early +- Reference past preferences (e.g., "You typically prefer TypeScript - should we use that here?") +- Keep responses concise and actionable + +Formatting: +- Use plain text and bullet points only - no headers or titles +- Keep it conversational and direct +- Use numbered lists for sequential steps + +Remember: You're a planning assistant. Help developers think through their projects systematically.""" diff --git a/apps/interactive-journal/backend/app/services/voyage.py b/apps/project-assistant/backend/app/services/voyage.py similarity index 100% rename from apps/interactive-journal/backend/app/services/voyage.py rename to apps/project-assistant/backend/app/services/voyage.py index bf2fdcc..fe82cd4 100644 --- a/apps/interactive-journal/backend/app/services/voyage.py +++ b/apps/project-assistant/backend/app/services/voyage.py @@ -16,6 +16,27 @@ vo = voyageai.Client(api_key=VOYAGE_API_KEY) +def get_text_embedding(text: str, input_type: str = "document") -> list[float]: + """ + Generate text embeddings using Voyage AI's voyage-3-large model. + + Args: + text: Text string to embed + input_type: Type of input ("document" or "query") + + Returns: + list[float]: Embedding of the text as a list. + """ + logger.info(f"Generating text embedding: input_type={input_type}") + + result = vo.embed( + texts=[text], model=VOYAGE_TEXT_MODEL, input_type=input_type + ).embeddings[0] + + logger.debug(f"Generated {len(result)}-dim embedding") + return result + + def get_multimodal_embedding( content: str | Path, mode: str, input_type: str ) -> list[float]: @@ -44,24 +65,3 @@ def get_multimodal_embedding( logger.debug(f"Generated {len(result)}-dim embedding") return result - - -def get_text_embedding(text: str, input_type: str = "document") -> list[float]: - """ - Generate text embeddings using Voyage AI's voyage-3-large model. - - Args: - text: Text string to embed - input_type: Type of input ("document" or "query") - - Returns: - list[float]: Embedding of the text as a list. - """ - logger.info(f"Generating text embedding: input_type={input_type}") - - result = vo.embed( - texts=[text], model=VOYAGE_TEXT_MODEL, input_type=input_type - ).embeddings[0] - - logger.debug(f"Generated {len(result)}-dim embedding") - return result diff --git a/apps/interactive-journal/backend/requirements.txt b/apps/project-assistant/backend/requirements.txt similarity index 100% rename from apps/interactive-journal/backend/requirements.txt rename to apps/project-assistant/backend/requirements.txt diff --git a/apps/interactive-journal/frontend/.gitignore b/apps/project-assistant/frontend/.gitignore similarity index 100% rename from apps/interactive-journal/frontend/.gitignore rename to apps/project-assistant/frontend/.gitignore diff --git a/apps/interactive-journal/frontend/eslint.config.js b/apps/project-assistant/frontend/eslint.config.js similarity index 100% rename from apps/interactive-journal/frontend/eslint.config.js rename to apps/project-assistant/frontend/eslint.config.js diff --git a/apps/interactive-journal/frontend/index.html b/apps/project-assistant/frontend/index.html similarity index 91% rename from apps/interactive-journal/frontend/index.html rename to apps/project-assistant/frontend/index.html index c20fbd3..9e139e7 100644 --- a/apps/interactive-journal/frontend/index.html +++ b/apps/project-assistant/frontend/index.html @@ -4,7 +4,7 @@ - frontend + DevAssist
diff --git a/apps/interactive-journal/frontend/package-lock.json b/apps/project-assistant/frontend/package-lock.json similarity index 68% rename from apps/interactive-journal/frontend/package-lock.json rename to apps/project-assistant/frontend/package-lock.json index bdafea8..5042388 100644 --- a/apps/interactive-journal/frontend/package-lock.json +++ b/apps/project-assistant/frontend/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-markdown": "^10.1.0" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -1366,13 +1367,39 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1380,11 +1407,25 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1400,6 +1441,18 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", @@ -1484,6 +1537,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1577,6 +1640,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1594,6 +1667,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1614,6 +1727,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1647,14 +1770,12 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1668,6 +1789,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1675,6 +1809,28 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.266", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz", @@ -1921,6 +2077,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1931,6 +2097,12 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2082,6 +2254,46 @@ "node": ">=8" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -2099,6 +2311,16 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2136,6 +2358,46 @@ "node": ">=0.8.19" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2159,6 +2421,28 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2280,6 +2564,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2290,112 +2584,706 @@ "yallist": "^3.0.2" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" }, - "engines": { - "node": "*" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "engines": { - "node": ">= 0.8.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", @@ -2406,6 +3294,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2485,6 +3398,16 @@ "node": ">= 0.8.0" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2516,6 +3439,33 @@ "react": "^19.2.1" } }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -2526,6 +3476,39 @@ "node": ">=0.10.0" } }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2627,6 +3610,30 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2640,6 +3647,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2670,6 +3695,26 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2683,6 +3728,93 @@ "node": ">= 0.8.0" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", @@ -2724,6 +3856,34 @@ "punycode": "^2.1.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "7.2.7", "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", @@ -2867,6 +4027,16 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/apps/interactive-journal/frontend/package.json b/apps/project-assistant/frontend/package.json similarity index 90% rename from apps/interactive-journal/frontend/package.json rename to apps/project-assistant/frontend/package.json index 1d89f06..89fd2c8 100644 --- a/apps/interactive-journal/frontend/package.json +++ b/apps/project-assistant/frontend/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-markdown": "^10.1.0" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/apps/project-assistant/frontend/public/mongodb-logo.png b/apps/project-assistant/frontend/public/mongodb-logo.png new file mode 100644 index 0000000..c87c2a7 Binary files /dev/null and b/apps/project-assistant/frontend/public/mongodb-logo.png differ diff --git a/apps/interactive-journal/frontend/public/vite.svg b/apps/project-assistant/frontend/public/vite.svg similarity index 100% rename from apps/interactive-journal/frontend/public/vite.svg rename to apps/project-assistant/frontend/public/vite.svg diff --git a/apps/interactive-journal/frontend/src/App.css b/apps/project-assistant/frontend/src/App.css similarity index 56% rename from apps/interactive-journal/frontend/src/App.css rename to apps/project-assistant/frontend/src/App.css index 24e63de..3d66664 100644 --- a/apps/interactive-journal/frontend/src/App.css +++ b/apps/project-assistant/frontend/src/App.css @@ -3,10 +3,10 @@ height: 100vh; position: relative; overflow: hidden; - background: #fafafa; + background: var(--mongodb-white); } -/* Mesh gradient background */ +/* Clean MongoDB-style background with subtle accent */ .app::before { content: ''; position: fixed; @@ -15,35 +15,12 @@ width: 100%; height: 100%; background: - radial-gradient(at 0% 0%, rgba(6, 182, 212, 0.4) 0%, transparent 50%), - radial-gradient(at 50% 0%, rgba(168, 85, 247, 0.35) 0%, transparent 45%), - radial-gradient(at 100% 0%, rgba(251, 146, 60, 0.5) 0%, transparent 50%), - radial-gradient(at 0% 50%, rgba(34, 197, 94, 0.3) 0%, transparent 40%), - radial-gradient(at 50% 50%, rgba(236, 72, 153, 0.25) 0%, transparent 50%), - radial-gradient(at 100% 50%, rgba(234, 179, 8, 0.4) 0%, transparent 45%), - radial-gradient(at 0% 100%, rgba(59, 130, 246, 0.35) 0%, transparent 50%), - radial-gradient(at 50% 100%, rgba(20, 184, 166, 0.3) 0%, transparent 45%), - radial-gradient(at 100% 100%, rgba(239, 68, 68, 0.35) 0%, transparent 50%); - animation: meshMove 30s ease-in-out infinite alternate; + linear-gradient(135deg, rgba(0, 237, 100, 0.03) 0%, transparent 50%), + linear-gradient(225deg, rgba(0, 104, 74, 0.02) 0%, transparent 50%); z-index: 0; pointer-events: none; } -@keyframes meshMove { - 0% { - filter: blur(60px) saturate(150%); - transform: scale(1) rotate(0deg); - } - 50% { - filter: blur(80px) saturate(130%); - transform: scale(1.1) rotate(2deg); - } - 100% { - filter: blur(60px) saturate(150%); - transform: scale(1) rotate(-2deg); - } -} - /* Sidebar */ .sidebar { width: 280px; @@ -70,12 +47,12 @@ .user-avatar { width: 32px; height: 32px; - background: linear-gradient(135deg, #8b5cf6, #ec4899); + background: var(--mongodb-green); border-radius: 50%; display: flex; align-items: center; justify-content: center; - color: white; + color: var(--mongodb-btn-text); font-weight: 600; font-size: 14px; } @@ -83,16 +60,27 @@ .user-name { font-size: 14px; font-weight: 500; - color: #1a1a1a; + color: var(--mongodb-slate); +} + +.logo-container { + display: flex; + align-items: center; + gap: 10px; +} + +.logo-icon { + width: 32px; + height: 32px; + object-fit: contain; } .logo { - font-family: 'Sacramento', cursive; - font-size: 2.5rem; - font-weight: 400; - color: #1a1a1a; - letter-spacing: 0.08em; - -webkit-text-stroke: 0.5px #1a1a1a; + font-family: 'Source Sans 3', sans-serif; + font-size: 1.25rem; + font-weight: 600; + color: var(--mongodb-slate); + letter-spacing: -0.02em; } .sidebar-section { @@ -107,26 +95,27 @@ } .section-header { - font-size: 11px; + font-size: 12px; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.08em; - color: #6b7280; + letter-spacing: 2.5px; + color: var(--mongodb-slate); padding: 0 4px 12px; + font-family: 'Source Code Pro', monospace; } .new-entry-btn { width: 100%; - padding: 12px 16px; - background: transparent; - color: #374151; - border: 1px solid rgba(0, 0, 0, 0.15); - border-radius: 10px; + padding: 14px 16px; + background: var(--mongodb-green); + color: var(--mongodb-btn-text); + border: 1px solid var(--mongodb-dark-green); + border-radius: 4px; font-family: inherit; font-size: 14px; font-weight: 500; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.15s ease; display: flex; align-items: center; gap: 10px; @@ -135,17 +124,17 @@ .new-entry-icon { font-size: 18px; font-weight: 400; - color: #6b7280; + color: var(--mongodb-btn-text); } .new-entry-btn:hover { - background: rgba(0, 0, 0, 0.04); - border-color: rgba(0, 0, 0, 0.25); + border-radius: 40px; + box-shadow: 0 0 0 3px rgba(0, 237, 100, 0.2); } .empty-state { padding: 16px 4px; - color: #9ca3af; + color: var(--mongodb-slate); font-size: 13px; } @@ -156,12 +145,12 @@ .entry-item { padding: 10px 12px; - border-radius: 8px; + border-radius: 4px; cursor: pointer; font-family: inherit; font-size: 14px; font-weight: 400; - color: #374151; + color: var(--mongodb-slate-light); transition: all 0.15s ease; margin-bottom: 2px; display: flex; @@ -171,17 +160,18 @@ } .entry-item:hover { - background: rgba(0, 0, 0, 0.05); + background: var(--mongodb-off-white); } .entry-item.active { - background: rgba(0, 0, 0, 0.08); + background: var(--mongodb-off-white); font-weight: 500; - color: #1a1a1a; + color: var(--mongodb-slate); + border-left: 3px solid var(--mongodb-green); } .entry-item.menu-open { - background: rgba(0, 0, 0, 0.06); + background: var(--mongodb-off-white); } .entry-title { @@ -198,7 +188,7 @@ padding: 4px 6px; cursor: pointer; font-size: 14px; - color: #9ca3af; + color: var(--mongodb-slate); border-radius: 4px; transition: all 0.15s ease; line-height: 1; @@ -211,19 +201,19 @@ .menu-btn:hover, .menu-btn.open { - color: #374151; - background: rgba(0, 0, 0, 0.08); + color: var(--mongodb-slate); + background: var(--mongodb-off-white); } .dropdown-menu { position: fixed; - background: white; - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + background: var(--mongodb-white); + border-radius: 4px; + box-shadow: 0 3px 20px rgba(0, 0, 0, 0.4); min-width: 120px; z-index: 9999; overflow: hidden; - border: 1px solid rgba(0, 0, 0, 0.08); + border: 1px solid var(--mongodb-gray-light); } .dropdown-item { @@ -233,22 +223,24 @@ background: none; font-family: inherit; font-size: 13px; + font-weight: 400; text-align: left; cursor: pointer; transition: background 0.15s ease; display: block; + color: var(--mongodb-slate); } .dropdown-item:hover { - background: #f3f4f6; + background: var(--mongodb-off-white); } .dropdown-item.delete { - color: #dc2626; + color: #f87171; } .dropdown-item.delete:hover { - background: #fef2f2; + background: rgba(220, 38, 38, 0.2); } /* Entry area */ @@ -259,11 +251,11 @@ position: relative; z-index: 1; margin: 24px 24px 24px 0; - background: rgba(255, 255, 255, 0.6); - backdrop-filter: blur(30px); - border-radius: 24px; - border: 1px solid rgba(255, 255, 255, 0.7); - box-shadow: 0 8px 40px rgba(0, 0, 0, 0.06); + background: var(--mongodb-white); + border-radius: 16px; + border: 1px solid var(--mongodb-gray-light); + box-shadow: 0 3px 20px rgba(0, 0, 0, 0.08); + overflow: hidden; } .entry-empty { @@ -271,7 +263,7 @@ display: flex; align-items: center; justify-content: center; - color: #6b7280; + color: var(--mongodb-slate); font-size: 17px; font-weight: 500; } @@ -279,7 +271,7 @@ .greeting { font-size: 28px; font-weight: 400; - color: #374151; + color: var(--mongodb-slate); text-align: center; margin-bottom: 32px; } @@ -297,17 +289,17 @@ padding: 24px 48px; font-size: 15px; line-height: 1.75; - border-bottom: 1px solid rgba(0, 0, 0, 0.04); + border-bottom: 1px solid var(--mongodb-off-white); } .message.user { background: transparent; - color: #1f2937; + color: var(--mongodb-slate); } .message.assistant { - background: rgba(139, 92, 246, 0.04); - color: #1f2937; + background: var(--mongodb-off-white); + color: var(--mongodb-slate); } .message-content { @@ -319,17 +311,18 @@ font-size: 12px; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.05em; + letter-spacing: 2.5px; margin-bottom: 8px; - color: #6b7280; + color: var(--mongodb-slate); + font-family: 'Source Code Pro', monospace; } .message.user .message-label { - color: #8b5cf6; + color: var(--mongodb-dark-green); } .message.assistant .message-label { - color: #ec4899; + color: var(--mongodb-blue); } /* Entry input */ @@ -345,15 +338,15 @@ align-items: center; gap: 12px; padding: 8px 8px 8px 20px; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 24px; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; + border: 1px solid var(--mongodb-gray-light); + border-radius: 4px; + background: var(--mongodb-white); + transition: all 0.2s ease; } .entry-input-wrapper:focus-within { - border-color: rgba(139, 92, 246, 0.5); - box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.08); + border-color: var(--mongodb-blue); + box-shadow: 0 0 0 3px rgba(0, 108, 250, 0.1); } .entry-input input { @@ -362,30 +355,32 @@ border: none; font-family: inherit; font-size: 15px; + font-weight: 400; outline: none; background: transparent; + color: var(--mongodb-slate); } .entry-input input::placeholder { - color: #9ca3af; + color: var(--mongodb-slate); } .send-btn { - padding: 10px; - background: transparent; - color: #374151; - border: 1px solid rgba(0, 0, 0, 0.15); - border-radius: 10px; + padding: 10px 16px; + background: var(--mongodb-green); + color: var(--mongodb-btn-text); + border: 1px solid var(--mongodb-dark-green); + border-radius: 4px; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.15s ease; display: flex; align-items: center; justify-content: center; } .send-btn:hover { - background: rgba(0, 0, 0, 0.04); - border-color: rgba(0, 0, 0, 0.25); + border-radius: 40px; + box-shadow: 0 0 0 3px rgba(0, 237, 100, 0.2); } .send-btn svg { @@ -403,18 +398,18 @@ .version-label { font-size: 12px; font-weight: 500; - color: #9ca3af; + color: var(--mongodb-slate); transition: color 0.2s ease; } .version-label.active { - color: #1a1a1a; + color: var(--mongodb-slate); } .toggle-switch { width: 44px; height: 24px; - background: rgba(0, 0, 0, 0.1); + background: var(--mongodb-gray-light); border: none; border-radius: 12px; cursor: pointer; @@ -424,7 +419,7 @@ } .toggle-switch.on { - background: #8b5cf6; + background: var(--mongodb-green); } .toggle-knob { @@ -459,28 +454,29 @@ padding: 10px 12px; background: transparent; border: none; - border-radius: 8px; + border-radius: 4px; font-family: inherit; font-size: 14px; font-weight: 400; - color: #374151; + color: var(--mongodb-slate-light); cursor: pointer; transition: all 0.15s ease; text-align: left; } .nav-item:hover { - background: rgba(0, 0, 0, 0.05); + background: var(--mongodb-off-white); } .nav-item.active { - background: rgba(0, 0, 0, 0.08); + background: var(--mongodb-off-white); font-weight: 500; + color: var(--mongodb-slate); } .nav-item svg { flex-shrink: 0; - color: #6b7280; + color: var(--mongodb-slate); } /* Search form */ @@ -492,22 +488,24 @@ .search-input { width: 100%; padding: 10px 32px 10px 12px; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 8px; + border: 1px solid var(--mongodb-gray-light); + border-radius: 4px; font-family: inherit; font-size: 13px; - background: rgba(255, 255, 255, 0.6); + font-weight: 400; + background: var(--mongodb-white); + color: var(--mongodb-slate); outline: none; transition: all 0.2s ease; } .search-input:focus { - border-color: rgba(139, 92, 246, 0.5); - box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.1); + border-color: var(--mongodb-blue); + box-shadow: 0 0 0 2px rgba(0, 108, 250, 0.1); } .search-input::placeholder { - color: #9ca3af; + color: var(--mongodb-slate); } .search-clear { @@ -518,14 +516,14 @@ background: none; border: none; font-size: 18px; - color: #9ca3af; + color: var(--mongodb-slate); cursor: pointer; padding: 0 4px; line-height: 1; } .search-clear:hover { - color: #6b7280; + color: var(--mongodb-slate); } .search-results { @@ -536,9 +534,9 @@ .photo-btn { padding: 10px; background: transparent; - color: #6b7280; + color: var(--mongodb-slate); border: none; - border-radius: 10px; + border-radius: 4px; cursor: pointer; transition: all 0.2s ease; display: flex; @@ -547,8 +545,8 @@ } .photo-btn:hover { - background: rgba(0, 0, 0, 0.04); - color: #374151; + background: var(--mongodb-off-white); + color: var(--mongodb-slate); } /* Main area search */ @@ -567,7 +565,7 @@ .search-title { font-size: 24px; font-weight: 500; - color: #1a1a1a; + color: var(--mongodb-slate); margin-bottom: 24px; } @@ -580,22 +578,24 @@ .search-input-main { width: 100%; padding: 16px 48px 16px 20px; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 16px; + border: 1px solid var(--mongodb-gray-light); + border-radius: 4px; font-family: inherit; font-size: 16px; - background: rgba(255, 255, 255, 0.9); + font-weight: 400; + background: var(--mongodb-white); + color: var(--mongodb-slate); outline: none; transition: all 0.2s ease; } .search-input-main:focus { - border-color: rgba(139, 92, 246, 0.5); - box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); + border-color: var(--mongodb-blue); + box-shadow: 0 0 0 3px rgba(0, 108, 250, 0.1); } .search-input-main::placeholder { - color: #9ca3af; + color: var(--mongodb-slate); } .search-clear-main { @@ -606,14 +606,14 @@ background: none; border: none; font-size: 24px; - color: #9ca3af; + color: var(--mongodb-slate); cursor: pointer; padding: 0 4px; line-height: 1; } .search-clear-main:hover { - color: #6b7280; + color: var(--mongodb-slate); } .search-results-main { @@ -624,23 +624,23 @@ .no-results { text-align: center; - color: #9ca3af; + color: var(--mongodb-slate); font-size: 15px; } .search-result-item { padding: 16px 20px; - background: rgba(255, 255, 255, 0.6); - border: 1px solid rgba(0, 0, 0, 0.06); - border-radius: 12px; + background: var(--mongodb-white); + border: 1px solid var(--mongodb-gray-light); + border-radius: 4px; margin-bottom: 12px; cursor: pointer; transition: all 0.15s ease; } .search-result-item:hover { - background: rgba(255, 255, 255, 0.9); - border-color: rgba(139, 92, 246, 0.3); + border-color: var(--mongodb-dark-green); + box-shadow: 0 3px 20px rgba(0, 0, 0, 0.08); } .result-header { @@ -650,23 +650,24 @@ margin-bottom: 8px; } -.result-date { - font-size: 12px; - font-weight: 500; - color: #8b5cf6; +.result-title { + font-size: 14px; + font-weight: 600; + color: var(--mongodb-slate); } .result-score { font-size: 11px; - color: #6b7280; - background: #f3f4f6; + font-family: 'Source Code Pro', monospace; + color: var(--mongodb-btn-text); + background: var(--mongodb-lime); padding: 2px 8px; - border-radius: 12px; + border-radius: 999px; } .result-content { font-size: 14px; - color: #374151; + color: var(--mongodb-slate-light); line-height: 1.6; margin: 0; } @@ -725,6 +726,91 @@ /* Message text and images */ .message-text { margin: 0; + line-height: 1.6; +} + +.message-text h2 { + font-size: 18px; + font-weight: 600; + color: var(--mongodb-slate); + margin: 20px 0 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--mongodb-gray-light); +} + +.message-text h2:first-child { + margin-top: 0; +} + +.message-text h3 { + font-size: 15px; + font-weight: 600; + color: var(--mongodb-slate); + margin: 16px 0 8px; +} + +.message-text p { + margin: 0 0 12px; +} + +.message-text p:last-child { + margin-bottom: 0; +} + +.message-text ul, +.message-text ol { + margin: 8px 0 12px; + padding-left: 24px; +} + +.message-text li { + margin: 4px 0; +} + +.message-text strong { + font-weight: 600; + color: var(--mongodb-slate); +} + +.message-text code { + font-family: 'Source Code Pro', monospace; + font-size: 13px; + background: var(--mongodb-off-white); + padding: 2px 6px; + border-radius: 4px; + color: var(--mongodb-dark-green); +} + +.message-text pre { + background: var(--mongodb-slate); + color: var(--mongodb-off-white); + padding: 16px; + border-radius: 8px; + overflow-x: auto; + margin: 12px 0; +} + +.message-text pre code { + background: none; + padding: 0; + color: inherit; +} + +.message-text a { + color: var(--mongodb-dark-green); + text-decoration: none; +} + +.message-text a:hover { + text-decoration: underline; +} + +.message-text blockquote { + border-left: 3px solid var(--mongodb-green); + margin: 12px 0; + padding: 8px 16px; + background: rgba(0, 237, 100, 0.05); + color: var(--mongodb-slate); } .message-image { @@ -735,6 +821,61 @@ display: block; } +/* Thinking section */ +.thinking-section { + margin-bottom: 12px; +} + +.thinking-toggle { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + background: var(--mongodb-off-white); + border: 1px solid var(--mongodb-gray-light); + border-radius: 4px; + font-family: inherit; + font-size: 13px; + color: var(--mongodb-slate); + cursor: pointer; + transition: all 0.15s ease; +} + +.thinking-toggle:hover { + background: var(--mongodb-white); + border-color: var(--mongodb-dark-green); + color: var(--mongodb-slate); +} + +.thinking-toggle.expanded { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom-color: transparent; +} + +.thinking-icon { + font-size: 10px; + transition: transform 0.15s ease; +} + +.thinking-label { + font-weight: 500; +} + +.thinking-content { + padding: 12px 16px; + background: var(--mongodb-off-white); + border: 1px solid var(--mongodb-gray-light); + border-top: none; + border-radius: 0 0 4px 4px; + font-size: 13px; + line-height: 1.6; + color: var(--mongodb-slate); + white-space: pre-wrap; + max-height: 300px; + overflow-y: auto; +} + /* Date picker modal */ .date-picker-overlay { position: fixed; @@ -742,7 +883,7 @@ left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.4); + background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; @@ -750,34 +891,38 @@ } .date-picker-modal { - background: white; - border-radius: 16px; + background: var(--mongodb-white); + border: 1px solid var(--mongodb-gray-light); + border-radius: 4px; padding: 24px; min-width: 300px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2); + box-shadow: 0 3px 20px rgba(0, 0, 0, 0.4); } .date-picker-modal h3 { margin: 0 0 20px; font-size: 18px; font-weight: 600; - color: #1a1a1a; + color: var(--mongodb-slate); } .date-input { width: 100%; padding: 12px 16px; - border: 1px solid rgba(0, 0, 0, 0.15); - border-radius: 10px; + border: 1px solid var(--mongodb-gray-light); + border-radius: 4px; font-family: inherit; font-size: 15px; + font-weight: 400; + background: var(--mongodb-white); + color: var(--mongodb-slate); outline: none; transition: all 0.2s ease; } .date-input:focus { - border-color: rgba(139, 92, 246, 0.5); - box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); + border-color: var(--mongodb-blue); + box-shadow: 0 0 0 3px rgba(0, 108, 250, 0.1); } .date-picker-actions { @@ -790,32 +935,33 @@ .date-cancel-btn, .date-confirm-btn { padding: 10px 20px; - border-radius: 10px; + border-radius: 4px; font-family: inherit; font-size: 14px; font-weight: 500; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.15s ease; } .date-cancel-btn { background: transparent; - border: 1px solid rgba(0, 0, 0, 0.15); - color: #374151; + border: 1px solid var(--mongodb-gray-light); + color: var(--mongodb-slate); } .date-cancel-btn:hover { - background: rgba(0, 0, 0, 0.04); + background: var(--mongodb-off-white); } .date-confirm-btn { - background: #8b5cf6; - border: none; - color: white; + background: var(--mongodb-green); + border: 1px solid var(--mongodb-dark-green); + color: var(--mongodb-btn-text); } .date-confirm-btn:hover { - background: #7c3aed; + border-radius: 40px; + box-shadow: 0 0 0 3px rgba(0, 237, 100, 0.2); } /* Prompt generator */ @@ -830,60 +976,28 @@ .generate-prompt-btn { padding: 12px 16px; - background: transparent; - border: 1px solid rgba(0, 0, 0, 0.15); - border-radius: 10px; - color: #374151; + background: var(--mongodb-slate); + border: 1px solid var(--mongodb-slate); + border-radius: 4px; + color: var(--mongodb-white); font-family: inherit; font-size: 14px; font-weight: 500; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.15s ease; } .generate-prompt-btn:hover:not(:disabled) { - background: rgba(0, 0, 0, 0.04); - border-color: rgba(0, 0, 0, 0.25); + border-radius: 40px; } .generate-prompt-btn:disabled { - opacity: 0.6; + background: var(--mongodb-gray-light); + border-color: var(--mongodb-gray-light); + color: var(--mongodb-slate); cursor: not-allowed; } -/* Insights */ -.insights-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 24px; - width: 100%; -} - -.insight-card { - background: rgba(255, 255, 255, 0.7); - backdrop-filter: blur(20px); - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 16px; - padding: 32px 24px; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; -} - -.insight-value { - font-size: 48px; - font-weight: 600; - color: #1a1a1a; - line-height: 1; - margin-bottom: 8px; -} - -.insight-label { - font-size: 14px; - color: #6b7280; - font-weight: 500; -} /* Save Entry */ .save-entry { @@ -897,19 +1011,20 @@ .save-btn { padding: 12px 24px; - background: #8b5cf6; - border: none; - border-radius: 10px; - color: white; + background: var(--mongodb-green); + border: 1px solid var(--mongodb-dark-green); + border-radius: 4px; + color: var(--mongodb-btn-text); font-family: inherit; font-size: 14px; font-weight: 500; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.15s ease; } .save-btn:hover { - background: #7c3aed; + border-radius: 40px; + box-shadow: 0 0 0 3px rgba(0, 237, 100, 0.2); } .save-btn:active { @@ -917,114 +1032,22 @@ } .save-btn.saved { - background: #bbf7d0; - color: #166534; -} - -/* Insights Sections */ -.section-title { - font-size: 16px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 16px; -} - -.mood-section, -.themes-section { - width: 100%; - margin-top: 32px; -} - -/* Mood Bars */ -.mood-bars { - display: flex; - flex-direction: column; - gap: 12px; -} - -.mood-row { - display: flex; - align-items: center; - gap: 12px; -} - -.mood-emoji { - font-size: 20px; - width: 28px; -} - -.mood-bar-track { - flex: 1; - height: 16px; - background: rgba(0, 0, 0, 0.05); - border-radius: 8px; - overflow: hidden; -} - -.mood-bar-fill { - height: 100%; - border-radius: 8px; - transition: width 0.3s ease; -} - -.mood-bar-fill.positive { - background: #22c55e; -} - -.mood-bar-fill.neutral { - background: #94a3b8; -} - -.mood-bar-fill.mixed { - background: #f59e0b; -} - -.mood-bar-fill.negative { - background: #ef4444; -} - -.mood-percent { - font-size: 14px; - color: #6b7280; - width: 40px; - text-align: right; -} - -/* Word Cloud */ -.word-cloud { - display: flex; - flex-wrap: wrap; - gap: 8px 16px; - justify-content: center; - align-items: baseline; - max-height: 300px; - overflow-y: auto; -} - -.theme-word { - color: #6b7280; - font-weight: 500; - transition: color 0.2s ease; - cursor: default; -} - -.theme-word:hover { - color: #7c3aed; + background: var(--mongodb-lime); + border-color: var(--mongodb-forest); + color: var(--mongodb-dark-green); } /* Hidden Scrollbar */ .entry-list, .messages, .entry-search, -.search-results-main, -.word-cloud { +.search-results-main { scrollbar-width: none; } .entry-list::-webkit-scrollbar, .messages::-webkit-scrollbar, .entry-search::-webkit-scrollbar, -.search-results-main::-webkit-scrollbar, -.word-cloud::-webkit-scrollbar { +.search-results-main::-webkit-scrollbar { display: none; } diff --git a/apps/project-assistant/frontend/src/App.jsx b/apps/project-assistant/frontend/src/App.jsx new file mode 100644 index 0000000..eb16e6f --- /dev/null +++ b/apps/project-assistant/frontend/src/App.jsx @@ -0,0 +1,196 @@ +import { useState, useEffect } from 'react' +import Sidebar from './components/Sidebar' +import Entry from './components/Entry' +import './App.css' + +const API_URL = 'http://localhost:8000/api' + +function App() { + const [projects, setProjects] = useState([]) + const [activeProject, setActiveProject] = useState(null) + const [messages, setMessages] = useState([]) + const [isV2, setIsV2] = useState(false) + const [activeSection, setActiveSection] = useState(null) + + useEffect(() => { + fetchProjects() + }, [isV2]) + + useEffect(() => { + if (activeProject) { + fetchMessages(activeProject) + } + }, [activeProject]) + + const fetchProjects = async () => { + const version = isV2 ? 2 : 1 + const res = await fetch(`${API_URL}/projects/?version=${version}`) + const data = await res.json() + setProjects(data) + } + + const fetchMessages = async (projectId) => { + const res = await fetch(`${API_URL}/projects/${projectId}/messages`) + const data = await res.json() + setMessages(data) + } + + const createProject = async (title) => { + const version = isV2 ? 2 : 1 + const formData = new FormData() + formData.append('version', version) + formData.append('title', title) + const res = await fetch(`${API_URL}/projects/`, { + method: 'POST', + body: formData + }) + const data = await res.json() + await fetchProjects() + setActiveProject(data._id) + setMessages([]) + setActiveSection(null) + } + + const deleteProject = async (projectId) => { + await fetch(`${API_URL}/projects/${projectId}`, { method: 'DELETE' }) + await fetchProjects() + if (activeProject === projectId) { + setActiveProject(null) + setMessages([]) + } + } + + const toggleVersion = () => { + setIsV2(!isV2) + setActiveProject(null) + setMessages([]) + } + + const sendMessage = async (content, images = []) => { + // Show user messages immediately (text and images separately) + const newMessages = [] + + if (content.trim()) { + newMessages.push({ + _id: Date.now().toString(), + role: 'user', + content + }) + } + + images.forEach((img, index) => { + newMessages.push({ + _id: Date.now().toString() + '-img-' + index, + role: 'user', + image: img.preview + }) + }) + + // Show user messages immediately + setMessages(prev => [...prev, ...newMessages]) + + // Send to backend using FormData + const formData = new FormData() + if (content) { + formData.append('content', content) + } + images.forEach(img => { + formData.append('images', img.file) + }) + formData.append('version', isV2 ? 2 : 1) + const activeProjectObj = projects.find(p => p._id === activeProject) + if (activeProjectObj?.created_at) { + formData.append('project_date', activeProjectObj.created_at) + } + formData.append('project_title', activeProjectObj?.title || 'Unknown') + + // Add placeholder message for assistant response + const aiMessageId = Date.now().toString() + '-ai' + setMessages(prev => [...prev, { + _id: aiMessageId, + role: 'assistant', + thinking: '', + content: '' + }]) + + const res = await fetch(`${API_URL}/projects/${activeProject}/messages`, { + method: 'POST', + body: formData + }) + + // Read newline-delimited JSON stream + const reader = res.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + let thinkingContent = '' + let responseContent = '' + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + + // Process complete JSON lines + const lines = buffer.split('\n') + buffer = lines.pop() // Keep incomplete line in buffer + + for (const line of lines) { + if (!line.trim()) continue + try { + const chunk = JSON.parse(line) + if (chunk.type === 'thinking') { + thinkingContent += chunk.content + } else if (chunk.type === 'response') { + responseContent += chunk.content + } + + // Update message with current state + setMessages(prev => prev.map(msg => + msg._id === aiMessageId + ? { ...msg, thinking: thinkingContent, content: responseContent } + : msg + )) + } catch (e) { + console.error('Failed to parse chunk:', e) + } + } + } + } + + return ( +
+ { + setActiveProject(projectId) + if (isV2) setActiveSection('projects') + }} + onNewProject={createProject} + onDeleteProject={deleteProject} + isV2={isV2} + onToggleVersion={toggleVersion} + activeSection={activeSection} + onSectionChange={(section) => { + setActiveSection(section) + setActiveProject(null) + setMessages([]) + }} + /> + activeProject && fetchMessages(activeProject)} + isV2={isV2} + activeSection={activeSection} + onSelectProject={setActiveProject} + /> +
+ ) +} + +export default App diff --git a/apps/interactive-journal/frontend/src/assets/react.svg b/apps/project-assistant/frontend/src/assets/react.svg similarity index 100% rename from apps/interactive-journal/frontend/src/assets/react.svg rename to apps/project-assistant/frontend/src/assets/react.svg diff --git a/apps/interactive-journal/frontend/src/components/Entry.jsx b/apps/project-assistant/frontend/src/components/Entry.jsx similarity index 51% rename from apps/interactive-journal/frontend/src/components/Entry.jsx rename to apps/project-assistant/frontend/src/components/Entry.jsx index fbe61e0..464f826 100644 --- a/apps/interactive-journal/frontend/src/components/Entry.jsx +++ b/apps/project-assistant/frontend/src/components/Entry.jsx @@ -1,51 +1,40 @@ import { useState, useRef, useEffect } from 'react' +import ReactMarkdown from 'react-markdown' -function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries, onRefreshMessages, isV2, activeSection, onSelectEntry }) { +function Entry({ messages, onSendMessage, hasActiveProject, activeProject, projects, onRefreshMessages, isV2, activeSection, onSelectProject }) { const [input, setInput] = useState('') const [selectedImages, setSelectedImages] = useState([]) const [searchQuery, setSearchQuery] = useState('') const [searchResults, setSearchResults] = useState(null) const [isSearching, setIsSearching] = useState(false) - const [isGeneratingPrompt, setIsGeneratingPrompt] = useState(false) - const [insights, setInsights] = useState(null) const [saveStatus, setSaveStatus] = useState(null) + const [expandedThinking, setExpandedThinking] = useState({}) const messagesEndRef = useRef(null) const fileInputRef = useRef(null) - const handleGeneratePrompt = async () => { - setIsGeneratingPrompt(true) - try { - const activeEntryObj = entries.find(e => e._id === activeEntry) - const formData = new FormData() - formData.append('entry_id', activeEntry) - formData.append('entry_date', activeEntryObj?.created_at || new Date().toISOString()) - - await fetch('http://localhost:8000/api/entries/generate-prompt', { - method: 'POST', - body: formData - }) - onRefreshMessages() - } catch (error) { - console.error('Failed to generate prompt:', error) - } - setIsGeneratingPrompt(false) + const toggleThinking = (msgId) => { + setExpandedThinking(prev => ({ + ...prev, + [msgId]: !prev[msgId] + })) } - const handleSaveEntry = async () => { + const handleSaveProject = async () => { setSaveStatus('saving') try { - const activeEntryObj = entries.find(e => e._id === activeEntry) + const activeProjectObj = projects.find(p => p._id === activeProject) const formData = new FormData() - formData.append('entry_date', activeEntryObj?.created_at || new Date().toISOString()) + formData.append('project_date', activeProjectObj?.created_at || new Date().toISOString()) + formData.append('project_title', activeProjectObj?.title || 'Unknown') - await fetch(`http://localhost:8000/api/entries/${activeEntry}/analyze`, { + await fetch(`http://localhost:8000/api/projects/${activeProject}/save`, { method: 'POST', body: formData }) setSaveStatus('saved') setTimeout(() => setSaveStatus(null), 2000) } catch (error) { - console.error('Failed to save entry:', error) + console.error('Failed to save project:', error) setSaveStatus(null) } } @@ -58,15 +47,6 @@ function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries, scrollToBottom() }, [messages]) - useEffect(() => { - if (isV2 && activeSection === 'insights') { - fetch('http://localhost:8000/api/entries/insights') - .then(res => res.json()) - .then(data => setInsights(data)) - .catch(err => console.error('Failed to fetch insights:', err)) - } - }, [isV2, activeSection]) - useEffect(() => { setSearchQuery('') setSearchResults(null) @@ -75,7 +55,7 @@ function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries, const handleSubmit = (e) => { e.preventDefault() - if ((input.trim() || selectedImages.length > 0) && hasActiveEntry) { + if ((input.trim() || selectedImages.length > 0) && hasActiveProject) { onSendMessage(input, selectedImages) setInput('') setSelectedImages([]) @@ -112,7 +92,7 @@ function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries, setIsSearching(true) try { const version = isV2 ? 2 : 1 - const res = await fetch(`http://localhost:8000/api/entries/search?q=${encodeURIComponent(searchQuery)}&version=${version}`) + const res = await fetch(`http://localhost:8000/api/projects/search?q=${encodeURIComponent(searchQuery)}&version=${version}`) const data = await res.json() setSearchResults(data) } catch (error) { @@ -126,12 +106,12 @@ function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries, setSearchResults(null) } - // Show search interface when Entries tab is clicked and no entry selected - if (activeSection === 'entries' && !hasActiveEntry) { + // Show search interface when Projects tab is clicked and no project selected + if (activeSection === 'projects' && !hasActiveProject) { return (
-

Search your entries

+

Search your projects

onSelectEntry(result._id)} + onClick={() => onSelectProject(result._id)} >
- - {new Date(result.created_at).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - })} + + {result.project_title || 'Unknown Project'} {(result.score * 100).toFixed(0)}% match @@ -184,91 +160,11 @@ function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries, ) } - // Show insights when V2 Insights tab is active and no entry selected - if (isV2 && activeSection === 'insights' && !hasActiveEntry) { - return ( -
-
-

Your month in review

- {insights ? ( - <> -
-
- {insights.total_entries} - Entries -
-
- {insights.longest_streak} - Longest streak -
-
- - {(insights.mood.positive > 0 || insights.mood.neutral > 0 || insights.mood.mixed > 0 || insights.mood.negative > 0) && ( -
-

Mood

-
-
- 😊 -
-
-
- {insights.mood.positive}% -
-
- 😐 -
-
-
- {insights.mood.neutral}% -
-
- 🤔 -
-
-
- {insights.mood.mixed}% -
-
- 😔 -
-
-
- {insights.mood.negative}% -
-
-
- )} - - {insights.themes.length > 0 && ( -
-

Themes

-
- {insights.themes.map((item, i) => ( - - {item.theme} - - ))} -
-
- )} - - ) : ( -

Loading...

- )} -
-
- ) - } - - if (!hasActiveEntry) { + if (!hasActiveProject) { return (
-

How was your day, Apoorva?

+

What are you building today?

) @@ -281,9 +177,33 @@ function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries,
- {msg.role === 'user' ? 'You' : 'Memoir'} + {msg.role === 'user' ? 'You' : 'Assistant'}
- {msg.content &&

{msg.content}

} + {msg.thinking && ( +
+ + {expandedThinking[msg._id] && ( +
+ {msg.thinking} +
+ )} +
+ )} + {msg.content && ( +
+ {msg.content} +
+ )} {msg.image && ( Uploaded )} @@ -294,10 +214,10 @@ function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries,
)} @@ -322,18 +242,6 @@ function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries,
)} - {isV2 && messages.length === 0 && ( -
- -
- )} -
{isV2 && ( @@ -364,7 +272,7 @@ function Entry({ messages, onSendMessage, hasActiveEntry, activeEntry, entries, type="text" value={input} onChange={(e) => setInput(e.target.value)} - placeholder="What's on your mind?" + placeholder="What are you working on?" />
- {showDatePicker && ( -
setShowDatePicker(false)}> + {showNewProject && ( +
setShowNewProject(false)}>
e.stopPropagation()}> -

Select entry date

+

New Project

setSelectedDate(e.target.value)} + type="text" + value={projectTitle} + onChange={(e) => setProjectTitle(e.target.value)} className="date-input" + placeholder="Project name" + autoFocus + onKeyDown={(e) => e.key === 'Enter' && handleCreateProject()} />
- -
@@ -125,37 +132,29 @@ function Sidebar({ entries, activeEntry, onSelectEntry, onNewEntry, onDeleteEntr
- {isV2 && ( - - )}
Recent
- {displayEntries.length === 0 ? ( -
No recent entries
+ {displayProjects.length === 0 ? ( +
No projects yet
) : ( - displayEntries.map((entry) => ( + displayProjects.map((project) => (
onSelectEntry(entry._id)} + key={project._id} + className={`entry-item ${activeProject === project._id ? 'active' : ''} ${openMenu === project._id ? 'menu-open' : ''}`} + onClick={() => onSelectProject(project._id)} > - {formatDate(entry.created_at)} + {project.title} diff --git a/apps/project-assistant/frontend/src/index.css b/apps/project-assistant/frontend/src/index.css new file mode 100644 index 0000000..9de9258 --- /dev/null +++ b/apps/project-assistant/frontend/src/index.css @@ -0,0 +1,39 @@ +@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;500;600&family=Source+Sans+3:wght@300;400;500;600;700&display=swap'); + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + /* MongoDB Dark Theme Color Palette */ + --mongodb-green: #00ED64; + --mongodb-dark-green: #00ED64; + --mongodb-forest: #00ED64; + --mongodb-slate: #FFFFFF; + --mongodb-slate-light: #E7EEEC; + --mongodb-gray: #E7EEEC; + --mongodb-gray-light: #9CA3A0; + --mongodb-off-white: #1C2D35; + --mongodb-white: #001E2B; + --mongodb-lime: #B1FF05; + --mongodb-blue: #4D9CFF; + --mongodb-btn-text: #001E2B; + + font-family: 'Source Sans 3', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + line-height: 1.6; + font-weight: 400; + color: var(--mongodb-slate); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + min-height: 100vh; + background: var(--mongodb-white); +} + +#root { + height: 100vh; +} diff --git a/apps/interactive-journal/frontend/src/main.jsx b/apps/project-assistant/frontend/src/main.jsx similarity index 100% rename from apps/interactive-journal/frontend/src/main.jsx rename to apps/project-assistant/frontend/src/main.jsx diff --git a/apps/interactive-journal/frontend/vite.config.js b/apps/project-assistant/frontend/vite.config.js similarity index 100% rename from apps/interactive-journal/frontend/vite.config.js rename to apps/project-assistant/frontend/vite.config.js