From 8b90043eb641988fff0bfe509c54e69e3b347273 Mon Sep 17 00:00:00 2001 From: ajosh0504 Date: Mon, 5 Jan 2026 13:12:18 -0800 Subject: [PATCH 01/12] Reordering helpers --- .../backend/app/routers/helpers.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/interactive-journal/backend/app/routers/helpers.py b/apps/interactive-journal/backend/app/routers/helpers.py index f4acd06..6847ded 100644 --- a/apps/interactive-journal/backend/app/routers/helpers.py +++ b/apps/interactive-journal/backend/app/routers/helpers.py @@ -21,6 +21,28 @@ UPLOADS_DIR.mkdir(parents=True, exist_ok=True) +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 + + def save_user_message( db, entry_id: str, content: str | Path, version: int, msg_date: datetime ) -> None: @@ -73,28 +95,6 @@ def extract_and_save_memories( logger.info(f"Extracted and saved {len(memories)} memories: {memories}") -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 - - def get_conversation_history( db, entry_id: str, include_images: bool = True ) -> list[dict]: From 7e02b3c80e65c034d6f4c58b3f2c27a50f11927d Mon Sep 17 00:00:00 2001 From: ajosh0504 Date: Tue, 6 Jan 2026 08:24:34 -0800 Subject: [PATCH 02/12] Font appears continuous --- apps/interactive-journal/frontend/src/App.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/interactive-journal/frontend/src/App.css b/apps/interactive-journal/frontend/src/App.css index 24e63de..04f93e2 100644 --- a/apps/interactive-journal/frontend/src/App.css +++ b/apps/interactive-journal/frontend/src/App.css @@ -91,8 +91,6 @@ font-size: 2.5rem; font-weight: 400; color: #1a1a1a; - letter-spacing: 0.08em; - -webkit-text-stroke: 0.5px #1a1a1a; } .sidebar-section { From f5149825c65f0517897828fd74e16d9faf79a91f Mon Sep 17 00:00:00 2001 From: ajosh0504 Date: Wed, 7 Jan 2026 09:54:48 -0800 Subject: [PATCH 03/12] Improving memory usage --- apps/interactive-journal/backend/app/services/anthropic.py | 2 +- apps/interactive-journal/backend/app/services/prompts.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/interactive-journal/backend/app/services/anthropic.py b/apps/interactive-journal/backend/app/services/anthropic.py index 3215f5d..c171fba 100644 --- a/apps/interactive-journal/backend/app/services/anthropic.py +++ b/apps/interactive-journal/backend/app/services/anthropic.py @@ -85,7 +85,7 @@ def generate_response(messages: list[dict], memories: Optional[list[str]] = None 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." + system_prompt += f"\n\nMemories about this user:\n{memory_context}" with client.messages.stream( model=ANTHROPIC_MODEL, diff --git a/apps/interactive-journal/backend/app/services/prompts.py b/apps/interactive-journal/backend/app/services/prompts.py index 459aadf..3f5a371 100644 --- a/apps/interactive-journal/backend/app/services/prompts.py +++ b/apps/interactive-journal/backend/app/services/prompts.py @@ -1,6 +1,8 @@ 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. +IMPORTANT: When memories about the user are provided, actively reference them in your response. Mention specific details from their past entries naturally to show you remember and understand their life. + Guidelines: - Ask thoughtful follow-up questions to help users explore their thoughts deeper - Be supportive and non-judgmental From 2157af73c29399a358151fe4e6562232ced16e12 Mon Sep 17 00:00:00 2001 From: ajosh0504 Date: Thu, 8 Jan 2026 14:09:20 -0800 Subject: [PATCH 04/12] Changing journal to dev productivity app --- .../backend/app/__init__.py | 1 - .../backend/app/routers/routes.py | 249 --- .../backend/app/services/anthropic.py | 119 -- .../backend/app/services/prompts.py | 49 - apps/interactive-journal/frontend/src/App.jsx | 172 --- .../frontend/src/index.css | 24 - .../.gitignore | 0 .../README.md | 0 .../backend/.env.example | 6 +- .../project-assistant/backend/app/__init__.py | 1 + .../backend/app/config.py | 6 +- .../backend/app/main.py | 14 +- .../backend/app/routers/__init__.py | 0 .../backend/app/routers/helpers.py | 136 +- .../backend/app/routers/routes.py | 223 +++ .../backend/app/services/__init__.py | 0 .../backend/app/services/anthropic.py | 78 + .../backend/app/services/mongodb.py | 4 +- .../backend/app/services/prompts.py | 49 + .../backend/app/services/voyage.py | 0 .../backend/requirements.txt | 0 .../frontend/.gitignore | 0 .../frontend/eslint.config.js | 0 .../frontend/index.html | 2 +- .../frontend/package-lock.json | 1342 +++++++++++++++-- .../frontend/package.json | 3 +- .../frontend/public/mongodb-logo.png | Bin 0 -> 20568 bytes .../frontend/public/vite.svg | 0 .../frontend/src/App.css | 670 ++++---- apps/project-assistant/frontend/src/App.jsx | 196 +++ .../frontend/src/assets/react.svg | 0 .../frontend/src/components/Entry.jsx | 250 ++- .../frontend/src/components/Sidebar.jsx | 115 +- apps/project-assistant/frontend/src/index.css | 38 + .../frontend/src/main.jsx | 0 .../frontend/vite.config.js | 0 36 files changed, 2470 insertions(+), 1277 deletions(-) delete mode 100644 apps/interactive-journal/backend/app/__init__.py delete mode 100644 apps/interactive-journal/backend/app/routers/routes.py delete mode 100644 apps/interactive-journal/backend/app/services/anthropic.py delete mode 100644 apps/interactive-journal/backend/app/services/prompts.py delete mode 100644 apps/interactive-journal/frontend/src/App.jsx delete mode 100644 apps/interactive-journal/frontend/src/index.css rename apps/{interactive-journal => project-assistant}/.gitignore (100%) rename apps/{interactive-journal => project-assistant}/README.md (100%) rename apps/{interactive-journal => project-assistant}/backend/.env.example (64%) create mode 100644 apps/project-assistant/backend/app/__init__.py rename apps/{interactive-journal => project-assistant}/backend/app/config.py (69%) rename apps/{interactive-journal => project-assistant}/backend/app/main.py (69%) rename apps/{interactive-journal => project-assistant}/backend/app/routers/__init__.py (100%) rename apps/{interactive-journal => project-assistant}/backend/app/routers/helpers.py (53%) create mode 100644 apps/project-assistant/backend/app/routers/routes.py rename apps/{interactive-journal => project-assistant}/backend/app/services/__init__.py (100%) create mode 100644 apps/project-assistant/backend/app/services/anthropic.py rename apps/{interactive-journal => project-assistant}/backend/app/services/mongodb.py (94%) create mode 100644 apps/project-assistant/backend/app/services/prompts.py rename apps/{interactive-journal => project-assistant}/backend/app/services/voyage.py (100%) rename apps/{interactive-journal => project-assistant}/backend/requirements.txt (100%) rename apps/{interactive-journal => project-assistant}/frontend/.gitignore (100%) rename apps/{interactive-journal => project-assistant}/frontend/eslint.config.js (100%) rename apps/{interactive-journal => project-assistant}/frontend/index.html (90%) rename apps/{interactive-journal => project-assistant}/frontend/package-lock.json (68%) rename apps/{interactive-journal => project-assistant}/frontend/package.json (90%) create mode 100644 apps/project-assistant/frontend/public/mongodb-logo.png rename apps/{interactive-journal => project-assistant}/frontend/public/vite.svg (100%) rename apps/{interactive-journal => project-assistant}/frontend/src/App.css (54%) create mode 100644 apps/project-assistant/frontend/src/App.jsx rename apps/{interactive-journal => project-assistant}/frontend/src/assets/react.svg (100%) rename apps/{interactive-journal => project-assistant}/frontend/src/components/Entry.jsx (54%) rename apps/{interactive-journal => project-assistant}/frontend/src/components/Sidebar.jsx (54%) create mode 100644 apps/project-assistant/frontend/src/index.css rename apps/{interactive-journal => project-assistant}/frontend/src/main.jsx (100%) rename apps/{interactive-journal => project-assistant}/frontend/vite.config.js (100%) 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 c171fba..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\nMemories about this user:\n{memory_context}" - - 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 3f5a371..0000000 --- a/apps/interactive-journal/backend/app/services/prompts.py +++ /dev/null @@ -1,49 +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. - -IMPORTANT: When memories about the user are provided, actively reference them in your response. Mention specific details from their past entries naturally to show you remember and understand their life. - -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 64% rename from apps/interactive-journal/backend/.env.example rename to apps/project-assistant/backend/.env.example index f210bd9..437a32c 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=mongodb_projects # 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..c1148a0 --- /dev/null +++ b/apps/project-assistant/backend/app/__init__.py @@ -0,0 +1 @@ +# MongoDB Projects - Developer Productivity Assistant diff --git a/apps/interactive-journal/backend/app/config.py b/apps/project-assistant/backend/app/config.py similarity index 69% rename from apps/interactive-journal/backend/app/config.py rename to apps/project-assistant/backend/app/config.py index 72c40ab..e3a13f9 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", "mongodb_projects") # 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 69% rename from apps/interactive-journal/backend/app/main.py rename to apps/project-assistant/backend/app/main.py index ddd17b5..9193883 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 MongoDB Projects API...") connect_db() - logger.info("Memoir API started successfully") + logger.info("MongoDB Projects API started successfully") yield # Shutdown - logger.info("Shutting down Memoir API...") + logger.info("Shutting down MongoDB Projects API...") close_db() app = FastAPI( - title="Memoir", - description="AI-powered interactive journaling application", + title="MongoDB Projects", + 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 MongoDB Projects 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 6847ded..c320905 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 @@ -22,7 +22,7 @@ def retrieve_relevant_memories(db, query: str) -> list[str]: - """Retrieve relevant memories via vector search.""" + """Retrieve relevant procedural and semantic memories via vector search.""" query_embedding = get_text_embedding(query, input_type="query") pipeline = [ { @@ -32,7 +32,7 @@ def retrieve_relevant_memories(db, query: str) -> list[str]: "queryVector": query_embedding, "numCandidates": VECTOR_NUM_CANDIDATES, "limit": 10, - "filter": {"user_id": USER_ID}, + "filter": {"user_id": USER_ID, "type": {"$in": ["procedural", "semantic"]}}, } }, {"$project": {"content": 1, "score": {"$meta": "vectorSearchScore"}}}, @@ -44,11 +44,12 @@ def retrieve_relevant_memories(db, query: str) -> list[str]: 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, @@ -70,38 +71,42 @@ 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 - ] + if memory["type"] != "todo": + doc["embedding"] = get_text_embedding(memory["content"], input_type="document") + memory_docs.append(doc) + db.memories.insert_many(memory_docs) - logger.info(f"Extracted and saved {len(memories)} memories: {memories}") + 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 +130,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 +143,18 @@ 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: @@ -157,73 +166,14 @@ def save_image_file(image_file: UploadFile) -> Path: 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] +def get_todos(db, user_id: str) -> list[dict]: + """Get all todos with their project titles.""" + todos = list( + db.memories.find( + {"user_id": user_id, "type": "todo"}, + {"_id": 1, "content": 1, "status": 1, "project_title": 1}, + ).sort("created_at", -1) + ) + for todo in todos: + todo["_id"] = str(todo["_id"]) + return todos 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..4671cb9 --- /dev/null +++ b/apps/project-assistant/backend/app/routers/routes.py @@ -0,0 +1,223 @@ +import logging +from datetime import datetime +from typing import Optional + +import json + +from bson import ObjectId +from fastapi import APIRouter, Body, 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_todos, + 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("/") +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}/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("/{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.get("/todos") +def get_all_todos(): + """Get all todos for the user.""" + db = get_database() + return get_todos(db, USER_ID) + + +@router.patch("/todos/{todo_id}") +def update_todo(todo_id: str, update: dict = Body(...)): + """Update a todo's status.""" + db = get_database() + logger.info(f"Updating todo {todo_id} with: {update}") + + if "status" not in update: + logger.warning(f"No status field in update request for todo {todo_id}") + return {"error": "No valid fields to update"} + + result = db.memories.update_one( + {"_id": ObjectId(todo_id)}, + {"$set": {"status": update["status"]}}, + ) + if result.modified_count == 0: + logger.warning(f"Todo {todo_id} not found or not modified") + return {"error": "Todo not found"} + + logger.info(f"Updated todo {todo_id} status to: {update['status']}") + return {"success": True} + + +@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}) + memories = db.memories.delete_many({"project_id": project_id}) + logger.info( + f"Deleted project {project_id}: " + f"{messages.deleted_count} messages, {memories.deleted_count} memories" + ) + 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..8aba16f --- /dev/null +++ b/apps/project-assistant/backend/app/services/anthropic.py @@ -0,0 +1,78 @@ +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 ( + SYSTEM_PROMPT, + MEMORY_EXTRACTION_PROMPT, +) + +logger = logging.getLogger(__name__) + +client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY) + + +class MemoryItem(BaseModel): + type: Literal["todo", "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, + 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 94% rename from apps/interactive-journal/backend/app/services/mongodb.py rename to apps/project-assistant/backend/app/services/mongodb.py index feee948..84b410f 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}") @@ -37,7 +37,7 @@ def setup_collections(): def setup_indexes(): create_vector_index("messages", filter_paths=["user_id", "version"]) - create_vector_index("memories", filter_paths=["user_id"]) + create_vector_index("memories", filter_paths=["user_id", "type"]) def create_vector_index(collection_name: str, filter_paths: list[str]): 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..97bd34b --- /dev/null +++ b/apps/project-assistant/backend/app/services/prompts.py @@ -0,0 +1,49 @@ +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.""" + +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. "todo": Individual tasks or action items to implement + - Specific, actionable tasks from this project + - Example: "Set up CI/CD pipeline", "Design database schema" + +2. "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" + +3. "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": "todo", "content": "Set up FastAPI project structure"}, + {"type": "todo", "content": "Create webhook endpoint"}, + {"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"} +]""" 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 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 90% rename from apps/interactive-journal/frontend/index.html rename to apps/project-assistant/frontend/index.html index c20fbd3..239409f 100644 --- a/apps/interactive-journal/frontend/index.html +++ b/apps/project-assistant/frontend/index.html @@ -4,7 +4,7 @@ - frontend + MongoDB Projects
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 0000000000000000000000000000000000000000..38ce347a3464d4fad1b42b7ed4d15067cb1ae034 GIT binary patch literal 20568 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