diff --git a/AGENT_SERVER_INTEGRATION_GUIDE.md b/AGENT_SERVER_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..eca1ee7 --- /dev/null +++ b/AGENT_SERVER_INTEGRATION_GUIDE.md @@ -0,0 +1,617 @@ +# Senior Developer Guide: Agents & Server Integration + +## Executive Summary + +The Tides monorepo has built sophisticated infrastructure but hasn't fully connected the intelligence layer. This guide explains how `@apps/agents/` and `@apps/server/` should work together to deliver AI-powered productivity insights through the MCP protocol. + +## Current Architecture Analysis + +```mermaid +graph TB + Mobile[Mobile App
React Native] --> Server[MCP Server
@apps/server] + Desktop[Desktop Client
Claude/MCP] --> Server + + Server --> D1[(Cloudflare D1
Database)] + Server --> R2[(Cloudflare R2
Storage)] + + Agents[Agents Layer
@apps/agents] --> D1 + Agents --> R2 + + subgraph "Current State" + Server -.->|"Not Connected"| Agents + end + + subgraph "Agents Directory" + HelloAgent[HelloAgent
Demo/Testing] + ProductivityAgent[TideProductivityAgent
AI Analysis] + end + + style Agents fill:#ffcccc + style Server fill:#ccffcc +``` + +**Problem**: The server handles MCP tools but doesn't leverage agent intelligence. Mobile app has tool configuration but gets basic responses without AI insights. + +## Target Integration Architecture + +```mermaid +sequenceDiagram + participant Mobile as Mobile App + participant Server as MCP Server + participant Agent as TideProductivityAgent + participant AI as Workers AI + + Mobile->>Server: tide_get_report() + + Note over Server: Basic tool execution + Server->>Server: Generate basic report + + Note over Server: Check for agent enhancement + Server->>Agent: enhance(basicReport, userContext) + + Agent->>AI: Analyze patterns + insights + AI-->>Agent: AI recommendations + + Agent-->>Server: Enhanced report + insights + Server-->>Mobile: Rich response with AI analysis + + Note over Mobile: Display insights UI +``` + +## Implementation Strategy + +### Phase 1: Agent-Aware Server (Week 1) + +#### 1.1 Server Enhancement Detection + +**File: `/apps/server/src/handlers/tools.ts`** + +```typescript +interface ToolEnhancement { + agentType: 'TideProductivityAgent' | 'HelloAgent'; + enhancementMethods: string[]; + fallbackOnError: boolean; +} + +const TOOL_ENHANCEMENTS: Record = { + tide_get_report: { + agentType: 'TideProductivityAgent', + enhancementMethods: ['analyzeProductivity', 'generateInsights'], + fallbackOnError: true + }, + tide_flow: { + agentType: 'TideProductivityAgent', + enhancementMethods: ['optimizeSchedule', 'energyAnalysis'], + fallbackOnError: true + } +}; +``` + +#### 1.2 Enhanced Tool Execution + +```typescript +async function executeEnhancedTool( + toolName: string, + params: any, + userContext: AuthContext, + env: Env +) { + // 1. Execute basic MCP tool + const basicResult = await executeBasicTool(toolName, params, userContext, env); + + // 2. Check for agent enhancement + const enhancement = TOOL_ENHANCEMENTS[toolName]; + if (!enhancement) { + return basicResult; + } + + try { + // 3. Route to agent + const agentId = env[getAgentBinding(enhancement.agentType)] + .idFromName(userContext.userId); + const agent = env[getAgentBinding(enhancement.agentType)].get(agentId); + + // 4. Get AI enhancement + const enhancedResult = await agent.fetch(new Request('', { + method: 'POST', + body: JSON.stringify({ + action: 'enhance', + toolName, + basicResult, + params, + userContext + }) + })); + + const agentData = await enhancedResult.json(); + + return { + ...basicResult, + agentEnhanced: true, + insights: agentData.insights, + recommendations: agentData.recommendations, + confidence: agentData.confidence + }; + + } catch (error) { + console.warn(`Agent enhancement failed for ${toolName}:`, error); + + if (enhancement.fallbackOnError) { + return basicResult; // Graceful degradation + } + throw error; + } +} +``` + +### Phase 2: Agent Intelligence Layer (Week 2) + +#### 2.1 TideProductivityAgent Enhancement Interface + +**File: `/apps/agents/tide-productivity-agent/handlers/enhancement.ts`** + +```typescript +export class EnhancementHandler { + async enhanceTideReport( + basicReport: any, + userContext: any, + mcpClient: MCPClient + ): Promise { + // 1. Get comprehensive user data + const rawData = await mcpClient.callTool('tide_get_raw_json', {}); + + // 2. AI analysis using MCP prompts + const insights = await Promise.all([ + this.aiAnalyzer.analyzeWithPrompt('productivity_insights', rawData), + this.aiAnalyzer.analyzeWithPrompt('optimize_energy', rawData), + this.aiAnalyzer.analyzeWithPrompt('analyze_tide', rawData) + ]); + + // 3. Generate actionable recommendations + const recommendations = await this.generateRecommendations(insights, userContext); + + return { + insights: { + productivityPatterns: insights[0], + energyOptimization: insights[1], + tideAnalysis: insights[2] + }, + recommendations: recommendations, + confidence: this.calculateConfidence(insights), + metadata: { + analysisVersion: '1.0', + generatedAt: new Date().toISOString(), + dataPoints: rawData.tides.length + } + }; + } + + async enhanceTideFlow( + basicFlow: any, + userContext: any, + mcpClient: MCPClient + ): Promise { + // Similar pattern for flow optimization + // ... + } +} +``` + +#### 2.2 Agent Request Router + +**File: `/apps/agents/tide-productivity-agent/agent.ts`** + +```typescript +export class TideProductivityAgent implements DurableObject { + private enhancementHandler: EnhancementHandler; + + async fetch(request: Request): Promise { + const url = new URL(request.url); + const body = await request.json(); + + switch (body.action) { + case 'enhance': + return this.handleEnhancement(body); + + case 'analyze': + return this.handleDirectAnalysis(body); + + default: + return this.handleLegacyRequests(url, body); + } + } + + private async handleEnhancement(body: any): Promise { + const { toolName, basicResult, params, userContext } = body; + + let enhancement; + switch (toolName) { + case 'tide_get_report': + enhancement = await this.enhancementHandler + .enhanceTideReport(basicResult, userContext, this.mcpClient); + break; + + case 'tide_flow': + enhancement = await this.enhancementHandler + .enhanceTideFlow(basicResult, userContext, this.mcpClient); + break; + + default: + throw new Error(`No enhancement available for tool: ${toolName}`); + } + + return new Response(JSON.stringify(enhancement), { + headers: { 'Content-Type': 'application/json' } + }); + } +} +``` + +### Phase 3: Mobile UI Enhancement (Week 3) + +#### 3.1 Enhanced Tool Configuration + +**File: `/apps/mobile/src/config/toolsConfig.ts`** + +```typescript +export interface ToolConfig { + title: string; + description: string; + category: string; + requiresAI?: boolean; + agentEnhanced?: boolean; // NEW + insightsEnabled?: boolean; // NEW + requiredParams: ToolParameter[]; + optionalParams: ToolParameter[]; + triggers: string[]; +} + +export const TOOLS_CONFIG: Record = { + tide_get_report: { + title: "Generate Report", + description: "Generate analytics reports with AI insights", + category: "Analytics & Data", + agentEnhanced: true, // NEW + insightsEnabled: true, // NEW + requiresAI: true, // NEW + // ... existing config + }, + + tide_flow: { + title: "Start Flow", + description: "Begin focused work session with AI optimization", + category: "Core Tides", + agentEnhanced: true, // NEW + insightsEnabled: true, // NEW + // ... existing config + } + // ... +}; +``` + +#### 3.2 Enhanced Response Handling + +**File: `/apps/mobile/src/context/MCPContext.tsx`** + +```typescript +const executeToolWithInsights = useCallback(async ( + toolName: string, + params: any +): Promise => { + try { + setIsExecutingTool(true); + + const response = await mcpService.callTool(toolName, params); + + // Handle agent-enhanced responses + if (response.agentEnhanced && response.insights) { + // Update insights state for UI + setLastInsights({ + toolName, + insights: response.insights, + recommendations: response.recommendations, + confidence: response.confidence, + timestamp: new Date().toISOString() + }); + + // Trigger insights display + showInsightsModal(response.insights); + } + + return response; + } catch (error) { + console.error('Enhanced tool execution failed:', error); + throw error; + } finally { + setIsExecutingTool(false); + } +}, [mcpService]); +``` + +#### 3.3 Insights Display Components + +**File: `/apps/mobile/src/components/insights/InsightsModal.tsx`** + +```typescript +interface AgentInsights { + productivityPatterns?: any; + energyOptimization?: any; + tideAnalysis?: any; +} + +interface InsightsModalProps { + visible: boolean; + insights: AgentInsights; + recommendations: string[]; + confidence: number; + onClose: () => void; +} + +export const InsightsModal: React.FC = ({ + visible, + insights, + recommendations, + confidence, + onClose +}) => { + return ( + + + + {/* AI Confidence Indicator */} + + 🤖 AI Insights + + + + {/* Productivity Patterns */} + {insights.productivityPatterns && ( + + )} + + {/* Energy Optimization */} + {insights.energyOptimization && ( + + )} + + {/* Recommendations */} + + + + + + + ); +}; +``` + +## Data Flow Architecture + +```mermaid +flowchart TD + A[Mobile App] -->|MCP Tool Request| B[Server Router] + B -->|Basic Execution| C[MCP Tool Handler] + C -->|Raw Result| D{Agent Enhancement?} + + D -->|No| E[Return Basic Result] + D -->|Yes| F[Route to Agent] + + F --> G[TideProductivityAgent] + G -->|Analyze| H[Workers AI] + G -->|Fetch Data| I[MCP Client] + + H -->|AI Insights| J[Enhancement Handler] + I -->|User Data| J + + J -->|Enhanced Result| K[Server Response] + K -->|Rich Data + Insights| L[Mobile UI] + + L --> M[Insights Modal] + L --> N[Recommendations] + L --> O[Basic Tool Result] + + style G fill:#e1f5fe + style H fill:#f3e5f5 + style M fill:#e8f5e8 +``` + +## Error Handling & Fallback Strategy + +### Graceful Degradation Pattern + +```mermaid +graph TD + Start[Tool Request] --> Execute[Execute Basic Tool] + Execute --> Check{Agent Available?} + + Check -->|Yes| Enhance[Enhance with Agent] + Check -->|No| Fallback[Return Basic Result] + + Enhance --> Success{Enhancement Success?} + Success -->|Yes| Rich[Return Rich Response] + Success -->|No| Log[Log Error] + Log --> Fallback + + Rich --> End[User Gets Insights] + Fallback --> End2[User Gets Basic Result] + + style Rich fill:#c8e6c9 + style Fallback fill:#ffecb3 + style Log fill:#ffcdd2 +``` + +### Implementation + +```typescript +// Circuit breaker pattern for agent failures +class AgentCircuitBreaker { + private failureCount = 0; + private lastFailureTime = 0; + private readonly threshold = 5; + private readonly timeout = 60000; // 1 minute + + async callAgent(agentCall: () => Promise): Promise { + if (this.isOpen()) { + console.warn('Circuit breaker open, skipping agent call'); + return null; + } + + try { + const result = await agentCall(); + this.reset(); + return result; + } catch (error) { + this.recordFailure(); + throw error; + } + } + + private isOpen(): boolean { + return this.failureCount >= this.threshold && + (Date.now() - this.lastFailureTime) < this.timeout; + } + + private recordFailure(): void { + this.failureCount++; + this.lastFailureTime = Date.now(); + } + + private reset(): void { + this.failureCount = 0; + } +} +``` + +## Monitoring & Observability + +### Key Metrics to Track + +```mermaid +graph LR + subgraph "Performance Metrics" + A[Agent Response Time
Target: <500ms] + B[Enhancement Success Rate
Target: >95%] + C[Fallback Frequency
Alert: >5%] + end + + subgraph "Business Metrics" + D[User Engagement
With Insights] + E[Recommendation
Acceptance Rate] + F[Feature Adoption
Rate] + end + + subgraph "System Health" + G[Agent Availability
Target: 99.9%] + H[Memory Usage
Per Agent Instance] + I[Error Rates
By Agent Type] + end +``` + +### Implementation + +```typescript +// Monitoring service +class AgentMonitoringService { + async trackAgentCall( + agentType: string, + method: string, + duration: number, + success: boolean + ) { + await env.ANALYTICS?.writeDataPoint({ + blobs: [`agent.${agentType}.${method}`], + doubles: [duration], + indexes: [success ? 'success' : 'failure'] + }); + } + + async trackUserEngagement( + userId: string, + toolName: string, + insightsViewed: boolean, + recommendationsAccepted: number + ) { + // Track business metrics + } +} +``` + +## Deployment Strategy + +### Phase 1: Server-Side Foundation (Week 1) +- [ ] Implement agent detection in server +- [ ] Add fallback mechanisms +- [ ] Deploy with feature flag (disabled) + +### Phase 2: Agent Intelligence (Week 2) +- [ ] Enhance TideProductivityAgent +- [ ] Add enhancement endpoints +- [ ] Test with server integration + +### Phase 3: Mobile Enhancement (Week 3) +- [ ] Add insights UI components +- [ ] Update tool configurations +- [ ] Implement progressive disclosure + +### Phase 4: Production Rollout (Week 4) +- [ ] Feature flag rollout (10% → 50% → 100%) +- [ ] Monitor performance and adoption +- [ ] Gather user feedback + +## Business Impact + +### Before Integration +- ✅ Basic productivity tracking +- ✅ Data collection and storage +- ❌ Limited actionable insights +- ❌ No intelligent recommendations + +### After Integration +- ✅ AI-powered productivity analysis +- ✅ Personalized optimization suggestions +- ✅ Pattern recognition and trends +- ✅ Proactive workflow recommendations +- ✅ Enhanced user engagement + +### Success Metrics +- **User Engagement**: +40% time spent in app +- **Feature Adoption**: 70% of users view insights +- **Productivity Improvement**: User-reported 25% efficiency gains +- **Technical Performance**: <500ms agent response times + +## Technical Debt Considerations + +### Immediate Technical Debt +- **Latency Impact**: Agent calls add 200-500ms to responses +- **Complexity**: Multiple failure modes to handle +- **State Management**: Agent instances need lifecycle management + +### Long-term Scalability +- **Agent Clustering**: Multiple instances per user type +- **Caching Strategy**: Redis for frequent agent responses +- **Event-Driven**: Move to pub/sub for async enhancements + +### Mitigation Strategy +- Implement comprehensive monitoring from day 1 +- Use feature flags for gradual rollout +- Plan for caching layer in Q2 2025 +- Design for eventual event-driven architecture + +## Conclusion + +This integration transforms Tides from a basic tracking tool into an intelligent productivity platform. The phased approach ensures reliability while delivering immediate business value through AI-powered insights. + +The key to success is maintaining the principle of **progressive enhancement** - basic functionality always works, intelligence layer adds value when available. \ No newline at end of file diff --git a/CHAT_CONTEXT_REFACTOR.md b/CHAT_CONTEXT_REFACTOR.md new file mode 100644 index 0000000..673b500 --- /dev/null +++ b/CHAT_CONTEXT_REFACTOR.md @@ -0,0 +1,252 @@ +# Chat Context Refactor + +**Files to focus on:** +- `apps/mobile/src/context/ChatContext.tsx` +- `apps/mobile/src/components/chat/ChatInput.tsx` - for the secnding messages to agent endpoint functionality + +## Summary + +`apps/mobile/src/context/ChatContext.tsx` needs a refactor + +## Situation Analysis + +## Current ChatContext.tsx notes + +_at commit `fe7dd4d76d8695490b61a4c4c473dc9c90640e8e`_ +_focus file: `apps/mobile/src/context/ChatContext.tsx`_ + +- I like the `agentStatus` (line 36) for error handling, but it might be redundant with `mcpConnectionStatus` (line 32), `agentConnectionStatus` (line 33) and isLoading (line 25) +- Regarding `messages` (line 24) are we just holding all the messages that are shown on the screen? Should we think of Tides as a context window/conversationr rather than a daily flow? +- Maybe the Chat Context responsibiilities should be split apart from the Tides Context responsibiities? + +### Example New Context Format + +``` +TimeContext (device time/location - stable) +├── TideContext (NEW - tide data & conversation persistence) + ├── ChatContext (NEW - pure UI communication layer) +``` + +TIME CONTEXT (`apps/mobile/src/context/TimeContext.tsx`) - Time & Location from user device. Context compoennt is currently stable. _If Tides is a conversation, TIME CONTEXT is the outside world_ + +TIDE CONTEXT - Current Tide Messages from the User, the CLoudflare Worker, and from the System (Mobile Client). These messages should be held in sessions torage or soemthign similar. Maybe add a functionality to clear the curretn tide and create a new daily tide by clicking a "New Tide" button that can go ont eh top right of `Home.tsx`. _If Tides is a conversation, TIDE CONTEXT is the room that the conversation takes place in_ + +CHAT CONTEXT - The context could be responsible for "Is TimeContext loaded/ready?" and then "Is TideContext loaded/ready?" and then "are we connected to the worker/server?". And then ChatCOntext is ultimately responsible for sharing relevant/useful inforamtion to the Tides Server/Agent Entrypoint. + +## Ultimate Goals + +The Component responsible for the Agent/Worker Entrypoint connection (lets use `apps/mobile/src/components/chat/ChatInput.tsx` as an example for now) should only import functions and parameters from the CHAT CONTEXT + +- All relevant info from TIME CONTEXT and TIDE CONTEXT are held and handled by CHAT CONTEXT which feeds right into ChatInput +- The info from TIDE CONTEXT and TIDE CONTEXT needs to be held by the ChatInput because the ChatInput will be passing all info the the "Tides" k2 Storage in Cloudflare int eh 006 ENV as well as Messages to teh Agen/Worker entrypoint. + +**It's easier to scale down what we are passing to the agent entrypoint rather than scale up** + +## Service Layer Analysis + +### agentService.ts - AI Communication Layer + +**Primary Responsibilities:** + +- AI conversation endpoint integration (`/ai/conversation`) +- Tool intent classification and routing +- Session and conversation ID management (lines 54-56) +- Enhanced conversation context with message history (lines 340-346) +- Fallback mechanisms for AI service unavailability (lines 380-392) +- User ID extraction from API keys for hybrid auth + +**Key Methods:** + +- `sendMessage()` - Main AI conversation interface +- `sendConversationMessage()` - Enhanced AI endpoint with context +- `classifyToolIntent()` - Natural language to tool routing +- `executeMCPTool()` - Direct MCP tool execution bridge + +### mcpService.ts - MCP Protocol Layer + +**Primary Responsibilities:** + +- JSON-RPC 2.0 MCP protocol implementation +- Direct tide tool execution (tide_create, tide_list, etc.) +- Hierarchical flow management via `startSmartFlow()` (lines 305-325) +- Context-aware operations with automatic daily tide creation +- Connection health monitoring and retry logic + +**Key Methods:** + +- `tool()` - Generic MCP tool caller +- `startSmartFlow()` - Always uses hierarchical flow system (ADR-003 compliant) +- `getOrCreateTide()` - Automatic context management +- `switchContext()` - Navigate between daily/weekly/monthly views + +## Addiitonal Notes + +- **Message Persistence**: Conversation history maintained per tide context + +**Implementation Implications:** + +- Messages scoped to current Daily Tide, not global chat +- Context switching preserves conversation history per daily tide +- A User can now have multiple dialy tides +- "New Tide" functionality creates the new daily tide/fresh converation +- The CHatMessages will only shwo messages from teh current Tide in the Tide Context +- Existing daily flow patterns continue working unchanged + +## Gameplan + +**TideContext (NEW):** + +- Current tide context management (daily/weekly/monthly) baked in but not focus +- Conversation message persistence per tide +- "New Tide" creation for fresh conversation contexts +- Session storage for tide-scoped conversations + +**ChatContext (NEW - Clean Implementation):** + +- Pure UI state management (single consolidated connection state) +- Agent communication orchestration (agentService integration) +- Tool execution coordination (mcpService bridge) +- Clean interface for ChatInput component + +**ChatInput Integration Pattern:** + +```typescript +// Single import pattern (Option 1) +const { sendMessage, isLoading, isConnected } = useChat(); +// ChatContext orchestrates TideContext + TimeContext internally +``` + +--- + +# ADDENDUM: Post-Implementation Analysis & Next Steps + +**Status:** ✅ **Integration Complete - Working Successfully** +**Date:** 2025-09-08 +**Agent Communication:** Fully functional end-to-end + +## ✅ Successfully Implemented + +### Bridge Layer Pattern Working +- **useChatMessaging.ts** - Bridge layer successfully coordinates all contexts +- **ChatInput.tsx** - Self-contained component with full send functionality +- **Agent endpoint** - Receiving proper data structure: + ```json + { + "user_id": "...", + "tide_id": "tide_1757313819957_unpv4v4t959", + "message": "user input", + "context": { + "tide_tool": "selectedTool", // optional + "tide": { /* full tide object */ }, + "tideContext": "daily", + "timeContext": { "timestamp": "...", "timezone": "..." }, + "conversationHistory": [/* recent messages */], + "toolSuggestions": ["createTide", "getTideList"] + } + } + ``` + +### K2 Storage Structure Confirmed +**Tide Object in K2:** +```json +{ + "id": "tide_1757313819957_unpv4v4t959", + "name": "Daily Focus - Sep 8, 2025", + "description": "Automatically created daily tide for 2025-09-08", + "created_at": "2025-09-08T06:43:39.957Z", + "status": "active", + "flow_sessions": [], + "energy_updates": [], + "task_links": [] +} +``` + +**Messages Storage:** +- Currently in AsyncStorage: `tide_messages_tide_1757313819957_unpv4v4t959` +- 4 messages successfully persisted locally +- Agent responses with suggested tools working + +## 🎯 Next Priority: Agent Response Processing + +### Current Gap Analysis +**✅ What's Working:** +- Agent receives full rich context +- Agent responds with content and suggested tools +- Messages stored locally per tide +- Tide metadata persists in K2 + +**❌ Missing Functionality:** +- Agent responses not updating K2 tide object +- Tool calls/suggestions not persisting to `flow_sessions` +- Energy insights not saved to `energy_updates` +- Task management not reflected in `task_links` +- Rich agent interactions not updating tide state + +### Proposed Agent Response Handlers + +**1. Tool Execution Handler** +```typescript +// When agent suggests tools, execute via MCP and update tide +if (response.suggestedTools) { + await mcpService.updateFlowSession(tideId, { + tools_suggested: response.suggestedTools, + conversation_context: message + }); +} +``` + +**2. Energy Analysis Handler** +```typescript +// When agent analyzes user energy/mood +if (response.energyInsight) { + await mcpService.addEnergyToTide(tideId, response.energyInsight.level, { + agent_analysis: response.energyInsight.analysis, + timestamp: new Date().toISOString() + }); +} +``` + +**3. Task Link Handler** +```typescript +// When agent helps with task management +if (response.taskRecommendations) { + for (const task of response.taskRecommendations) { + await mcpService.linkTaskToTide(tideId, task.url, task.title, 'agent_suggested'); + } +} +``` + +## 🚀 Immediate Next Steps + +### Phase 1: Message Display (Priority 1) +- Display agent responses with proper formatting + +### Phase 2: Agent Response Processing (Priority 2) +- Add response parsing logic to extract structured data +- Implement K2 tide object updates via MCP service calls +- Sync agent insights back to tide state (flow_sessions, energy_updates, task_links) + +### Phase 3: Enhanced UI Integration +- Display suggested tools as actionable buttons +- Show energy insights in UI +- Integrate task links with tide workflow + +## 🏗️ Technical Implementation Notes + +**Message Flow Architecture:** +``` +User Input → ChatInput → Bridge Layer → Agent Service → Agent Response + ↓ ↓ +AsyncStorage (messages) ← TideContext ← Response Handler → K2 Storage (tide updates) +``` + +**Key Integration Points:** +- Bridge layer handles all context aggregation +- Agent service manages AI communication +- Response handlers update K2 storage via MCP service +- TideContext maintains local message persistence +- ChatContext manages pure UI state + +--- + +**Status:** Ready to implement message display and agent response processing diff --git a/CLAUDE.md b/CLAUDE.md index 661061d..73cb730 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,12 +1,6 @@ # Tides -## Tides Monorepo - -**Tides** - MCP ecosystem with: - -- **Server** (`apps/server/`): Cloudflare Workers MCP server -- **Mobile** (`apps/mobile/`): React Native workflow tracker -- **Architecture**: Mobile → HTTP/JSON-RPC 2.0 → MCP Server → Cloudflare D1/R2 +MCP ecosystem with React Native mobile app, Cloudflare Workers server, and JSON-RPC 2.0 protocol. ### Structure @@ -25,33 +19,36 @@ tides/ ### Commands -**DO NOT TRY TO RUN AN EMUALTOR YOURSELF, ASK FOR ME TO DO IT AND ALL MANUAL TESTING** +**DO NOT TRY TO RUN AN EMULATOR YOURSELF, ASK FOR ME TO DO IT AND ALL MANUAL TESTING** -#### Development +#### Server Development & Deployment ```bash -npm run dev # All apps -npm run dev:server # Server -npm run dev:mobile # Mobile -npm run dev:web # Web +# Development +npm run dev # Server dev with logging +npm run dev:prod # Dev against prod environment +npm run dev:staging # Dev against staging environment + +# Testing +npm run test # All server tests +npm run test:unit # Unit tests only +npm run test:integration # Integration tests only +npm run test:e2e # End-to-end tests + +# Deployment +npm run deploy:prod # Deploy to env.001 +npm run deploy:staging # Deploy to env.002 +npm run deploy:dev # Deploy to env.003 + +# Monitoring +npm run monitor:simple # Basic health check +npm run monitor:live # Real-time logs ``` -#### Testing +#### Mobile Development ```bash -npm run test # All tests -npm run test:server # Server tests -npm run test:mobile # Mobile tests -npm run test:web # Web tests -``` - -#### Deployment - -```bash -npm run build # Build all -npm run build:server # Deploy server -npm run build:mobile:android -npm run build:mobile:ios +npm start # React Native dev server ``` ### Tech Stack @@ -73,18 +70,21 @@ npm run build:mobile:ios **Data Storage:** -- **Primary**: Cloudflare D1 (SQL) + R2 (Object Storage) - - D1: User auth, API keys, tide metadata - - R2: Full tide JSON data at `users/{userId}/tides/{tideId}.json` -- **Supabase**: ONLY for user authentication & initial API key generation -- **NOT in Supabase**: Tide data, flow sessions, energy levels, task links +- **D1 Databases**: Per-environment SQL storage + - `tides-001-db` (prod), `tides-002-db` (staging), `tides-003-db` (dev), `tides-006-db` (Mason dev) + - Schema: User auth, API keys, tide metadata, task links +- **R2 Buckets**: Per-environment object storage + - `tides-001-storage` (prod), `tides-002-storage` (staging), etc. + - Pattern: `users/{userId}/tides/{tideId}.json` +- **Supabase**: Authentication only (`hcfxujzqlyaxvbetyano.supabase.co`) +- **KV Namespaces**: Staging (002) and Mason dev (006) environments for auth caching + +**MCP Tools (Implemented):** -**MCP Tools:** +- `linkTideTask` - Link external tasks to tides +- `listTideTaskLinks` - List task links for a tide -1. `tide_create`, `tide_list`, `tide_flow` -2. `tide_add_energy`, `tide_link_task` -3. `tide_list_task_links`, `tide_get_report` -4. `tides_get_participants` +**Note:** Server implements comprehensive tool framework with analytics, sessions, and task management. ### Config @@ -92,17 +92,18 @@ npm run build:mobile:ios **Mobile**: Bundle ID `com.tidesmobile` **Workers Envs**: -- env.001 → `tides-001.mpazbot.workers.dev` (prod) -- env.002 → `tides-002.mpazbot.workers.dev` (staging) -- env.003 → `tides-003.mpazbot.workers.dev` (dev) +- env.001 → `tides-001.mpazbot.workers.dev` (prod) - AI binding enabled +- env.002 → `tides-002.mpazbot.workers.dev` (staging) - KV + AI, demo mode +- env.003 → `tides-003.mpazbot.workers.dev` (dev) - AI binding enabled +- env.006 → `tides-006.mpazbot.workers.dev` (Mason dev) - Supabase auth enabled ### Guidelines **Context7 Library IDs:** -- Cloudflare Workers: `/llmstxt/developers_cloudflare_com-workers-llms-full.txt` +- Cloudflare Workers: `/cloudflare/workers-sdk` - MCP patterns: `/cloudflare/mcp-server-cloudflare` -- React Native: `/facebook/react-native-website` +- React Native: `/websites/reactnative_dev` - Supabase: `/supabase/supabase` **Standards:** @@ -123,13 +124,6 @@ npm run build:mobile:ios **Mobile**: Supabase JS 2.52.1, React Navigation 7.x, AsyncStorage 2.2.0 **Package Manager**: npm throughout -### Status - -**Completed**: Monorepo, MCP foundation, mobile auth, navigation, **major mobile refactoring** (86% code reduction) -**Active**: 8 tide tools integration, hybrid auth optimization, feature expansion with maintainable codebase -**Next**: Complete MCP integration, desktop UUID/QR setup - -**Recent ADR Implementation**: ADR-004 Eliminate Active Tides System - All tools now work with context-based tides (daily/weekly/monthly) that always exist, removing dependency on user-created "active tides" **Requirements**: @@ -149,227 +143,43 @@ See `apps/server/CLAUDE.md` and `apps/mobile/CLAUDE.md` for app-specific docs. - Mobile App: - Web App: -## Tides Mobile Development +## Mobile App -**React Native MCP client for tide workflow management** +React Native 0.80.2 client connecting to MCP server at `tides-006.mpazbot.workers.dev`. -**Architecture:** React Native → JSON-RPC 2.0 → MCP Server → Supabase +### Current Implementation -### Development Process +**Architecture:** +- Auth: Supabase + API key caching with `authService` +- Communication: `agentService` with retry logic and conversation history +- State: Multiple contexts (Auth, MCP, Chat, ServerEnvironment, Tide) +- Navigation: React Navigation 7.x with type-safe routing +- Storage: AsyncStorage for auth persistence -**Required:** Query Context7 MCP first for all implementations +**Key Services:** +- `authService.ts` - Supabase auth + API key management +- `agentService.ts` - Server communication with AI conversation endpoints +- `mcpService.ts` - JSON-RPC 2.0 protocol implementation -**Context7 Library IDs:** - -- React Native: `/facebook/react-native-website` -- Supabase: `/supabase/supabase` -- TypeScript: `/microsoft/typescript` -- React Navigation: `/react-navigation/react-navigation.github.io` -- Cloudflare Workers: `/cloudflare/workers-sdk` - -### Tech Stack - -**Core:** React Native 0.80.2 (NO EXPO), React 19.1.0, TypeScript 5.0.4 - -**Auth:** Supabase with hybrid authentication: - -- Mobile: `tides_{userId}_{randomId}` API keys -- Desktop: UUID tokens - -**Navigation:** React Navigation 7.x -**Storage:** AsyncStorage -**Testing:** Jest, React Testing Library +**Server Connection:** +- Primary: `https://tides-006.mpazbot.workers.dev` (Mason dev environment) +- Auth: Bearer tokens via `tides_{userId}_{randomId}` format +- Endpoints: `/ai/conversation`, `/ai/classify-intent`, `/agents/tide-productivity/` +- Retry: 3 attempts with exponential backoff ### Configuration **Bundle ID:** com.tidesmobile -**Supabase URL:** `https://hcfxujzqlyaxvbetyano.supabase.co` +**Supabase:** `https://hcfxujzqlyaxvbetyano.supabase.co` **Anon Key:** `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhjZnh1anpxbHlheHZiZXR5YW5vIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTMwNDMyMjUsImV4cCI6MjA2ODYxOTIyNX0.5e4B-tb0orqvZdod2RanoP6O_j8j7Y8ZpjpUq30qA5Y` -### Architecture - -**Status:** 95% TypeScript coverage, full MCP integration - -**Patterns:** - -- Layered contexts: Auth → MCP → Chat → Environment -- useReducer for state management -- Singleton services with getInstance() -- React.memo optimization -- Type-safe navigation -- Token-based design system - -#### Comprehensive Folder Architecture - -``` -src/ -├── components/ # Modular UI components with memoization (REFACTORED) -│ ├── chat/ # Chat-related components (NEW) -│ │ ├── ChatInput.tsx # Message input interface -│ │ ├── ChatMessages.tsx # Messages container with empty state -│ │ └── MessageBubble.tsx # Individual message display -│ ├── tides/ # Tides display components (NEW) -│ │ ├── TidesSection.tsx # Context tides section with loading states -│ │ └── TideCard.tsx # Individual tide card with icons -│ ├── tools/ # Tool-related components (NEW) -│ │ ├── ToolMenu.tsx # Tool selection menu with animations -│ │ └── ToolCallDisplay.tsx # Tool execution display -│ ├── debug/ # Debug components (NEW) -│ │ └── DebugPanel.tsx # Debug test interface -│ ├── Auth.tsx # Authentication form with validation -│ ├── FlowSession.tsx # Complex flow session management -│ └── ServerEnvironmentSelector.tsx # Multi-environment switching -├── config/ # Environment and service configuration -│ └── supabase.ts # Supabase client configuration -├── context/ # Advanced state management with useReducer -│ ├── AuthContext.tsx # Authentication state with API key management -│ ├── MCPContext.tsx # MCP connection and tool execution -│ ├── ChatContext.tsx # Agent communication management -│ ├── ServerEnvironmentContext.tsx # Multi-environment configuration -│ ├── authTypes.ts # Auth reducer patterns and types -│ ├── mcpTypes.ts # MCP reducer patterns and types -│ └── ServerEnvironmentTypes.ts # Environment configuration types -├── design-system/ # Comprehensive design token system -│ ├── tokens.ts # Colors, typography, spacing, shadows -│ ├── components/ # Reusable UI components -│ │ ├── Button.tsx # 5 variants × 3 sizes with loading states -│ │ ├── Card.tsx # 3 variants with shadow system -│ │ ├── Text.tsx # Variant-based with font loading -│ │ ├── Input.tsx # Form inputs with validation -│ │ ├── Container.tsx # Layout containers -│ │ ├── Stack.tsx # Spacing and layout utilities -│ │ ├── SafeArea.tsx # Safe area management -│ │ ├── Loading.tsx # Loading states and indicators -│ │ ├── Notification.tsx # User feedback system -│ │ └── ErrorBoundary.tsx # Error boundary with logging -│ └── index.ts # Design system exports -├── navigation/ # Type-safe navigation architecture -│ ├── RootNavigator.tsx # Auth-gated navigation root -│ ├── AuthNavigator.tsx # Authentication flow navigation -│ ├── MainNavigator.tsx # Main app navigation with headers -│ ├── types.ts # Navigation type definitions -│ ├── hooks.ts # Type-safe navigation utilities -│ └── index.ts # Navigation exports -├── screens/ # Feature-rich screen components -│ ├── Auth/ # Authentication screens -│ │ ├── Initial.tsx # Sign-in with OAuth providers -│ │ └── CreateAccount.tsx # Registration with validation -│ └── Main/ # Main application screens -│ ├── Home.tsx # Clean orchestration layer (269 lines, refactored from 1,866) -│ └── Settings.tsx # Configuration and debug interface -├── services/ # Enterprise-grade service layer -│ ├── authService.ts # Supabase auth + API key management -│ ├── mcpService.ts # JSON-RPC 2.0 MCP client implementation -│ ├── agentService.ts # Agent communication service -│ ├── loggingService.ts # Centralized logging service -│ ├── secureStorage.ts # Secure storage utilities -│ └── index.ts # Service exports -├── types/ # Comprehensive type system -│ ├── index.ts # Central type export point -│ ├── chat.ts # Chat interface and agent communication -│ ├── mcp.ts # MCP protocol and JSON-RPC 2.0 types -│ ├── models.ts # Domain model definitions -│ ├── api-types.ts # API response contracts -│ ├── api.ts # API client types -│ ├── connection.ts # Connection state types -│ └── agents.ts # Agent service types -├── hooks/ # Custom hook patterns (ENHANCED) -│ ├── useTidesManagement.ts # Tides state & operations (NEW) -│ ├── useToolMenu.ts # Tool menu state & animations (NEW) -│ ├── useDebugPanel.ts # Debug functionality (NEW) -│ ├── useChatInput.ts # Chat input logic (NEW) -│ ├── useAsyncAction.ts # Base async operation pattern -│ ├── useAuthActions.ts # Authentication action helpers -│ ├── useAuthStatus.ts # Authentication state utilities -│ ├── useMCPConnection.ts # MCP connection management -│ └── index.ts # Hook exports -├── utils/ # Utility functions (ENHANCED) -│ ├── agentCommandUtils.ts # Agent context & execution (NEW) -│ ├── debugUtils.ts # Debug test functions (NEW) -│ └── fonts.ts # Font loading utilities -└── constants/ # Application constants - └── index.ts # Centralized constants -``` - -#### Patterns - -**Services:** Singleton with `getInstance()` -**State:** useReducer for complex state -**Performance:** React.memo + useCallback -**Contexts:** Auth, MCP, Chat, Environment - -### MCP Server Integration - -**Primary:** `https://tides-001.mpazbot.workers.dev` -**Protocol:** JSON-RPC 2.0 over HTTP -**Auth:** Bearer tokens (mobile: `tides_{userId}_{randomId}`, desktop: `{uuid}`) -**Tools:** 8 tide management functions -**Reference:** `/tides-server` - -#### MCP Tools - -1. `tide_create` - Create workflows -2. `tide_list` - List tides -3. `tide_flow` - Manage flow states -4. `tide_add_energy` - Add energy data -5. `tide_link_task` - Link tasks -6. `tide_list_task_links` - List links -7. `tide_get_report` - Generate reports -8. `tides_get_participants` - Get participants - -#### Protocol - -**Communication:** JSON-RPC 2.0 over HTTP -**Retry:** Exponential backoff -**Auth:** Hybrid Bearer tokens -**Health:** Auto health checks -**Recovery:** Auto reconnection - -### Guidelines - -**Code:** - -- TypeScript interfaces in components -- Design system components only -- Services with error handling -- NO Expo dependencies - -**Testing:** - -- Auth flows -- MCP communication -- Network error handling - -### Status - -**Complete:** - -- ✅ Auth system -- ✅ Navigation -- ✅ Supabase integration -- ✅ MCP client -- ✅ **Active tides elimination (ADR-004)** - Tools always available via context tides - -**Active:** - -- 8 tide tools integration -- Hybrid auth deployment -- JSONB optimization - -### Requirements - -1. Query Context7 MCP first -2. Test auth flows (mobile + desktop) -3. Network error handling -4. AsyncStorage for auth state -5. Cross-client compatibility - -**Storage:** JSONB over R2 complexity - -### Commands +### Development ```bash -npm start # Mobile dev -wrangler dev --local # Server dev -wrangler deploy # Deploy server +npm start # React Native dev server ``` + +**Requirements:** +1. Test auth flows with Supabase + API key fallback +2. Handle network failures gracefully with retries +3. Maintain conversation context across sessions diff --git a/ENERGY_CHART_REFACTOR.md b/ENERGY_CHART_REFACTOR.md new file mode 100644 index 0000000..903ee90 --- /dev/null +++ b/ENERGY_CHART_REFACTOR.md @@ -0,0 +1,22 @@ +# Energy Chart Refactor + +**Files to focus on:** +- `apps/mobile/src/context/ChartDisplayContext.tsx` +- `apps/mobile/src/components/NewEnergyChart.tsx` + +## Summary + +I think the new cleaned up `TimeContext.tsx` allows for a better Energy Chart + +## Style Focuses + +- I want the chart to have a lwoer opacity line that follows the exact same path running behind the 100% opacity line with an aribtrary endpoint added to the far right for a complete look - similar if not exactly like `EnergyChart.tsx` +- The path attatched to the 100% opacity line should instead bind to the lower oapcity line as the lower oapcty line will go completely across from left to right, so it will make for a better presentation. + +## ChartDisplayContext Notes + +This refactor is going to greatly affect `apps/mobile/src/components/NewEnergyChart.tsx`. Especcially how it will ract to the range of dates now availabel in `apps/mobile/src/context/ChartDisplayContext.tsx` + +## New Energy Chart Refacotr Goals + +- When the dateRange is '1day' the chart's x-axis should be 12am to \ No newline at end of file diff --git a/NOTES.md b/NOTES.md deleted file mode 100644 index 2a19ddc..0000000 --- a/NOTES.md +++ /dev/null @@ -1 +0,0 @@ -when an agent proactively does a tool, it asks permission to do so unless the tool was selected. it shoud be a prompwith the parameters created by the agent, and then the options should be "continue or edit , edit will allow you to edit the parameters, continue will submit the tides tool. this eppearas as a message udnerneath the last agent message diff --git a/README.md b/README.md index d5aab55..f6b47d1 100644 --- a/README.md +++ b/README.md @@ -285,43 +285,6 @@ graph TB 4. **Performance Optimized**: React.memo, useCallback, and efficient state management 5. **Scalable Storage**: JSONB over enterprise complexity -### Code Organization Patterns - -#### Mobile App (86% Code Reduction Achieved) - -``` -src/ -├── components/ # Modular UI components (extracted from Home.tsx) -├── hooks/ # Custom state management hooks -├── context/ # useReducer-based state management -├── services/ # Singleton service pattern -├── design-system/ # Token-based design system -└── screens/ # Clean orchestration layers -``` - -#### Server App (Domain-Driven Design) - -``` -src/ -├── handlers/ # Request handling by domain -├── tools/ # MCP tools organized by function -├── storage/ # Storage abstraction layer -├── prompts/ # AI prompt templates -└── services/ # Business logic services -``` - -#### Agents App (Service-Oriented Architecture) - -``` -agents/ -├── tide-productivity-agent/ -│ ├── services/ # Core business services -│ ├── handlers/ # Request/response handling -│ ├── types/ # Domain types -│ └── utils/ # Utility functions -└── hello/ # Reference implementation -``` - ## Performance & Scalability Considerations ### Optimization Strategies diff --git a/WHITEBOARD.md b/WHITEBOARD.md new file mode 100644 index 0000000..fecff6a --- /dev/null +++ b/WHITEBOARD.md @@ -0,0 +1,41 @@ +# Tides Chart Implementation Plan + +## Goal +Implement a line chart in `apps/mobile/src/screens/Main/Home.tsx` similar to existing examples in: +- `apps/mobile/src/components/NewEnergyChart.tsx` +- `apps/mobile/src/components/EnergyChart.tsx` + +## Chart Requirements + +**Display Elements:** +- Date display +- Line chart visualization +- X-axis labels +- Context toggle (1 day, 3 days, 1 week, 1 month, 3 months, 1 year) + +**Chart Behavior:** +- **Linear Time View**: Continuous time scale (like `EnergyChart.tsx` daily view) +- **Aggregated Points**: Fixed data points (like `EnergyChart.tsx` weekly/monthly views) +- Context-specific labels and scaling + +## Architecture Options + +**Implementation Approach:** +- Labels: Hardcoded per context (pragmatic approach) +- Date calculations: Inside component (simple, self-contained) +- Chart variations: Different per context + +**Context Management:** +1. **App-level `TimeContext`**: User's actual time and location +2. **Chart-level `TimeDisplayContext`**: Chart context state (1day, 3day, etc.) synced across: + - Date display + - Chart component + - Label rendering + - Context toggle + +## Architecture Decision + +**Question**: Single component with switch cases vs. 6 separate components? + +**Recommendation needed** for optimal organization approach. + diff --git a/WHITEBOARD2.md b/WHITEBOARD2.md new file mode 100644 index 0000000..619f476 --- /dev/null +++ b/WHITEBOARD2.md @@ -0,0 +1,48 @@ +# Context Audit - "Red Stapler" Performance Review + +I want to audit every context in `apps/mobile/App.tsx`. Current context stack has 5 layers - we might not need them all, or can add them back incrementally. + +**Current Contexts in App.tsx:** +1. ServerEnvironmentProvider +2. AuthProvider +3. MCPProvider +4. TimeContextProvider +5. ChatProvider + +## Performance Review Sessions + +*[HR sits across from each context in a dull office room]* + +**HR**: "So, what would you say you do here?" + +### ServerEnvironmentContext +*[clutches red stapler defensively]* + +"Well, I manage the server environment selection! The user can switch between dev/staging/prod MCP servers. Without me, they'd be stuck on one server forever! I provide `currentEnvironment` and `setEnvironment` to the whole app!" + +### AuthProvider +*[straightens tie nervously]* + +"I handle all authentication! Supabase sessions, API key management, user state... I'm literally the foundation! Without me, nobody gets past the login screen! I wrap the entire authenticated experience!" + +### MCPProvider +*[waves hands frantically]* + +"I manage the MCP connection! JSON-RPC 2.0 to the server, tool execution, connection state... I'm the bridge between mobile and server! The 8 tide tools depend on me! Without me, no tides functionality!" + +### TimeContextProvider +*[looks confused]* + +"I... I provide time context for the user's location and timezone? I think I'm used for... time-based features? Energy charts need me for daily/weekly/monthly views... right? Please don't fire me!" + +### ChatProvider +*[sweating profusely]* + +"I manage the agent chat system! Message history, chat state, agent communication... I'm essential for the conversational interface! Users need to talk to the AI agents through me!" + +## Audit Questions + +1. **Which contexts are actually essential?** +2. **Which can be removed or simplified?** +3. **Which have overlapping responsibilities?** +4. **Can we start minimal and add back incrementally?** \ No newline at end of file diff --git a/apps/agents/tide-productivity-agent/types/analysis.ts b/apps/agents/tide-productivity-agent/types/analysis.ts index 19b93c6..b6cdb0b 100644 --- a/apps/agents/tide-productivity-agent/types/analysis.ts +++ b/apps/agents/tide-productivity-agent/types/analysis.ts @@ -22,7 +22,6 @@ export interface AnalysisResult { export interface TideInfo { id: string; name: string; - flow_type: string; status?: string; created_at?: string; description?: string; diff --git a/apps/agents/tide-productivity-agent/utils/tide-fetcher.ts b/apps/agents/tide-productivity-agent/utils/tide-fetcher.ts index acfeb06..fa62be0 100644 --- a/apps/agents/tide-productivity-agent/utils/tide-fetcher.ts +++ b/apps/agents/tide-productivity-agent/utils/tide-fetcher.ts @@ -69,7 +69,6 @@ export class TideFetcher { return content.tides.map((tide: any) => ({ id: tide.id, name: tide.name || 'Untitled Tide', - flow_type: tide.flow_type || 'unknown', status: tide.status, created_at: tide.created_at, description: tide.description @@ -137,7 +136,6 @@ export class TideFetcher { return { id: tide.id, name: tide.name || 'Untitled Tide', - flow_type: tide.flow_type || 'unknown', status: tide.status, created_at: tide.created_at, description: tide.description @@ -165,8 +163,7 @@ export class TideFetcher { const lowerQuestion = question.toLowerCase(); for (const tide of tides) { - if (lowerQuestion.includes(tide.name.toLowerCase()) || - lowerQuestion.includes(tide.flow_type.toLowerCase())) { + if (lowerQuestion.includes(tide.name.toLowerCase())) { return tide.id; } } diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx index a7e563a..3696ceb 100644 --- a/apps/mobile/App.tsx +++ b/apps/mobile/App.tsx @@ -1,49 +1,41 @@ -// GREEN - import React from "react"; import { NavigationContainer } from "@react-navigation/native"; -import { TimeContextProvider } from "./src/context/TimeContext"; -import { ServerEnvironmentProvider } from "./src/context/ServerEnvironmentContext"; -import { AuthProvider } from "./src/context/AuthContext"; -import { MCPProvider } from "./src/context/MCPContext"; -import { ChatProvider } from "./src/context/ChatContext"; -import RootNavigator from "./src/navigation/RootNavigator"; import { KeyboardAvoidingView, Platform, View } from "react-native"; import { SafeAreaProvider, useSafeAreaInsets, } from "react-native-safe-area-context"; -import { colors } from "./src/design-system/tokens"; import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { colors } from "./src/design-system/tokens"; +import RootNavigator from "./src/navigation/RootNavigator"; + +import { AuthProvider } from "./src/context/AuthContext"; +import { MCPProvider } from "./src/context/MCPContext"; +import { TimeContextProvider } from "./src/context/TimeContext"; +import { ChatProvider } from "./src/context/ChatContext"; +import { TideProvider } from "./src/context/TideContext"; const AppContent: React.FC = () => { const insets = useSafeAreaInsets(); - return ( <> - - - - - + + + + - - - - + + + + ex.autolinkLibrariesFromCommand() } rootProject.name = 'TidesMobile' include ':app' +include ':react-native-localize' +project(':react-native-localize').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-localize/android') includeBuild('../node_modules/@react-native/gradle-plugin') diff --git a/apps/mobile/ios/Podfile b/apps/mobile/ios/Podfile index aad87c4..3cd9c65 100644 --- a/apps/mobile/ios/Podfile +++ b/apps/mobile/ios/Podfile @@ -23,6 +23,9 @@ target 'TidesMobile' do :app_path => "#{Pod::Config.instance.installation_root}/.." ) + # Manual linking for react-native-localize + pod 'RNLocalize', :path => '../node_modules/react-native-localize' + post_install do |installer| # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 react_native_post_install( diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock index 24b181a..41a457f 100644 --- a/apps/mobile/ios/Podfile.lock +++ b/apps/mobile/ios/Podfile.lock @@ -2359,6 +2359,35 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga + - RNLocalize (3.5.2): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - RNReanimated (4.1.0): - boost - DoubleConversion @@ -2742,6 +2771,7 @@ DEPENDENCIES: - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNKeychain (from `../node_modules/react-native-keychain`) + - RNLocalize (from `../node_modules/react-native-localize`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - RNSVG (from `../node_modules/react-native-svg`) @@ -2909,6 +2939,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-gesture-handler" RNKeychain: :path: "../node_modules/react-native-keychain" + RNLocalize: + :path: "../node_modules/react-native-localize" RNReanimated: :path: "../node_modules/react-native-reanimated" RNScreens: @@ -2998,6 +3030,7 @@ SPEC CHECKSUMS: RNCAsyncStorage: 767abb068db6ad28b5f59a129fbc9fab18b377e2 RNGestureHandler: cdca641e24d0ab743dcd90a24de4e6259e6aa0de RNKeychain: f9022b0a123bb459ba2e6045f79203a7c7e54956 + RNLocalize: 99cfa0ece4586b0e249592836c598ecaf9a1e8bc RNReanimated: 3d16d7db5b36d76df0477df0723d28d2235c97f8 RNScreens: f50530f3288ada9391f240467bd3ca5ca22a67a0 RNSVG: 432ca012e24410cab11449afef353ce20573a59f @@ -3005,6 +3038,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 1c52fbd270869e556504def7e94fffbf67f53f7b -PODFILE CHECKSUM: 53c69396d3e9ce9df21aa988c95d8bbef522ed55 +PODFILE CHECKSUM: 4d4da7e80d0b733d1045bd62c144d209097d8a71 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/apps/mobile/ios/TidesMobile/Info.plist b/apps/mobile/ios/TidesMobile/Info.plist index 0b708c7..6dcf808 100644 --- a/apps/mobile/ios/TidesMobile/Info.plist +++ b/apps/mobile/ios/TidesMobile/Info.plist @@ -62,6 +62,19 @@ UIViewControllerBasedStatusBarAppearance + CFBundleLocalizations + + en + es + fr + de + it + pt + zh-Hans + zh-Hant + ja + ko + UIAppFonts Inter-Italic-VariableFont_opsz,wght.ttf diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index 4ee2e1b..1baf201 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -23,6 +23,7 @@ "react-native-gesture-handler": "^2.28.0", "react-native-graph": "^1.1.0", "react-native-keychain": "^10.0.0", + "react-native-localize": "^3.5.2", "react-native-markdown-display": "^7.0.2", "react-native-reanimated": "^4.1.0", "react-native-redash": "^18.1.3", @@ -12135,6 +12136,26 @@ "node": ">=16" } }, + "node_modules/react-native-localize": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-3.5.2.tgz", + "integrity": "sha512-HfQdwv5sRjh4AQ8a97OTjXYcxPNRlBxiQb861c7Ob6mRuNYCPtaJ45QTcZxZr31vAM3THvtOBp1soqWlQFxjnA==", + "license": "MIT", + "peerDependencies": { + "@expo/config-plugins": "^9.0.0 || ^10.0.0", + "react": "*", + "react-native": "*", + "react-native-macos": "*" + }, + "peerDependenciesMeta": { + "@expo/config-plugins": { + "optional": true + }, + "react-native-macos": { + "optional": true + } + } + }, "node_modules/react-native-markdown-display": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/react-native-markdown-display/-/react-native-markdown-display-7.0.2.tgz", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 55e3d3e..dd8839e 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -25,6 +25,7 @@ "react-native-gesture-handler": "^2.28.0", "react-native-graph": "^1.1.0", "react-native-keychain": "^10.0.0", + "react-native-localize": "^3.5.2", "react-native-markdown-display": "^7.0.2", "react-native-reanimated": "^4.1.0", "react-native-redash": "^18.1.3", diff --git a/apps/mobile/src/components/ChartHeader.tsx b/apps/mobile/src/components/ChartHeader.tsx new file mode 100644 index 0000000..2cd6008 --- /dev/null +++ b/apps/mobile/src/components/ChartHeader.tsx @@ -0,0 +1,48 @@ +import { StyleSheet, View } from "react-native"; +import React from "react"; +import { Text } from "./Text"; +import { typography } from "../design-system"; +import { useChartDisplayContext } from "../context/ChartDisplayContext"; + +// ChartHeader: Displays time range labels from ChartDisplayContext +// - headerTopLabel: Primary display (e.g., "Today", "Last Week", "Mon - Wed") +// - headerBottomLabel: Date range (e.g., "Aug 31st", "Aug 29th - Aug 31st") +const ChartHeader = () => { + const { headerTopLabel, headerBottomLabel } = useChartDisplayContext(); + + return ( + + {headerTopLabel} + {headerBottomLabel} + + ); +}; + +export default ChartHeader; + +const styles = StyleSheet.create({ + header: { + width: "100%", + marginBottom: 16, + alignItems: "flex-start", + paddingHorizontal: 16, + }, + topDate: { + fontSize: typography.fontSize.largeTitle, + color: "white", + fontWeight: typography.fontWeight.medium, + lineHeight: typography.lineHeight.largeTitle, + letterSpacing: typography.letterSpacing.inter( + typography.fontSize.largeTitle + ), + }, + bottomDate: { + fontSize: typography.fontSize.largeTitle, + color: "white", + fontWeight: typography.fontWeight.semibold, + lineHeight: typography.lineHeight.largeTitle, + letterSpacing: typography.letterSpacing.inter( + typography.fontSize.largeTitle + ), + }, +}); diff --git a/apps/mobile/src/components/chat/ChatMessages.tsx b/apps/mobile/src/components/ChatMessages.tsx similarity index 63% rename from apps/mobile/src/components/chat/ChatMessages.tsx rename to apps/mobile/src/components/ChatMessages.tsx index 9758fa9..08867f7 100644 --- a/apps/mobile/src/components/chat/ChatMessages.tsx +++ b/apps/mobile/src/components/ChatMessages.tsx @@ -1,9 +1,9 @@ import React, { forwardRef } from "react"; import { ScrollView, StyleSheet } from "react-native"; -import { Stack } from "../Stack"; +import { Stack } from "./Stack"; import { MessageBubble } from "./MessageBubble"; -import { spacing } from "../../design-system/tokens"; -import type { ChatMessage } from "../../types/chat"; +import { spacing } from "../design-system/tokens"; +import type { ChatMessage } from "../types/chat"; interface ChatMessagesProps { messages: ChatMessage[]; @@ -12,20 +12,10 @@ interface ChatMessagesProps { export const ChatMessages = forwardRef( ({ messages }, ref) => { return ( - - - {messages.map((message) => ( - + + + {messages.map((message, index) => ( + ))} @@ -42,7 +32,7 @@ const styles = StyleSheet.create({ paddingRight: 11, // borderWidth: 1, // borderColor: "blue", - height: 50, + flex: 1, }, messagesContent: { paddingBottom: spacing[4], diff --git a/apps/mobile/src/components/ContextToggle.tsx b/apps/mobile/src/components/ContextToggle.tsx deleted file mode 100644 index 2624b25..0000000 --- a/apps/mobile/src/components/ContextToggle.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import React, { useState } from "react"; -import { - TouchableOpacity, - View, - Modal, - TouchableWithoutFeedback, - Pressable, -} from "react-native"; -import { ChartLine, Sun, Waves, Moon, Lock } from "lucide-react-native"; -import { colors } from "../design-system/tokens"; -import { useTimeContext, TimeContextType } from "../context/TimeContext"; -import { Text } from "./Text"; - -interface ContextToggleProps { - showLabels?: boolean; - variant?: "compact" | "full"; -} - -export const ContextToggle: React.FC = ({ - showLabels = false, - variant = "compact", -}) => { - const { - currentContext, - setCurrentContext, - isAtPresent, - resetToPresent, - contextSwitchingDisabled, - } = useTimeContext(); - const [showTooltip, setShowTooltip] = useState(false); - - const contextOptions: { - label: string; - value: TimeContextType; - icon: any; - disabled?: boolean; - }[] = [ - { label: "Daily", value: "daily", icon: Sun }, - { label: "Weekly", value: "weekly", icon: Waves }, - { label: "Monthly", value: "monthly", icon: Moon }, - { label: "Project", value: "project", icon: ChartLine, disabled: true }, - ]; - - const handleTogglePress = () => { - if (contextSwitchingDisabled) return; - setShowTooltip(!showTooltip); - }; - - const handleContextSelect = async (value: TimeContextType) => { - if (contextSwitchingDisabled) return; - - if (currentContext === value && !isAtPresent) { - resetToPresent(); - } else { - await setCurrentContext(value); - } - setShowTooltip(false); - }; - - if (variant === "full") { - // Segmented control layout for energy chart - const activeOptions = contextOptions.filter((option) => !option.disabled); - - return ( - - {activeOptions.map((option) => { - const isSelected = currentContext === option.value; - const isDisabled = contextSwitchingDisabled || option.disabled; - - return ( - handleContextSelect(option.value)} - disabled={isDisabled} - style={{ - flex: 1, - alignItems: "center", - height: 28, - justifyContent: "center", - borderRadius: 6, - backgroundColor: isSelected - ? "rgba(255,255,255,0.15)" - : "transparent", - shadowColor: "#000", - shadowOffset: { - width: 0, - height: 4, - }, - shadowRadius: 20, - shadowOpacity: 0.035, - }} - > - - {option.label} - - - ); - })} - - ); - } - - // Compact modal variant (original design) - return ( - - - {(() => { - const currentOption = contextOptions.find( - (option) => option.value === currentContext - ); - const IconComponent = currentOption?.icon || Sun; - return ( - <> - - {showLabels && ( - - {currentOption?.label} - - )} - - ); - })()} - - - - setShowTooltip(false)}> - - - {contextOptions.map((option) => { - const IconComponent = option.icon; - const isSelected = currentContext === option.value; - const isDisabled = contextSwitchingDisabled || option.disabled; - - return ( - handleContextSelect(option.value)} - disabled={isDisabled} - style={{ - paddingVertical: 10, - paddingBottom: 7.5, - borderRadius: 8, - backgroundColor: isSelected - ? colors.primary[100] - : "transparent", - alignItems: "center", - width: 64, - opacity: isDisabled ? 0.7 : 1.0, - }} - > - - - {option.disabled && ( - - )} - - {option.label} - - - - ); - })} - - - - - - ); -}; diff --git a/apps/mobile/src/components/EnergyChart.tsx b/apps/mobile/src/components/EnergyChart.tsx index 89e7a90..f76e73c 100644 --- a/apps/mobile/src/components/EnergyChart.tsx +++ b/apps/mobile/src/components/EnergyChart.tsx @@ -1,1427 +1,1427 @@ -import { StyleSheet, Alert, Clipboard, View } from "react-native"; -import React, { useMemo, useEffect } from "react"; -import { useTimeContext } from "../context/TimeContext"; -import { useLocationData } from "../hooks/useLocationData"; -import * as SunCalc from "suncalc"; -import { - Canvas, - Path, - Skia, - Circle, - Group, - Shadow, -} from "@shopify/react-native-skia"; -import { curveBasis, line, scaleLinear, curveCardinal } from "d3"; -import { useSharedValue, withTiming } from "react-native-reanimated"; -import { colors, Text } from "../design-system"; - -// ✅ TUTORIAL COMPARISON: Missing scalePoint import for proper x-axis scaling -// Current implementation uses scaleLinear for both axes, but tutorial uses scalePoint for x-axis - -interface ChartDataPoint { - x: number; - y: number; - label: string; - timestamp: string; - originalLevel: string | number; - isGenerated?: boolean; // Optional flag for generated points -} - -type TideContext = "daily" | "weekly" | "monthly"; - -type Props = { - data: ChartDataPoint[]; // ✅ REQUIREMENT 2: Sample data structure - context?: TideContext; - chartHeight: number; // ✅ REQUIREMENT 1: Defined size of chart and canvas (height) - chartMargin: number; // ✅ REQUIREMENT 1: Defined size of chart and canvas (margin) - chartWidth: number; // ✅ REQUIREMENT 1: Defined size of chart and canvas (width) -}; - -const EnergyChart = ({ data, chartHeight, chartMargin, chartWidth }: Props) => { - // Validate chart dimensions for scalability - if (chartWidth <= 0 || chartHeight <= 0) { - console.warn("EnergyChart: Invalid chart dimensions", { - chartWidth, - chartHeight, - }); - return null; - } - - if (data.length === 0) { - return null; // Gracefully handle empty data - } - const { currentContext, dateOffset } = useTimeContext(); - const { locationInfo } = useLocationData(); - - // Calculate time range based on current context and date offset - const getTimeRange = () => { - const now = new Date(); - const offsetDate = new Date(now); - - if (currentContext === "daily") { - offsetDate.setDate(now.getDate() - dateOffset); - const start = new Date(offsetDate); - start.setHours(0, 0, 0, 0); - const end = new Date(offsetDate); - end.setHours(23, 59, 59, 999); - return [start.getTime(), end.getTime()]; - } else if (currentContext === "weekly") { - const weekStart = new Date(offsetDate); - const dayOfWeek = weekStart.getDay(); - weekStart.setDate(weekStart.getDate() - dayOfWeek - dateOffset * 7); - weekStart.setHours(0, 0, 0, 0); - const weekEnd = new Date(weekStart); - weekEnd.setDate(weekStart.getDate() + 6); - weekEnd.setHours(23, 59, 59, 999); - return [weekStart.getTime(), weekEnd.getTime()]; - } else if (currentContext === "monthly") { - const monthStart = new Date( - offsetDate.getFullYear(), - offsetDate.getMonth() - dateOffset, - 1 - ); - monthStart.setHours(0, 0, 0, 0); - - const monthEnd = new Date( - offsetDate.getFullYear(), - offsetDate.getMonth() - dateOffset + 1, - 0 // Day 0 of next month = last day of current month - ); - monthEnd.setHours(23, 59, 59, 999); - - return [monthStart.getTime(), monthEnd.getTime()]; - } else { - // For project context, show all data - return data.length > 0 - ? [Math.min(...data.map((d) => d.x)), Math.max(...data.map((d) => d.x))] - : [0, 1]; - } - }; - - const [startTime, endTime] = getTimeRange(); - - // Calculate sunrise/sunset for the current date being displayed - const getSunTimes = useMemo(() => { - if (!locationInfo.latitude || !locationInfo.longitude) return null; - - const targetDate = new Date(); - if (currentContext === "daily") { - targetDate.setDate(targetDate.getDate() - dateOffset); - return SunCalc.getTimes( - targetDate, - locationInfo.latitude, - locationInfo.longitude - ); - } else { - return null; // No sun times for weekly/monthly/project - } - }, [locationInfo, currentContext, dateOffset]); - - const animationLine = useSharedValue(0); - - // Calculate animation progress based on current time and context - useEffect(() => { - // Reset animation to prevent "already working" error - animationLine.value = 0; - - // Small delay to prevent animation conflicts - const timer = setTimeout(() => { - // Animate all contexts - they all get the same smooth drawing animation - animationLine.value = withTiming(1, { duration: 600 }); - }, 0); - - return () => clearTimeout(timer); - }, [currentContext, dateOffset, startTime, endTime]); - - // Filter data to show only points within the time range for non-project contexts - const filteredData = - currentContext === "project" - ? data - : data.filter((d) => d.x >= startTime && d.x <= endTime); - - // Process data for different contexts - const processedData = useMemo(() => { - if (currentContext === "daily" && filteredData.length > 0) { - // For daily context: add a starting point at midnight - const dayStart = new Date(startTime); - - // For current day animation, filter out future data points - let dataToUse = filteredData; - if (dateOffset === 0) { - const now = new Date(); - dataToUse = filteredData.filter((point) => point.x <= now.getTime()); - } - - // Use the first data point's energy level or a default of 6 (medium) - const startingEnergyLevel = dataToUse.length > 0 ? dataToUse[0].y : 6; - - const startingPoint: ChartDataPoint = { - x: dayStart.getTime(), - y: startingEnergyLevel, - label: "Day start", - timestamp: dayStart.toISOString(), - originalLevel: startingEnergyLevel, - isGenerated: true, // Flag to identify this as a generated point - }; - - // For current day, add current time endpoint if needed - if (dateOffset === 0 && dataToUse.length > 0) { - const now = new Date(); - const lastDataPoint = dataToUse[dataToUse.length - 1]; - - // Add current time point with last known energy level - const currentTimePoint: ChartDataPoint = { - x: now.getTime(), - y: lastDataPoint.y, // Use last energy level - label: "Current time", - timestamp: now.toISOString(), - originalLevel: lastDataPoint.y, - isGenerated: true, - }; - - dataToUse = [...dataToUse, currentTimePoint]; - } - - // Combine starting point with actual data - const combinedData = [startingPoint, ...dataToUse]; - - // Sort by time to ensure proper line drawing - return combinedData.sort((a, b) => a.x - b.x); - } - - if (currentContext === "weekly" && filteredData.length > 0) { - // For weekly context: group by day of week (hard-coded positions) - const weeklyData = new Map(); - - // Initialize all 7 days of the week - for (let i = 0; i < 7; i++) { - weeklyData.set(i, []); - } - - filteredData.forEach((point) => { - const dayOfWeek = new Date(point.x).getDay(); // 0 = Sunday, 1 = Monday, etc. - weeklyData.get(dayOfWeek)!.push(point); - }); - - // Only create points for days that have actual data - const weeklyAverages: ChartDataPoint[] = []; - const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - - for (let dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) { - const dayPoints = weeklyData.get(dayOfWeek)!; - - // Only add points for days with actual data - if (dayPoints.length > 0) { - // Has data - show actual average - const avgEnergy = - dayPoints.reduce((sum, point) => sum + point.y, 0) / - dayPoints.length; - - // Use actual day timestamp (noon of that day) for proper alignment with current time - const weekStart = new Date(startTime); - const dayTimestamp = new Date(weekStart); - dayTimestamp.setDate(weekStart.getDate() + dayOfWeek); - dayTimestamp.setHours(12, 0, 0, 0); // Noon of that day - - weeklyAverages.push({ - x: dayTimestamp.getTime(), - y: avgEnergy, - label: `${dayNames[dayOfWeek]} avg: ${avgEnergy.toFixed(1)}`, - timestamp: dayTimestamp.toISOString(), - originalLevel: avgEnergy.toFixed(1), - }); - } - // Skip days with no data - don't add any point - } - - console.log("Weekly averages by day:", weeklyAverages.length, "points"); - - // Add invisible edge points for continuous line if Sunday/Saturday missing - if (weeklyAverages.length > 0) { - const hasSunday = weeklyAverages.some((point) => - point.label.startsWith("Sun") - ); - const hasSaturday = weeklyAverages.some((point) => - point.label.startsWith("Sat") - ); - - // Add invisible Sunday point if missing (use first available day's energy) - if (!hasSunday) { - const firstPoint = weeklyAverages[0]; - const weekStart = new Date(startTime); - const sundayTimestamp = new Date(weekStart); - sundayTimestamp.setDate(weekStart.getDate() + 0); // Sunday (day 0) - sundayTimestamp.setHours(12, 0, 0, 0); - - weeklyAverages.unshift({ - x: sundayTimestamp.getTime(), - y: firstPoint.y, - label: `Sun edge: ${firstPoint.y.toFixed(1)}`, - timestamp: sundayTimestamp.toISOString(), - originalLevel: firstPoint.y.toFixed(1), - isGenerated: true, // Invisible edge point - }); - } - - // Add invisible Saturday point if missing (use last available day's energy) - if (!hasSaturday) { - const lastPoint = weeklyAverages[weeklyAverages.length - 1]; - const weekStart = new Date(startTime); - const saturdayTimestamp = new Date(weekStart); - saturdayTimestamp.setDate(weekStart.getDate() + 6); // Saturday (day 6) - saturdayTimestamp.setHours(12, 0, 0, 0); - - weeklyAverages.push({ - x: saturdayTimestamp.getTime(), - y: lastPoint.y, - label: `Sat edge: ${lastPoint.y.toFixed(1)}`, - timestamp: saturdayTimestamp.toISOString(), - originalLevel: lastPoint.y.toFixed(1), - isGenerated: true, // Invisible edge point - }); - } - - // Sort by position to ensure proper line drawing - weeklyAverages.sort((a, b) => a.x - b.x); - } - - // For current week (dateOffset === 0), add current time endpoint - if (dateOffset === 0 && weeklyAverages.length > 0) { - const now = new Date(); - const lastDataPoint = weeklyAverages[weeklyAverages.length - 1]; - - const currentTimePoint: ChartDataPoint = { - x: now.getTime(), - y: lastDataPoint.y, - label: "Current time", - timestamp: now.toISOString(), - originalLevel: lastDataPoint.y, - isGenerated: true, - }; - weeklyAverages.push(currentTimePoint); - } - - return weeklyAverages; - } - - if (currentContext !== "monthly" || filteredData.length === 0) { - return filteredData; - } - - // For monthly context: calculate 3-day rolling average for each day that has data - console.log( - "Monthly context - filteredData:", - filteredData.length, - "points" - ); - - // Group data by date first - const dailyData = new Map(); - - filteredData.forEach((point) => { - const dateKey = new Date(point.x).toDateString(); - if (!dailyData.has(dateKey)) { - dailyData.set(dateKey, []); - } - dailyData.get(dateKey)!.push(point); - }); - - console.log("Monthly daily groups:", Array.from(dailyData.keys())); - - // Calculate daily averages for days with data - const dailyAverages = new Map(); // dayOfMonth -> average energy - for (const [dateKey, dayPoints] of dailyData.entries()) { - const avgEnergy = - dayPoints.reduce((sum, point) => sum + point.y, 0) / dayPoints.length; - const dayOfMonth = new Date(dateKey).getDate(); - dailyAverages.set(dayOfMonth, avgEnergy); - console.log( - `Day ${dayOfMonth}: ${avgEnergy.toFixed(1)} (from ${ - dayPoints.length - } points)` - ); - } - - // Calculate total days in month for positioning - const monthStart = new Date(startTime); - const monthEnd = new Date(endTime); - const totalDays = Math.ceil( - (monthEnd.getTime() - monthStart.getTime()) / (1000 * 60 * 60 * 24) - ); - - // Apply 3-day rolling average and position according to actual timestamps - const monthlyPoints: ChartDataPoint[] = []; - const now = new Date(); - - for (const [dayOfMonth, dailyAvg] of dailyAverages.entries()) { - // For current month, only include days up to today - const dayTimestamp = new Date( - monthStart.getFullYear(), - monthStart.getMonth(), - dayOfMonth, - 12, - 0, - 0 - ); - if (dateOffset === 0 && dayTimestamp.getTime() > now.getTime()) { - continue; // Skip future days in current month - } - - // Calculate 3-day rolling average (day-1, day, day+1) - let sum = 0; - let count = 0; - - for (let offset = -1; offset <= 1; offset++) { - const checkDay = dayOfMonth + offset; - if (dailyAverages.has(checkDay)) { - sum += dailyAverages.get(checkDay)!; - count++; - } - } - - const rollingAvg = count > 0 ? sum / count : dailyAvg; - - monthlyPoints.push({ - x: dayTimestamp.getTime(), - y: rollingAvg, - label: `Day ${dayOfMonth}: ${rollingAvg.toFixed(1)}`, - timestamp: dayTimestamp.toISOString(), - originalLevel: rollingAvg.toFixed(1), - }); - } - - // Add month start point for continuous line (like daily midnight start) - if (monthlyPoints.length > 0) { - const firstDataPoint = monthlyPoints[0]; - const monthStartPoint: ChartDataPoint = { - x: startTime, - y: firstDataPoint.y, - label: "Month start", - timestamp: new Date(startTime).toISOString(), - originalLevel: firstDataPoint.y, - isGenerated: true, - }; - monthlyPoints.unshift(monthStartPoint); - - // For current month (dateOffset === 0), add current time endpoint - if (dateOffset === 0) { - const now = new Date(); - const lastDataPoint = monthlyPoints[monthlyPoints.length - 1]; - - const currentTimePoint: ChartDataPoint = { - x: now.getTime(), - y: lastDataPoint.y, - label: "Current time", - timestamp: now.toISOString(), - originalLevel: lastDataPoint.y, - isGenerated: true, - }; - monthlyPoints.push(currentTimePoint); - } - } - - console.log("Monthly rolling averages:", monthlyPoints.length, "points"); - return monthlyPoints.sort((a, b) => a.x - b.x); - }, [currentContext, filteredData]); - - // Chart scaling domains and ranges (memoized to prevent re-renders) - // ✅ For daily context: use full 24-hour range to position data at actual times - const xDomain = useMemo(() => { - if (currentContext === "daily") { - // Always use full day range for proper time positioning - return [startTime, endTime]; // 00:00 to 23:59 of the selected day - } else if (currentContext === "weekly") { - // Always use full week range for proper day positioning - return [startTime, endTime]; // Full week regardless of which days have data - } else if (currentContext === "monthly") { - // Always use full month range for proper day positioning - return [startTime, endTime]; // Full month regardless of which days have data - } else { - // For project: use min/max of actual data - return processedData.length > 0 - ? [ - Math.min(...processedData.map((d) => d.x)), - Math.max(...processedData.map((d) => d.x)), - ] - : [startTime, endTime]; - } - }, [currentContext, processedData, startTime, endTime]); - - // ✅ REQUIREMENT 4 & 9: Y-domain fixed to 0-10 for consistent energy level scaling - const yDomain = [0, 10]; // Always use full energy scale range - - // ✅ REQUIREMENT 3: D3 scales for mapping data to pixels - // ❌ REQUIREMENT 5 & 7: Should use scalePoint for x-axis (discrete time points), currently using scaleLinear - const xScale = useMemo( - () => scaleLinear().domain(xDomain).range([0, chartWidth]), // ✅ REQUIREMENT 6: Range for x-axis (pixel space) - start at beginning - [xDomain, chartWidth] - ); - - // ✅ REQUIREMENT 11: Y-scale created using scaleLinear mapping values from yDomain to yRange - const yScale = useMemo( - () => scaleLinear().domain(yDomain).range([chartHeight, 0]), // ✅ REQUIREMENT 10: Range for y-axis from chartHeight to 0 (inverted) - use full height - [chartHeight] // yDomain is now constant [0, 10] - ); - - // Generate full background line (left to right across entire chart) - const fullBackgroundLine = useMemo(() => { - if (processedData.length === 0) return null; - - // For background line, exclude current time endpoints to avoid the "turn around" effect - const backgroundData = processedData.filter( - (point) => !point.isGenerated || point.label !== "Current time" - ); - - // Create extended data that spans full chart width - const extendedData = [...backgroundData]; - - // Add starting point at left edge - natural extension from first point - if (backgroundData.length > 0 && backgroundData[0].x > xDomain[0]) { - extendedData.unshift({ - ...backgroundData[0], - x: xDomain[0], - y: backgroundData[0].y, // Use first point's energy for natural lead-in - isGenerated: true, - }); - } - - // Add ending point at right edge - natural extension from last point - if ( - backgroundData.length > 0 && - backgroundData[backgroundData.length - 1].x < xDomain[1] - ) { - extendedData.push({ - ...backgroundData[backgroundData.length - 1], - x: xDomain[1], - y: backgroundData[backgroundData.length - 1].y, // Use last point's energy for natural lead-out - isGenerated: true, - }); - } - - return line() - .x((d) => xScale(d.x)) - .y((d) => yScale(d.y)) - .curve(curveCardinal.tension(0))(extendedData); - }, [processedData, xScale, yScale, xDomain]); - - // Generate filled area below the background line - const backgroundFillPath = useMemo(() => { - if (processedData.length === 0) return null; - - // For background line, exclude current time endpoints to avoid the "turn around" effect - const backgroundData = processedData.filter( - (point) => !point.isGenerated || point.label !== "Current time" - ); - - // Create extended data that spans full chart width - const extendedData = [...backgroundData]; - - // Add starting point at left edge - if (backgroundData.length > 0 && backgroundData[0].x > xDomain[0]) { - extendedData.unshift({ - ...backgroundData[0], - x: xDomain[0], - y: backgroundData[0].y, - isGenerated: true, - }); - } - - // Add ending point at right edge - if ( - backgroundData.length > 0 && - backgroundData[backgroundData.length - 1].x < xDomain[1] - ) { - extendedData.push({ - ...backgroundData[backgroundData.length - 1], - x: xDomain[1], - y: backgroundData[backgroundData.length - 1].y, - isGenerated: true, - }); - } - - // Create the line path first - const linePath = line() - .x((d) => xScale(d.x)) - .y((d) => yScale(d.y)) - .curve(curveCardinal.tension(0))(extendedData); - - if (!linePath) return null; - - // Convert to fill by adding bottom edge points - const fillPathString = `${linePath} L${xScale( - xDomain[1] - )},${chartHeight} L${xScale(xDomain[0])},${chartHeight} Z`; - - try { - return Skia.Path.MakeFromSVGString(fillPathString); - } catch (error) { - console.warn("Failed to create background fill path:", error); - return null; - } - }, [processedData, xScale, yScale, xDomain, chartHeight]); - - // Generate daylight filled area below the background line (daily context only) - const daylightFillPath = useMemo(() => { - if ( - currentContext !== "daily" || - !getSunTimes?.sunrise || - !getSunTimes?.sunset - ) { - return null; - } - - const sunriseTime = getSunTimes.sunrise.getTime(); - const sunsetTime = getSunTimes.sunset.getTime(); - - // Calculate 7px buffer zones in time units - const bufferTimeMs = (7 / chartWidth) * (xDomain[1] - xDomain[0]); - const sunriseBufferEnd = sunriseTime + bufferTimeMs; - const sunsetBufferStart = sunsetTime - bufferTimeMs; - - // Use background data processing or create default flat line if no data - const backgroundData = processedData.filter( - (point) => !point.isGenerated || point.label !== "Current time" - ); - - let extendedData = [...backgroundData]; - - // If no data, create a flat line at energy level 5 (medium) for backgrounds to follow - if (backgroundData.length === 0) { - extendedData = [ - { - x: xDomain[0], - y: 5, - label: "default start", - timestamp: "", - originalLevel: 5, - isGenerated: true, - }, - { - x: xDomain[1], - y: 5, - label: "default end", - timestamp: "", - originalLevel: 5, - isGenerated: true, - }, - ]; - } else { - // Add starting point at left edge - if (backgroundData.length > 0 && backgroundData[0].x > xDomain[0]) { - extendedData.unshift({ - ...backgroundData[0], - x: xDomain[0], - y: backgroundData[0].y, - isGenerated: true, - }); - } - - // Add ending point at right edge - if ( - backgroundData.length > 0 && - backgroundData[backgroundData.length - 1].x < xDomain[1] - ) { - extendedData.push({ - ...backgroundData[backgroundData.length - 1], - x: xDomain[1], - y: backgroundData[backgroundData.length - 1].y, - isGenerated: true, - }); - } - } - - // Find or interpolate the energy level at sunrise and sunset times - const findEnergyAtTime = (targetTime: number) => { - // Find the closest points before and after the target time - const beforePoint = extendedData.filter((p) => p.x <= targetTime).pop(); - const afterPoint = extendedData.find((p) => p.x >= targetTime); - - if (beforePoint && afterPoint && beforePoint.x !== afterPoint.x) { - // Linear interpolation - const ratio = - (targetTime - beforePoint.x) / (afterPoint.x - beforePoint.x); - return beforePoint.y + ratio * (afterPoint.y - beforePoint.y); - } else if (beforePoint) { - return beforePoint.y; - } else if (afterPoint) { - return afterPoint.y; - } - return 5; // Default middle energy level - }; - - // Create daylight data with 7px buffer zones - const sunriseBufferY = findEnergyAtTime(sunriseBufferEnd); - const sunsetBufferY = findEnergyAtTime(sunsetBufferStart); - - // Daylight area (shrunk by 7px buffer on each side) - let daylightData = [ - { - x: sunriseBufferEnd, - y: sunriseBufferY, - label: "sunrise buffer end", - timestamp: "", - originalLevel: sunriseBufferY, - }, - ]; - - // Add actual data points within daylight hours (excluding buffer zones) - const innerPoints = extendedData.filter( - (point) => point.x > sunriseBufferEnd && point.x < sunsetBufferStart - ); - daylightData.push(...innerPoints); - - // Add sunset buffer boundary point - daylightData.push({ - x: sunsetBufferStart, - y: sunsetBufferY, - label: "sunset buffer start", - timestamp: "", - originalLevel: sunsetBufferY, - }); - - if (daylightData.length < 2) { - return null; // Need at least sunrise and sunset points - } - - // Create the daylight line path - const daylightLinePath = line() - .x((d) => xScale(d.x)) - .y((d) => yScale(d.y)) - .curve(curveCardinal.tension(0))(daylightData); - - if (!daylightLinePath) return null; - - // Create the fill path: line path + bottom edge (shrunk daylight area) - const fillPathString = `${daylightLinePath} L${xScale( - sunsetBufferStart - )},${chartHeight} L${xScale(sunriseBufferEnd)},${chartHeight} Z`; - - try { - return Skia.Path.MakeFromSVGString(fillPathString); - } catch (error) { - console.warn("Failed to create daylight fill path:", error); - return null; - } - }, [processedData, xScale, yScale, currentContext, getSunTimes, chartHeight]); - - // Generate sunrise buffer zone fill path (7px transition area) - const sunriseBufferFillPath = useMemo(() => { - if ( - currentContext !== "daily" || - !getSunTimes?.sunrise - ) { - return null; - } - - const sunriseTime = getSunTimes.sunrise.getTime(); - const bufferTimeMs = (7 / chartWidth) * (xDomain[1] - xDomain[0]); - const sunriseBufferEnd = sunriseTime + bufferTimeMs; - - // Find energy levels at buffer boundaries using same helper as daylight - const backgroundData = processedData.filter( - (point) => !point.isGenerated || point.label !== "Current time" - ); - const extendedData = [...backgroundData]; - - if (backgroundData.length > 0 && backgroundData[0].x > xDomain[0]) { - extendedData.unshift({ - ...backgroundData[0], - x: xDomain[0], - y: backgroundData[0].y, - isGenerated: true, - }); - } - - if ( - backgroundData.length > 0 && - backgroundData[backgroundData.length - 1].x < xDomain[1] - ) { - extendedData.push({ - ...backgroundData[backgroundData.length - 1], - x: xDomain[1], - y: backgroundData[backgroundData.length - 1].y, - isGenerated: true, - }); - } - - const findEnergyAtTime = (targetTime: number) => { - const beforePoint = extendedData.filter((p) => p.x <= targetTime).pop(); - const afterPoint = extendedData.find((p) => p.x >= targetTime); - - if (beforePoint && afterPoint && beforePoint.x !== afterPoint.x) { - const ratio = - (targetTime - beforePoint.x) / (afterPoint.x - beforePoint.x); - return beforePoint.y + ratio * (afterPoint.y - beforePoint.y); - } else if (beforePoint) { - return beforePoint.y; - } else if (afterPoint) { - return afterPoint.y; - } - return 5; - }; - - const sunriseY = findEnergyAtTime(sunriseTime); - const sunriseBufferY = findEnergyAtTime(sunriseBufferEnd); - - // Create buffer zone data - const bufferData = [ - { x: sunriseTime, y: sunriseY }, - { x: sunriseBufferEnd, y: sunriseBufferY }, - ]; - - // Create the buffer line path - const bufferLinePath = line() - .x((d) => xScale(d.x)) - .y((d) => yScale(d.y)) - .curve(curveCardinal.tension(0))(bufferData); - - if (!bufferLinePath) return null; - - // Create fill path for sunrise buffer - const fillPathString = `${bufferLinePath} L${xScale( - sunriseBufferEnd - )},${chartHeight} L${xScale(sunriseTime)},${chartHeight} Z`; - - try { - return Skia.Path.MakeFromSVGString(fillPathString); - } catch (error) { - console.warn("Failed to create sunrise buffer fill path:", error); - return null; - } - }, [ - processedData, - xScale, - yScale, - currentContext, - getSunTimes, - chartHeight, - xDomain, - ]); - - // Generate sunset buffer zone fill path (7px transition area) - const sunsetBufferFillPath = useMemo(() => { - if ( - currentContext !== "daily" || - !getSunTimes?.sunset - ) { - return null; - } - - const sunsetTime = getSunTimes.sunset.getTime(); - const bufferTimeMs = (7 / chartWidth) * (xDomain[1] - xDomain[0]); - const sunsetBufferStart = sunsetTime - bufferTimeMs; - - // Find energy levels at buffer boundaries using same helper as daylight - const backgroundData = processedData.filter( - (point) => !point.isGenerated || point.label !== "Current time" - ); - const extendedData = [...backgroundData]; - - if (backgroundData.length > 0 && backgroundData[0].x > xDomain[0]) { - extendedData.unshift({ - ...backgroundData[0], - x: xDomain[0], - y: backgroundData[0].y, - isGenerated: true, - }); - } - - if ( - backgroundData.length > 0 && - backgroundData[backgroundData.length - 1].x < xDomain[1] - ) { - extendedData.push({ - ...backgroundData[backgroundData.length - 1], - x: xDomain[1], - y: backgroundData[backgroundData.length - 1].y, - isGenerated: true, - }); - } - - const findEnergyAtTime = (targetTime: number) => { - const beforePoint = extendedData.filter((p) => p.x <= targetTime).pop(); - const afterPoint = extendedData.find((p) => p.x >= targetTime); - - if (beforePoint && afterPoint && beforePoint.x !== afterPoint.x) { - const ratio = - (targetTime - beforePoint.x) / (afterPoint.x - beforePoint.x); - return beforePoint.y + ratio * (afterPoint.y - beforePoint.y); - } else if (beforePoint) { - return beforePoint.y; - } else if (afterPoint) { - return afterPoint.y; - } - return 5; - }; - - const sunsetBufferY = findEnergyAtTime(sunsetBufferStart); - const sunsetY = findEnergyAtTime(sunsetTime); - - // Create buffer zone data - const bufferData = [ - { x: sunsetBufferStart, y: sunsetBufferY }, - { x: sunsetTime, y: sunsetY }, - ]; - - // Create the buffer line path - const bufferLinePath = line() - .x((d) => xScale(d.x)) - .y((d) => yScale(d.y)) - .curve(curveCardinal.tension(0))(bufferData); - - if (!bufferLinePath) return null; - - // Create fill path for sunset buffer - const fillPathString = `${bufferLinePath} L${xScale( - sunsetTime - )},${chartHeight} L${xScale(sunsetBufferStart)},${chartHeight} Z`; - - try { - return Skia.Path.MakeFromSVGString(fillPathString); - } catch (error) { - console.warn("Failed to create sunset buffer fill path:", error); - return null; - } - }, [ - processedData, - xScale, - yScale, - currentContext, - getSunTimes, - chartHeight, - xDomain, - ]); - - // Generate current time line (follows background path but stops at current time) - const curvedLine = useMemo(() => { - if (processedData.length === 0) return null; - - // Use IDENTICAL data processing as fullBackgroundLine - const backgroundData = processedData.filter( - (point) => !point.isGenerated || point.label !== "Current time" - ); - - // Create extended data EXACTLY like fullBackgroundLine - const extendedData = [...backgroundData]; - - // Add starting point at left edge - IDENTICAL to background line - if (backgroundData.length > 0 && backgroundData[0].x > xDomain[0]) { - extendedData.unshift({ - ...backgroundData[0], - x: xDomain[0], - y: backgroundData[0].y, // Use first point's energy for natural lead-in - isGenerated: true, - }); - } - - // Add ending point at right edge - IDENTICAL to background line - if ( - backgroundData.length > 0 && - backgroundData[backgroundData.length - 1].x < xDomain[1] - ) { - extendedData.push({ - ...backgroundData[backgroundData.length - 1], - x: xDomain[1], - y: backgroundData[backgroundData.length - 1].y, // Use last point's energy for natural lead-out - isGenerated: true, - }); - } - - // For current periods (dateOffset === 0), create a truncated version at current time - if (dateOffset === 0) { - const now = new Date(); - - // Generate the full path first, then interpolate at current time - const fullLine = line() - .x((d) => xScale(d.x)) - .y((d) => yScale(d.y)) - .curve(curveCardinal.tension(0))(extendedData); - - if (!fullLine) return null; - - // Find the Y value at current time by interpolating the background curve - const currentTimeX = xScale(now.getTime()); - - // Filter points up to current time and add interpolated endpoint - const dataUpToNow = extendedData.filter( - (point) => point.x <= now.getTime() - ); - - // Add current time point with interpolated Y value from the background curve - if (dataUpToNow.length > 0) { - const lastPoint = dataUpToNow[dataUpToNow.length - 1]; - dataUpToNow.push({ - x: now.getTime(), - y: lastPoint.y, // Use last known energy level - label: "Current time endpoint", - timestamp: now.toISOString(), - originalLevel: lastPoint.y, - isGenerated: true, - }); - } - - return line() - .x((d) => xScale(d.x)) - .y((d) => yScale(d.y)) - .curve(curveCardinal.tension(0))(dataUpToNow); - } - - // For past periods, use the full extended data (same as background) - return line() - .x((d) => xScale(d.x)) - .y((d) => yScale(d.y)) - .curve(curveCardinal.tension(0))(extendedData); - }, [processedData, xScale, yScale, dateOffset, xDomain]); - - // Convert background line to Skia path - const backgroundLinePath = useMemo(() => { - if (!fullBackgroundLine) return null; - try { - return Skia.Path.MakeFromSVGString(fullBackgroundLine); - } catch (error) { - console.warn( - "Failed to create background Skia path from SVG string:", - error - ); - return null; - } - }, [fullBackgroundLine]); - - // Convert D3 SVG string to Skia path (following tutorial pattern) - const linePath = useMemo(() => { - if (!curvedLine) return null; - try { - return Skia.Path.MakeFromSVGString(curvedLine); - } catch (error) { - console.warn("Failed to create Skia path from SVG string:", error); - return null; - } - }, [curvedLine]); - - const copyDebugInfo = () => { - const debugInfo = ` -ENERGY CHART DEBUG INFO -====================== -Context: ${currentContext} ${dateOffset > 0 ? `(-${dateOffset})` : "(current)"} -Energy Points: ${processedData.length} of ${data.length} ${ - currentContext === "monthly" ? "(3-day rolling avg)" : "" - } -X Min/Max: ${new Date(xDomain[0]).toLocaleDateString()} - ${new Date( - xDomain[1] - ).toLocaleDateString()} -Y Min/Max: [${yDomain[0]} - ${yDomain[1]}] -X Range: [0px - ${chartWidth}px] -Y Range: [${chartHeight}px - 0px] -CurvedLine: ${curvedLine ? "✅ Generated" : "❌ Failed"} -LinePath: ${linePath ? "✅ Created" : "❌ Failed"} -Processed Data: ${processedData.length} points -Chart Dimensions: ${chartWidth}x${chartHeight}, margin: ${chartMargin} -Current Time: ${new Date().toLocaleTimeString()} -Start Time: ${new Date(startTime).toLocaleTimeString()} -End Time: ${new Date(endTime).toLocaleTimeString()} -Time Progress: ${ - currentContext === "daily" && dateOffset === 0 - ? `${( - ((new Date().getTime() - startTime) / (endTime - startTime)) * - 100 - ).toFixed(1)}%` - : "100%" - } -Animation Value: ${animationLine.value.toFixed(3)} - -PROCESSED DATA POINTS: -${processedData - .map( - (d) => - `- ${new Date(d.x).toLocaleString()}: Level ${d.y.toFixed(1)} (${ - d.originalLevel - })` - ) - .join("\n")} - -RAW DATA: -${data - .map( - (d) => - `- ${new Date(d.x).toLocaleString()}: Level ${d.y} (${d.originalLevel})` - ) - .join("\n")} - `.trim(); - - Clipboard.setString(debugInfo); - Alert.alert( - "Debug Info Copied!", - "All debug information copied to clipboard" - ); - }; - - return ( - - - {/* Filled background area below the energy line */} - {backgroundFillPath && ( - <> - {currentContext === "daily" ? ( - <> - {/* Dark blue nighttime fill for daily */} - - - {/* Light blue daylight fill that follows the energy line */} - {daylightFillPath && ( - - )} - - {/* Buffer zones - middle colors between night and day */} - {sunriseBufferFillPath && ( - - )} - {sunsetBufferFillPath && ( - - )} - - ) : ( - /* Solid fill for weekly and monthly */ - - )} - - )} - - {/* Background line - 10% opacity, spans full chart from left to right */} - {backgroundLinePath && ( - - )} - - {/* Animated line - 100% opacity, fills from left to current time */} - {linePath && ( - - - {/* */} - - )} - - {/* Current time indicator for all contexts */} - {dateOffset === 0 && - (() => { - const now = new Date(); - const currentTimeX = xScale(now.getTime()); - return currentTimeX >= 0 && currentTimeX <= chartWidth ? ( - - {/* Current time line */} - - - ) : null; - })()} - - {/* Data point markers for all contexts (excluding generated points) - always show all dots */} - {processedData - .filter((point) => !point.isGenerated) - .map((point, index) => { - const x = xScale(point.x); - const y = yScale(point.y); - - return ( - - {/* Data point circle */} - - - ); - })} - - - {currentContext === "daily" && - // Daily: Show 25 hour notches (0-24) with labels: 3, 6, 9, 12, 3, 6, 9 - // Includes midnight at start (hour 0) and midnight at end (hour 24) - Array.from({ length: 25 }, (_, i) => { - const hour = i; // Hours 0-24 (0=start midnight, 24=end midnight) - const isFirstNotch = hour === 0; - const isLastNotch = hour === 24; - - return ( - - {!isFirstNotch && !isLastNotch && } - {(hour === 3 || - hour === 6 || - hour === 9 || - hour === 12 || - hour === 15 || - hour === 18 || - hour === 21) && ( - - {hour === 12 ? 12 : hour > 12 ? hour - 12 : hour} - - )} - - ); - })} - - {currentContext === "weekly" && - (() => { - // Calculate the start of the week - const weekStart = new Date(startTime); - const dayLabels = ["S", "M", "T", "W", "T", "F", "S"]; - const days = []; - - for (let i = 0; i < 7; i++) { - const currentDay = new Date(weekStart); - currentDay.setDate(weekStart.getDate() + i); - const dayOfWeek = currentDay.getDay(); - const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; - - days.push( - - - - {dayLabels[dayOfWeek]} - - - ); - } - return days; - })()} - - {currentContext === "monthly" && - (() => { - // Monthly: Show notch for every day + 2 edge notches, label every 3rd day (skip first/last) - const monthStart = new Date(startTime); - const monthEnd = new Date(endTime); - const totalDays = Math.ceil( - (monthEnd.getTime() - monthStart.getTime()) / - (1000 * 60 * 60 * 24) - ); - const totalNotches = totalDays + 2; // Add 2 edge notches - const edgeMargin = -chartWidth / (totalNotches * 2); - const notches = []; - - for (let dayNum = 0; dayNum <= totalDays + 1; dayNum++) { - const isFirstNotch = dayNum === 0; - const isLastNotch = dayNum === totalDays + 1; - const isEdgeNotch = isFirstNotch || isLastNotch; - - // For labeling, only consider actual days (1 to totalDays) - const actualDay = dayNum; - const currentDate = new Date(monthStart); - currentDate.setDate(monthStart.getDate() + dayNum - 1); - - notches.push( - - {!isEdgeNotch && } - {!isEdgeNotch && - actualDay % 3 === 1 && ( // Label every 3rd day, skip edge notches - - {currentDate.getDate()} - - )} - - ); - } - return notches; - })()} - - - {/* Tooltips showing energy levels and native timestamps (excluding generated points) */} - {processedData - .filter((point) => !point.isGenerated) - .map((point, index) => { - const x = xScale(point.x); - const y = yScale(point.y); - - // Format time/date in user's native timezone based on context - const localDate = new Date(point.x); - const energyLevel = Math.round(point.y).toString(); - let timeText = ""; - - if (currentContext === "daily") { - // Show time for daily context: "7:00 AM" - timeText = localDate - .toLocaleTimeString([], { - hour: "numeric", - minute: "2-digit", - hour12: true, - }) - .toLowerCase() - .replace(" ", ""); - } else if (currentContext === "weekly") { - // Show just the day for weekly context (average per day) - timeText = localDate.toLocaleDateString([], { - weekday: "short", - }); - } else if (currentContext === "monthly") { - // Show date for monthly context (3-day avg): "Aug 31" - timeText = localDate.toLocaleDateString([], { - month: "short", - day: "numeric", - }); - } else { - // Project context: show full date and time - timeText = localDate - .toLocaleString([], { - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - hour12: true, - }) - .toLowerCase() - .replace(" ", ""); - } - - return ( - - - {timeText} - - - {energyLevel} - - - ); - })} - - ); -}; - -export default EnergyChart; - -const styles = StyleSheet.create({ - container: { - padding: 16, - backgroundColor: "#f5f5f5", - borderRadius: 8, - justifyContent: "center", - alignItems: "center", - }, - notchWrapper: { - display: "flex", - flexDirection: "row", - overflow: "visible", - }, - notchItem: { - display: "flex", - flexDirection: "column", - alignItems: "center", - gap: 3, - flex: 1, - overflow: "visible", - }, - notch: { - height: 3, - width: 0.5, - backgroundColor: "rgba(255,255,255,.3)", - overflow: "visible", - }, - notchNumber: { - color: "rgba(255,255,255,.4)", - overflow: "visible", - minWidth: 24, - textAlign: "center", - fontSize: 11, - lineHeight: 11, - }, - weekendNotch: { - backgroundColor: "rgba(255,255,255,.1)", - overflow: "visible", - }, - weekendLabel: { - color: "rgba(255,255,255,.25)", - overflow: "visible", - }, -}); +// import { StyleSheet, Alert, Clipboard, View } from "react-native"; +// import React, { useMemo, useEffect } from "react"; +// import { useLocationData } from "../../apps/mobile/src/hooks/useLocationData"; +// import * as SunCalc from "suncalc"; +// import { +// Canvas, +// Path, +// Skia, +// Circle, +// Group, +// Shadow, +// } from "@shopify/react-native-skia"; +// import { curveBasis, line, scaleLinear, curveCardinal } from "d3"; +// import { useSharedValue, withTiming } from "react-native-reanimated"; +// import { colors, Text } from "../../apps/mobile/src/design-system"; + +// // ✅ TUTORIAL COMPARISON: Missing scalePoint import for proper x-axis scaling +// // Current implementation uses scaleLinear for both axes, but tutorial uses scalePoint for x-axis + +// interface ChartDataPoint { +// x: number; +// y: number; +// label: string; +// timestamp: string; +// originalLevel: string | number; +// isGenerated?: boolean; // Optional flag for generated points +// } + +// type TideContext = "daily" | "weekly" | "monthly"; + +// type Props = { +// data: ChartDataPoint[]; // ✅ REQUIREMENT 2: Sample data structure +// context?: TideContext; +// chartHeight: number; // ✅ REQUIREMENT 1: Defined size of chart and canvas (height) +// chartMargin: number; // ✅ REQUIREMENT 1: Defined size of chart and canvas (margin) +// chartWidth: number; // ✅ REQUIREMENT 1: Defined size of chart and canvas (width) +// }; + +// const EnergyChart = ({ data, chartHeight, chartMargin, chartWidth }: Props) => { +// // Validate chart dimensions for scalability +// if (chartWidth <= 0 || chartHeight <= 0) { +// console.warn("EnergyChart: Invalid chart dimensions", { +// chartWidth, +// chartHeight, +// }); +// return null; +// } + +// if (data.length === 0) { +// return null; // Gracefully handle empty data +// } +// const { currentContext, dateOffset } = useTimeContext(); +// const { locationInfo } = useLocationData(); + +// // Calculate time range based on current context and date offset +// const getTimeRange = () => { +// const now = new Date(); +// const offsetDate = new Date(now); + +// if (currentContext === "daily") { +// offsetDate.setDate(now.getDate() - dateOffset); +// const start = new Date(offsetDate); +// start.setHours(0, 0, 0, 0); +// const end = new Date(offsetDate); +// end.setHours(23, 59, 59, 999); +// return [start.getTime(), end.getTime()]; +// } else if (currentContext === "weekly") { +// const weekStart = new Date(offsetDate); +// const dayOfWeek = weekStart.getDay(); +// weekStart.setDate(weekStart.getDate() - dayOfWeek - dateOffset * 7); +// weekStart.setHours(0, 0, 0, 0); +// const weekEnd = new Date(weekStart); +// weekEnd.setDate(weekStart.getDate() + 6); +// weekEnd.setHours(23, 59, 59, 999); +// return [weekStart.getTime(), weekEnd.getTime()]; +// } else if (currentContext === "monthly") { +// const monthStart = new Date( +// offsetDate.getFullYear(), +// offsetDate.getMonth() - dateOffset, +// 1 +// ); +// monthStart.setHours(0, 0, 0, 0); + +// const monthEnd = new Date( +// offsetDate.getFullYear(), +// offsetDate.getMonth() - dateOffset + 1, +// 0 // Day 0 of next month = last day of current month +// ); +// monthEnd.setHours(23, 59, 59, 999); + +// return [monthStart.getTime(), monthEnd.getTime()]; +// } else { +// // For project context, show all data +// return data.length > 0 +// ? [Math.min(...data.map((d) => d.x)), Math.max(...data.map((d) => d.x))] +// : [0, 1]; +// } +// }; + +// const [startTime, endTime] = getTimeRange(); + +// // Calculate sunrise/sunset for the current date being displayed +// const getSunTimes = useMemo(() => { +// if (!locationInfo.latitude || !locationInfo.longitude) return null; + +// const targetDate = new Date(); +// if (currentContext === "daily") { +// targetDate.setDate(targetDate.getDate() - dateOffset); +// return SunCalc.getTimes( +// targetDate, +// locationInfo.latitude, +// locationInfo.longitude +// ); +// } else { +// return null; // No sun times for weekly/monthly/project +// } +// }, [locationInfo, currentContext, dateOffset]); + +// const animationLine = useSharedValue(0); + +// // Calculate animation progress based on current time and context +// useEffect(() => { +// // Reset animation to prevent "already working" error +// animationLine.value = 0; + +// // Small delay to prevent animation conflicts +// const timer = setTimeout(() => { +// // Animate all contexts - they all get the same smooth drawing animation +// animationLine.value = withTiming(1, { duration: 600 }); +// }, 0); + +// return () => clearTimeout(timer); +// }, [currentContext, dateOffset, startTime, endTime]); + +// // Filter data to show only points within the time range for non-project contexts +// const filteredData = +// currentContext === "project" +// ? data +// : data.filter((d) => d.x >= startTime && d.x <= endTime); + +// // Process data for different contexts +// const processedData = useMemo(() => { +// if (currentContext === "daily" && filteredData.length > 0) { +// // For daily context: add a starting point at midnight +// const dayStart = new Date(startTime); + +// // For current day animation, filter out future data points +// let dataToUse = filteredData; +// if (dateOffset === 0) { +// const now = new Date(); +// dataToUse = filteredData.filter((point) => point.x <= now.getTime()); +// } + +// // Use the first data point's energy level or a default of 6 (medium) +// const startingEnergyLevel = dataToUse.length > 0 ? dataToUse[0].y : 6; + +// const startingPoint: ChartDataPoint = { +// x: dayStart.getTime(), +// y: startingEnergyLevel, +// label: "Day start", +// timestamp: dayStart.toISOString(), +// originalLevel: startingEnergyLevel, +// isGenerated: true, // Flag to identify this as a generated point +// }; + +// // For current day, add current time endpoint if needed +// if (dateOffset === 0 && dataToUse.length > 0) { +// const now = new Date(); +// const lastDataPoint = dataToUse[dataToUse.length - 1]; + +// // Add current time point with last known energy level +// const currentTimePoint: ChartDataPoint = { +// x: now.getTime(), +// y: lastDataPoint.y, // Use last energy level +// label: "Current time", +// timestamp: now.toISOString(), +// originalLevel: lastDataPoint.y, +// isGenerated: true, +// }; + +// dataToUse = [...dataToUse, currentTimePoint]; +// } + +// // Combine starting point with actual data +// const combinedData = [startingPoint, ...dataToUse]; + +// // Sort by time to ensure proper line drawing +// return combinedData.sort((a, b) => a.x - b.x); +// } + +// if (currentContext === "weekly" && filteredData.length > 0) { +// // For weekly context: group by day of week (hard-coded positions) +// const weeklyData = new Map(); + +// // Initialize all 7 days of the week +// for (let i = 0; i < 7; i++) { +// weeklyData.set(i, []); +// } + +// filteredData.forEach((point) => { +// const dayOfWeek = new Date(point.x).getDay(); // 0 = Sunday, 1 = Monday, etc. +// weeklyData.get(dayOfWeek)!.push(point); +// }); + +// // Only create points for days that have actual data +// const weeklyAverages: ChartDataPoint[] = []; +// const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +// for (let dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) { +// const dayPoints = weeklyData.get(dayOfWeek)!; + +// // Only add points for days with actual data +// if (dayPoints.length > 0) { +// // Has data - show actual average +// const avgEnergy = +// dayPoints.reduce((sum, point) => sum + point.y, 0) / +// dayPoints.length; + +// // Use actual day timestamp (noon of that day) for proper alignment with current time +// const weekStart = new Date(startTime); +// const dayTimestamp = new Date(weekStart); +// dayTimestamp.setDate(weekStart.getDate() + dayOfWeek); +// dayTimestamp.setHours(12, 0, 0, 0); // Noon of that day + +// weeklyAverages.push({ +// x: dayTimestamp.getTime(), +// y: avgEnergy, +// label: `${dayNames[dayOfWeek]} avg: ${avgEnergy.toFixed(1)}`, +// timestamp: dayTimestamp.toISOString(), +// originalLevel: avgEnergy.toFixed(1), +// }); +// } +// // Skip days with no data - don't add any point +// } + +// console.log("Weekly averages by day:", weeklyAverages.length, "points"); + +// // Add invisible edge points for continuous line if Sunday/Saturday missing +// if (weeklyAverages.length > 0) { +// const hasSunday = weeklyAverages.some((point) => +// point.label.startsWith("Sun") +// ); +// const hasSaturday = weeklyAverages.some((point) => +// point.label.startsWith("Sat") +// ); + +// // Add invisible Sunday point if missing (use first available day's energy) +// if (!hasSunday) { +// const firstPoint = weeklyAverages[0]; +// const weekStart = new Date(startTime); +// const sundayTimestamp = new Date(weekStart); +// sundayTimestamp.setDate(weekStart.getDate() + 0); // Sunday (day 0) +// sundayTimestamp.setHours(12, 0, 0, 0); + +// weeklyAverages.unshift({ +// x: sundayTimestamp.getTime(), +// y: firstPoint.y, +// label: `Sun edge: ${firstPoint.y.toFixed(1)}`, +// timestamp: sundayTimestamp.toISOString(), +// originalLevel: firstPoint.y.toFixed(1), +// isGenerated: true, // Invisible edge point +// }); +// } + +// // Add invisible Saturday point if missing (use last available day's energy) +// if (!hasSaturday) { +// const lastPoint = weeklyAverages[weeklyAverages.length - 1]; +// const weekStart = new Date(startTime); +// const saturdayTimestamp = new Date(weekStart); +// saturdayTimestamp.setDate(weekStart.getDate() + 6); // Saturday (day 6) +// saturdayTimestamp.setHours(12, 0, 0, 0); + +// weeklyAverages.push({ +// x: saturdayTimestamp.getTime(), +// y: lastPoint.y, +// label: `Sat edge: ${lastPoint.y.toFixed(1)}`, +// timestamp: saturdayTimestamp.toISOString(), +// originalLevel: lastPoint.y.toFixed(1), +// isGenerated: true, // Invisible edge point +// }); +// } + +// // Sort by position to ensure proper line drawing +// weeklyAverages.sort((a, b) => a.x - b.x); +// } + +// // For current week (dateOffset === 0), add current time endpoint +// if (dateOffset === 0 && weeklyAverages.length > 0) { +// const now = new Date(); +// const lastDataPoint = weeklyAverages[weeklyAverages.length - 1]; + +// const currentTimePoint: ChartDataPoint = { +// x: now.getTime(), +// y: lastDataPoint.y, +// label: "Current time", +// timestamp: now.toISOString(), +// originalLevel: lastDataPoint.y, +// isGenerated: true, +// }; +// weeklyAverages.push(currentTimePoint); +// } + +// return weeklyAverages; +// } + +// if (currentContext !== "monthly" || filteredData.length === 0) { +// return filteredData; +// } + +// // For monthly context: calculate 3-day rolling average for each day that has data +// console.log( +// "Monthly context - filteredData:", +// filteredData.length, +// "points" +// ); + +// // Group data by date first +// const dailyData = new Map(); + +// filteredData.forEach((point) => { +// const dateKey = new Date(point.x).toDateString(); +// if (!dailyData.has(dateKey)) { +// dailyData.set(dateKey, []); +// } +// dailyData.get(dateKey)!.push(point); +// }); + +// console.log("Monthly daily groups:", Array.from(dailyData.keys())); + +// // Calculate daily averages for days with data +// const dailyAverages = new Map(); // dayOfMonth -> average energy +// for (const [dateKey, dayPoints] of dailyData.entries()) { +// const avgEnergy = +// dayPoints.reduce((sum, point) => sum + point.y, 0) / dayPoints.length; +// const dayOfMonth = new Date(dateKey).getDate(); +// dailyAverages.set(dayOfMonth, avgEnergy); +// console.log( +// `Day ${dayOfMonth}: ${avgEnergy.toFixed(1)} (from ${ +// dayPoints.length +// } points)` +// ); +// } + +// // Calculate total days in month for positioning +// const monthStart = new Date(startTime); +// const monthEnd = new Date(endTime); +// const totalDays = Math.ceil( +// (monthEnd.getTime() - monthStart.getTime()) / (1000 * 60 * 60 * 24) +// ); + +// // Apply 3-day rolling average and position according to actual timestamps +// const monthlyPoints: ChartDataPoint[] = []; +// const now = new Date(); + +// for (const [dayOfMonth, dailyAvg] of dailyAverages.entries()) { +// // For current month, only include days up to today +// const dayTimestamp = new Date( +// monthStart.getFullYear(), +// monthStart.getMonth(), +// dayOfMonth, +// 12, +// 0, +// 0 +// ); +// if (dateOffset === 0 && dayTimestamp.getTime() > now.getTime()) { +// continue; // Skip future days in current month +// } + +// // Calculate 3-day rolling average (day-1, day, day+1) +// let sum = 0; +// let count = 0; + +// for (let offset = -1; offset <= 1; offset++) { +// const checkDay = dayOfMonth + offset; +// if (dailyAverages.has(checkDay)) { +// sum += dailyAverages.get(checkDay)!; +// count++; +// } +// } + +// const rollingAvg = count > 0 ? sum / count : dailyAvg; + +// monthlyPoints.push({ +// x: dayTimestamp.getTime(), +// y: rollingAvg, +// label: `Day ${dayOfMonth}: ${rollingAvg.toFixed(1)}`, +// timestamp: dayTimestamp.toISOString(), +// originalLevel: rollingAvg.toFixed(1), +// }); +// } + +// // Add month start point for continuous line (like daily midnight start) +// if (monthlyPoints.length > 0) { +// const firstDataPoint = monthlyPoints[0]; +// const monthStartPoint: ChartDataPoint = { +// x: startTime, +// y: firstDataPoint.y, +// label: "Month start", +// timestamp: new Date(startTime).toISOString(), +// originalLevel: firstDataPoint.y, +// isGenerated: true, +// }; +// monthlyPoints.unshift(monthStartPoint); + +// // For current month (dateOffset === 0), add current time endpoint +// if (dateOffset === 0) { +// const now = new Date(); +// const lastDataPoint = monthlyPoints[monthlyPoints.length - 1]; + +// const currentTimePoint: ChartDataPoint = { +// x: now.getTime(), +// y: lastDataPoint.y, +// label: "Current time", +// timestamp: now.toISOString(), +// originalLevel: lastDataPoint.y, +// isGenerated: true, +// }; +// monthlyPoints.push(currentTimePoint); +// } +// } + +// console.log("Monthly rolling averages:", monthlyPoints.length, "points"); +// return monthlyPoints.sort((a, b) => a.x - b.x); +// }, [currentContext, filteredData]); + +// // Chart scaling domains and ranges (memoized to prevent re-renders) +// // ✅ For daily context: use full 24-hour range to position data at actual times +// const xDomain = useMemo(() => { +// if (currentContext === "daily") { +// // Always use full day range for proper time positioning +// return [startTime, endTime]; // 00:00 to 23:59 of the selected day +// } else if (currentContext === "weekly") { +// // Always use full week range for proper day positioning +// return [startTime, endTime]; // Full week regardless of which days have data +// } else if (currentContext === "monthly") { +// // Always use full month range for proper day positioning +// return [startTime, endTime]; // Full month regardless of which days have data +// } else { +// // For project: use min/max of actual data +// return processedData.length > 0 +// ? [ +// Math.min(...processedData.map((d) => d.x)), +// Math.max(...processedData.map((d) => d.x)), +// ] +// : [startTime, endTime]; +// } +// }, [currentContext, processedData, startTime, endTime]); + +// // ✅ REQUIREMENT 4 & 9: Y-domain fixed to 0-10 for consistent energy level scaling +// const yDomain = [0, 10]; // Always use full energy scale range + +// // ✅ REQUIREMENT 3: D3 scales for mapping data to pixels +// // ❌ REQUIREMENT 5 & 7: Should use scalePoint for x-axis (discrete time points), currently using scaleLinear +// const xScale = useMemo( +// () => scaleLinear().domain(xDomain).range([0, chartWidth]), // ✅ REQUIREMENT 6: Range for x-axis (pixel space) - start at beginning +// [xDomain, chartWidth] +// ); + +// // ✅ REQUIREMENT 11: Y-scale created using scaleLinear mapping values from yDomain to yRange +// const yScale = useMemo( +// () => scaleLinear().domain(yDomain).range([chartHeight, 0]), // ✅ REQUIREMENT 10: Range for y-axis from chartHeight to 0 (inverted) - use full height +// [chartHeight] // yDomain is now constant [0, 10] +// ); + +// // Generate full background line (left to right across entire chart) +// const fullBackgroundLine = useMemo(() => { +// if (processedData.length === 0) return null; + +// // For background line, exclude current time endpoints to avoid the "turn around" effect +// const backgroundData = processedData.filter( +// (point) => !point.isGenerated || point.label !== "Current time" +// ); + +// // Create extended data that spans full chart width +// const extendedData = [...backgroundData]; + +// // Add starting point at left edge - natural extension from first point +// if (backgroundData.length > 0 && backgroundData[0].x > xDomain[0]) { +// extendedData.unshift({ +// ...backgroundData[0], +// x: xDomain[0], +// y: backgroundData[0].y, // Use first point's energy for natural lead-in +// isGenerated: true, +// }); +// } + +// // Add ending point at right edge - natural extension from last point +// if ( +// backgroundData.length > 0 && +// backgroundData[backgroundData.length - 1].x < xDomain[1] +// ) { +// extendedData.push({ +// ...backgroundData[backgroundData.length - 1], +// x: xDomain[1], +// y: backgroundData[backgroundData.length - 1].y, // Use last point's energy for natural lead-out +// isGenerated: true, +// }); +// } + +// return line() +// .x((d) => xScale(d.x)) +// .y((d) => yScale(d.y)) +// .curve(curveCardinal.tension(0))(extendedData); +// }, [processedData, xScale, yScale, xDomain]); + +// // Generate filled area below the background line +// const backgroundFillPath = useMemo(() => { +// if (processedData.length === 0) return null; + +// // For background line, exclude current time endpoints to avoid the "turn around" effect +// const backgroundData = processedData.filter( +// (point) => !point.isGenerated || point.label !== "Current time" +// ); + +// // Create extended data that spans full chart width +// const extendedData = [...backgroundData]; + +// // Add starting point at left edge +// if (backgroundData.length > 0 && backgroundData[0].x > xDomain[0]) { +// extendedData.unshift({ +// ...backgroundData[0], +// x: xDomain[0], +// y: backgroundData[0].y, +// isGenerated: true, +// }); +// } + +// // Add ending point at right edge +// if ( +// backgroundData.length > 0 && +// backgroundData[backgroundData.length - 1].x < xDomain[1] +// ) { +// extendedData.push({ +// ...backgroundData[backgroundData.length - 1], +// x: xDomain[1], +// y: backgroundData[backgroundData.length - 1].y, +// isGenerated: true, +// }); +// } + +// // Create the line path first +// const linePath = line() +// .x((d) => xScale(d.x)) +// .y((d) => yScale(d.y)) +// .curve(curveCardinal.tension(0))(extendedData); + +// if (!linePath) return null; + +// // Convert to fill by adding bottom edge points +// const fillPathString = `${linePath} L${xScale( +// xDomain[1] +// )},${chartHeight} L${xScale(xDomain[0])},${chartHeight} Z`; + +// try { +// return Skia.Path.MakeFromSVGString(fillPathString); +// } catch (error) { +// console.warn("Failed to create background fill path:", error); +// return null; +// } +// }, [processedData, xScale, yScale, xDomain, chartHeight]); + +// // Generate daylight filled area below the background line (daily context only) +// const daylightFillPath = useMemo(() => { +// if ( +// currentContext !== "daily" || +// !getSunTimes?.sunrise || +// !getSunTimes?.sunset +// ) { +// return null; +// } + +// const sunriseTime = getSunTimes.sunrise.getTime(); +// const sunsetTime = getSunTimes.sunset.getTime(); + +// // Calculate 7px buffer zones in time units +// const bufferTimeMs = (7 / chartWidth) * (xDomain[1] - xDomain[0]); +// const sunriseBufferEnd = sunriseTime + bufferTimeMs; +// const sunsetBufferStart = sunsetTime - bufferTimeMs; + +// // Use background data processing or create default flat line if no data +// const backgroundData = processedData.filter( +// (point) => !point.isGenerated || point.label !== "Current time" +// ); + +// let extendedData = [...backgroundData]; + +// // If no data, create a flat line at energy level 5 (medium) for backgrounds to follow +// if (backgroundData.length === 0) { +// extendedData = [ +// { +// x: xDomain[0], +// y: 5, +// label: "default start", +// timestamp: "", +// originalLevel: 5, +// isGenerated: true, +// }, +// { +// x: xDomain[1], +// y: 5, +// label: "default end", +// timestamp: "", +// originalLevel: 5, +// isGenerated: true, +// }, +// ]; +// } else { +// // Add starting point at left edge +// if (backgroundData.length > 0 && backgroundData[0].x > xDomain[0]) { +// extendedData.unshift({ +// ...backgroundData[0], +// x: xDomain[0], +// y: backgroundData[0].y, +// isGenerated: true, +// }); +// } + +// // Add ending point at right edge +// if ( +// backgroundData.length > 0 && +// backgroundData[backgroundData.length - 1].x < xDomain[1] +// ) { +// extendedData.push({ +// ...backgroundData[backgroundData.length - 1], +// x: xDomain[1], +// y: backgroundData[backgroundData.length - 1].y, +// isGenerated: true, +// }); +// } +// } + +// // Find or interpolate the energy level at sunrise and sunset times +// const findEnergyAtTime = (targetTime: number) => { +// // Find the closest points before and after the target time +// const beforePoint = extendedData.filter((p) => p.x <= targetTime).pop(); +// const afterPoint = extendedData.find((p) => p.x >= targetTime); + +// if (beforePoint && afterPoint && beforePoint.x !== afterPoint.x) { +// // Linear interpolation +// const ratio = +// (targetTime - beforePoint.x) / (afterPoint.x - beforePoint.x); +// return beforePoint.y + ratio * (afterPoint.y - beforePoint.y); +// } else if (beforePoint) { +// return beforePoint.y; +// } else if (afterPoint) { +// return afterPoint.y; +// } +// return 5; // Default middle energy level +// }; + +// // Create daylight data with 7px buffer zones +// const sunriseBufferY = findEnergyAtTime(sunriseBufferEnd); +// const sunsetBufferY = findEnergyAtTime(sunsetBufferStart); + +// // Daylight area (shrunk by 7px buffer on each side) +// let daylightData = [ +// { +// x: sunriseBufferEnd, +// y: sunriseBufferY, +// label: "sunrise buffer end", +// timestamp: "", +// originalLevel: sunriseBufferY, +// }, +// ]; + +// // Add actual data points within daylight hours (excluding buffer zones) +// const innerPoints = extendedData.filter( +// (point) => point.x > sunriseBufferEnd && point.x < sunsetBufferStart +// ); +// daylightData.push(...innerPoints); + +// // Add sunset buffer boundary point +// daylightData.push({ +// x: sunsetBufferStart, +// y: sunsetBufferY, +// label: "sunset buffer start", +// timestamp: "", +// originalLevel: sunsetBufferY, +// }); + +// if (daylightData.length < 2) { +// return null; // Need at least sunrise and sunset points +// } + +// // Create the daylight line path +// const daylightLinePath = line() +// .x((d) => xScale(d.x)) +// .y((d) => yScale(d.y)) +// .curve(curveCardinal.tension(0))(daylightData); + +// if (!daylightLinePath) return null; + +// // Create the fill path: line path + bottom edge (shrunk daylight area) +// const fillPathString = `${daylightLinePath} L${xScale( +// sunsetBufferStart +// )},${chartHeight} L${xScale(sunriseBufferEnd)},${chartHeight} Z`; + +// try { +// return Skia.Path.MakeFromSVGString(fillPathString); +// } catch (error) { +// console.warn("Failed to create daylight fill path:", error); +// return null; +// } +// }, [processedData, xScale, yScale, currentContext, getSunTimes, chartHeight]); + +// // Generate sunrise buffer zone fill path (7px transition area) +// const sunriseBufferFillPath = useMemo(() => { +// if ( +// currentContext !== "daily" || +// !getSunTimes?.sunrise +// ) { +// return null; +// } + +// const sunriseTime = getSunTimes.sunrise.getTime(); +// const bufferTimeMs = (7 / chartWidth) * (xDomain[1] - xDomain[0]); +// const sunriseBufferEnd = sunriseTime + bufferTimeMs; + +// // Find energy levels at buffer boundaries using same helper as daylight +// const backgroundData = processedData.filter( +// (point) => !point.isGenerated || point.label !== "Current time" +// ); +// const extendedData = [...backgroundData]; + +// if (backgroundData.length > 0 && backgroundData[0].x > xDomain[0]) { +// extendedData.unshift({ +// ...backgroundData[0], +// x: xDomain[0], +// y: backgroundData[0].y, +// isGenerated: true, +// }); +// } + +// if ( +// backgroundData.length > 0 && +// backgroundData[backgroundData.length - 1].x < xDomain[1] +// ) { +// extendedData.push({ +// ...backgroundData[backgroundData.length - 1], +// x: xDomain[1], +// y: backgroundData[backgroundData.length - 1].y, +// isGenerated: true, +// }); +// } + +// const findEnergyAtTime = (targetTime: number) => { +// const beforePoint = extendedData.filter((p) => p.x <= targetTime).pop(); +// const afterPoint = extendedData.find((p) => p.x >= targetTime); + +// if (beforePoint && afterPoint && beforePoint.x !== afterPoint.x) { +// const ratio = +// (targetTime - beforePoint.x) / (afterPoint.x - beforePoint.x); +// return beforePoint.y + ratio * (afterPoint.y - beforePoint.y); +// } else if (beforePoint) { +// return beforePoint.y; +// } else if (afterPoint) { +// return afterPoint.y; +// } +// return 5; +// }; + +// const sunriseY = findEnergyAtTime(sunriseTime); +// const sunriseBufferY = findEnergyAtTime(sunriseBufferEnd); + +// // Create buffer zone data +// const bufferData = [ +// { x: sunriseTime, y: sunriseY }, +// { x: sunriseBufferEnd, y: sunriseBufferY }, +// ]; + +// // Create the buffer line path +// const bufferLinePath = line() +// .x((d) => xScale(d.x)) +// .y((d) => yScale(d.y)) +// .curve(curveCardinal.tension(0))(bufferData); + +// if (!bufferLinePath) return null; + +// // Create fill path for sunrise buffer +// const fillPathString = `${bufferLinePath} L${xScale( +// sunriseBufferEnd +// )},${chartHeight} L${xScale(sunriseTime)},${chartHeight} Z`; + +// try { +// return Skia.Path.MakeFromSVGString(fillPathString); +// } catch (error) { +// console.warn("Failed to create sunrise buffer fill path:", error); +// return null; +// } +// }, [ +// processedData, +// xScale, +// yScale, +// currentContext, +// getSunTimes, +// chartHeight, +// xDomain, +// ]); + +// // Generate sunset buffer zone fill path (7px transition area) +// const sunsetBufferFillPath = useMemo(() => { +// if ( +// currentContext !== "daily" || +// !getSunTimes?.sunset +// ) { +// return null; +// } + +// const sunsetTime = getSunTimes.sunset.getTime(); +// const bufferTimeMs = (7 / chartWidth) * (xDomain[1] - xDomain[0]); +// const sunsetBufferStart = sunsetTime - bufferTimeMs; + +// // Find energy levels at buffer boundaries using same helper as daylight +// const backgroundData = processedData.filter( +// (point) => !point.isGenerated || point.label !== "Current time" +// ); +// const extendedData = [...backgroundData]; + +// if (backgroundData.length > 0 && backgroundData[0].x > xDomain[0]) { +// extendedData.unshift({ +// ...backgroundData[0], +// x: xDomain[0], +// y: backgroundData[0].y, +// isGenerated: true, +// }); +// } + +// if ( +// backgroundData.length > 0 && +// backgroundData[backgroundData.length - 1].x < xDomain[1] +// ) { +// extendedData.push({ +// ...backgroundData[backgroundData.length - 1], +// x: xDomain[1], +// y: backgroundData[backgroundData.length - 1].y, +// isGenerated: true, +// }); +// } + +// const findEnergyAtTime = (targetTime: number) => { +// const beforePoint = extendedData.filter((p) => p.x <= targetTime).pop(); +// const afterPoint = extendedData.find((p) => p.x >= targetTime); + +// if (beforePoint && afterPoint && beforePoint.x !== afterPoint.x) { +// const ratio = +// (targetTime - beforePoint.x) / (afterPoint.x - beforePoint.x); +// return beforePoint.y + ratio * (afterPoint.y - beforePoint.y); +// } else if (beforePoint) { +// return beforePoint.y; +// } else if (afterPoint) { +// return afterPoint.y; +// } +// return 5; +// }; + +// const sunsetBufferY = findEnergyAtTime(sunsetBufferStart); +// const sunsetY = findEnergyAtTime(sunsetTime); + +// // Create buffer zone data +// const bufferData = [ +// { x: sunsetBufferStart, y: sunsetBufferY }, +// { x: sunsetTime, y: sunsetY }, +// ]; + +// // Create the buffer line path +// const bufferLinePath = line() +// .x((d) => xScale(d.x)) +// .y((d) => yScale(d.y)) +// .curve(curveCardinal.tension(0))(bufferData); + +// if (!bufferLinePath) return null; + +// // Create fill path for sunset buffer +// const fillPathString = `${bufferLinePath} L${xScale( +// sunsetTime +// )},${chartHeight} L${xScale(sunsetBufferStart)},${chartHeight} Z`; + +// try { +// return Skia.Path.MakeFromSVGString(fillPathString); +// } catch (error) { +// console.warn("Failed to create sunset buffer fill path:", error); +// return null; +// } +// }, [ +// processedData, +// xScale, +// yScale, +// currentContext, +// getSunTimes, +// chartHeight, +// xDomain, +// ]); + +// // Generate current time line (follows background path but stops at current time) +// const curvedLine = useMemo(() => { +// if (processedData.length === 0) return null; + +// // Use IDENTICAL data processing as fullBackgroundLine +// const backgroundData = processedData.filter( +// (point) => !point.isGenerated || point.label !== "Current time" +// ); + +// // Create extended data EXACTLY like fullBackgroundLine +// const extendedData = [...backgroundData]; + +// // Add starting point at left edge - IDENTICAL to background line +// if (backgroundData.length > 0 && backgroundData[0].x > xDomain[0]) { +// extendedData.unshift({ +// ...backgroundData[0], +// x: xDomain[0], +// y: backgroundData[0].y, // Use first point's energy for natural lead-in +// isGenerated: true, +// }); +// } + +// // Add ending point at right edge - IDENTICAL to background line +// if ( +// backgroundData.length > 0 && +// backgroundData[backgroundData.length - 1].x < xDomain[1] +// ) { +// extendedData.push({ +// ...backgroundData[backgroundData.length - 1], +// x: xDomain[1], +// y: backgroundData[backgroundData.length - 1].y, // Use last point's energy for natural lead-out +// isGenerated: true, +// }); +// } + +// // For current periods (dateOffset === 0), create a truncated version at current time +// if (dateOffset === 0) { +// const now = new Date(); + +// // Generate the full path first, then interpolate at current time +// const fullLine = line() +// .x((d) => xScale(d.x)) +// .y((d) => yScale(d.y)) +// .curve(curveCardinal.tension(0))(extendedData); + +// if (!fullLine) return null; + +// // Find the Y value at current time by interpolating the background curve +// const currentTimeX = xScale(now.getTime()); + +// // Filter points up to current time and add interpolated endpoint +// const dataUpToNow = extendedData.filter( +// (point) => point.x <= now.getTime() +// ); + +// // Add current time point with interpolated Y value from the background curve +// if (dataUpToNow.length > 0) { +// const lastPoint = dataUpToNow[dataUpToNow.length - 1]; +// dataUpToNow.push({ +// x: now.getTime(), +// y: lastPoint.y, // Use last known energy level +// label: "Current time endpoint", +// timestamp: now.toISOString(), +// originalLevel: lastPoint.y, +// isGenerated: true, +// }); +// } + +// return line() +// .x((d) => xScale(d.x)) +// .y((d) => yScale(d.y)) +// .curve(curveCardinal.tension(0))(dataUpToNow); +// } + +// // For past periods, use the full extended data (same as background) +// return line() +// .x((d) => xScale(d.x)) +// .y((d) => yScale(d.y)) +// .curve(curveCardinal.tension(0))(extendedData); +// }, [processedData, xScale, yScale, dateOffset, xDomain]); + +// // Convert background line to Skia path +// const backgroundLinePath = useMemo(() => { +// if (!fullBackgroundLine) return null; +// try { +// return Skia.Path.MakeFromSVGString(fullBackgroundLine); +// } catch (error) { +// console.warn( +// "Failed to create background Skia path from SVG string:", +// error +// ); +// return null; +// } +// }, [fullBackgroundLine]); + +// // Convert D3 SVG string to Skia path (following tutorial pattern) +// const linePath = useMemo(() => { +// if (!curvedLine) return null; +// try { +// return Skia.Path.MakeFromSVGString(curvedLine); +// } catch (error) { +// console.warn("Failed to create Skia path from SVG string:", error); +// return null; +// } +// }, [curvedLine]); + +// const copyDebugInfo = () => { +// const debugInfo = ` +// ENERGY CHART DEBUG INFO +// ====================== +// Context: ${currentContext} ${dateOffset > 0 ? `(-${dateOffset})` : "(current)"} +// Energy Points: ${processedData.length} of ${data.length} ${ +// currentContext === "monthly" ? "(3-day rolling avg)" : "" +// } +// X Min/Max: ${new Date(xDomain[0]).toLocaleDateString()} - ${new Date( +// xDomain[1] +// ).toLocaleDateString()} +// Y Min/Max: [${yDomain[0]} - ${yDomain[1]}] +// X Range: [0px - ${chartWidth}px] +// Y Range: [${chartHeight}px - 0px] +// CurvedLine: ${curvedLine ? "✅ Generated" : "❌ Failed"} +// LinePath: ${linePath ? "✅ Created" : "❌ Failed"} +// Processed Data: ${processedData.length} points +// Chart Dimensions: ${chartWidth}x${chartHeight}, margin: ${chartMargin} +// Current Time: ${new Date().toLocaleTimeString()} +// Start Time: ${new Date(startTime).toLocaleTimeString()} +// End Time: ${new Date(endTime).toLocaleTimeString()} +// Time Progress: ${ +// currentContext === "daily" && dateOffset === 0 +// ? `${( +// ((new Date().getTime() - startTime) / (endTime - startTime)) * +// 100 +// ).toFixed(1)}%` +// : "100%" +// } +// Animation Value: ${animationLine.value.toFixed(3)} + +// PROCESSED DATA POINTS: +// ${processedData +// .map( +// (d) => +// `- ${new Date(d.x).toLocaleString()}: Level ${d.y.toFixed(1)} (${ +// d.originalLevel +// })` +// ) +// .join("\n")} + +// RAW DATA: +// ${data +// .map( +// (d) => +// `- ${new Date(d.x).toLocaleString()}: Level ${d.y} (${d.originalLevel})` +// ) +// .join("\n")} +// `.trim(); + +// Clipboard.setString(debugInfo); +// Alert.alert( +// "Debug Info Copied!", +// "All debug information copied to clipboard" +// ); +// }; + +// return ( +// +// +// {/* Filled background area below the energy line */} +// {backgroundFillPath && ( +// <> +// {currentContext === "daily" ? ( +// <> +// {/* Dark blue nighttime fill for daily */} +// + +// {/* Light blue daylight fill that follows the energy line */} +// {daylightFillPath && ( +// +// )} + +// {/* Buffer zones - middle colors between night and day */} +// {sunriseBufferFillPath && ( +// +// )} +// {sunsetBufferFillPath && ( +// +// )} +// +// ) : ( +// /* Solid fill for weekly and monthly */ +// +// )} +// +// )} + +// {/* Background line - 10% opacity, spans full chart from left to right */} +// {backgroundLinePath && ( +// +// )} + +// {/* Animated line - 100% opacity, fills from left to current time */} +// {linePath && ( +// +// +// {/* */} +// +// )} + +// {/* Current time indicator for all contexts */} +// {dateOffset === 0 && +// (() => { +// const now = new Date(); +// const currentTimeX = xScale(now.getTime()); +// return currentTimeX >= 0 && currentTimeX <= chartWidth ? ( +// +// {/* Current time line */} +// +// +// ) : null; +// })()} + +// {/* Data point markers for all contexts (excluding generated points) - always show all dots */} +// {processedData +// .filter((point) => !point.isGenerated) +// .map((point, index) => { +// const x = xScale(point.x); +// const y = yScale(point.y); + +// return ( +// +// {/* Data point circle */} +// +// +// ); +// })} +// +// +// {currentContext === "daily" && +// // Daily: Show 25 hour notches (0-24) with labels: 3, 6, 9, 12, 3, 6, 9 +// // Includes midnight at start (hour 0) and midnight at end (hour 24) +// Array.from({ length: 25 }, (_, i) => { +// const hour = i; // Hours 0-24 (0=start midnight, 24=end midnight) +// const isFirstNotch = hour === 0; +// const isLastNotch = hour === 24; + +// return ( +// +// {!isFirstNotch && !isLastNotch && } +// {(hour === 3 || +// hour === 6 || +// hour === 9 || +// hour === 12 || +// hour === 15 || +// hour === 18 || +// hour === 21) && ( +// +// {hour === 12 ? 12 : hour > 12 ? hour - 12 : hour} +// +// )} +// +// ); +// })} + +// {currentContext === "weekly" && +// (() => { +// // Calculate the start of the week +// const weekStart = new Date(startTime); +// const dayLabels = ["S", "M", "T", "W", "T", "F", "S"]; +// const days = []; + +// for (let i = 0; i < 7; i++) { +// const currentDay = new Date(weekStart); +// currentDay.setDate(weekStart.getDate() + i); +// const dayOfWeek = currentDay.getDay(); +// const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + +// days.push( +// +// +// +// {dayLabels[dayOfWeek]} +// +// +// ); +// } +// return days; +// })()} + +// {currentContext === "monthly" && +// (() => { +// // Monthly: Show notch for every day + 2 edge notches, label every 3rd day (skip first/last) +// const monthStart = new Date(startTime); +// const monthEnd = new Date(endTime); +// const totalDays = Math.ceil( +// (monthEnd.getTime() - monthStart.getTime()) / +// (1000 * 60 * 60 * 24) +// ); +// const totalNotches = totalDays + 2; // Add 2 edge notches +// const edgeMargin = -chartWidth / (totalNotches * 2); +// const notches = []; + +// for (let dayNum = 0; dayNum <= totalDays + 1; dayNum++) { +// const isFirstNotch = dayNum === 0; +// const isLastNotch = dayNum === totalDays + 1; +// const isEdgeNotch = isFirstNotch || isLastNotch; + +// // For labeling, only consider actual days (1 to totalDays) +// const actualDay = dayNum; +// const currentDate = new Date(monthStart); +// currentDate.setDate(monthStart.getDate() + dayNum - 1); + +// notches.push( +// +// {!isEdgeNotch && } +// {!isEdgeNotch && +// actualDay % 3 === 1 && ( // Label every 3rd day, skip edge notches +// +// {currentDate.getDate()} +// +// )} +// +// ); +// } +// return notches; +// })()} +// + +// {/* Tooltips showing energy levels and native timestamps (excluding generated points) */} +// {processedData +// .filter((point) => !point.isGenerated) +// .map((point, index) => { +// const x = xScale(point.x); +// const y = yScale(point.y); + +// // Format time/date in user's native timezone based on context +// const localDate = new Date(point.x); +// const energyLevel = Math.round(point.y).toString(); +// let timeText = ""; + +// if (currentContext === "daily") { +// // Show time for daily context: "7:00 AM" +// timeText = localDate +// .toLocaleTimeString([], { +// hour: "numeric", +// minute: "2-digit", +// hour12: true, +// }) +// .toLowerCase() +// .replace(" ", ""); +// } else if (currentContext === "weekly") { +// // Show just the day for weekly context (average per day) +// timeText = localDate.toLocaleDateString([], { +// weekday: "short", +// }); +// } else if (currentContext === "monthly") { +// // Show date for monthly context (3-day avg): "Aug 31" +// timeText = localDate.toLocaleDateString([], { +// month: "short", +// day: "numeric", +// }); +// } else { +// // Project context: show full date and time +// timeText = localDate +// .toLocaleString([], { +// month: "short", +// day: "numeric", +// hour: "numeric", +// minute: "2-digit", +// hour12: true, +// }) +// .toLowerCase() +// .replace(" ", ""); +// } + +// return ( +// +// +// {timeText} +// +// +// {energyLevel} +// +// +// ); +// })} +// +// ); +// }; + +// export default EnergyChart; + +// const styles = StyleSheet.create({ +// container: { +// padding: 16, +// backgroundColor: "#f5f5f5", +// borderRadius: 8, +// justifyContent: "center", +// alignItems: "center", + +// }, +// notchWrapper: { +// display: "flex", +// flexDirection: "row", +// overflow: "visible", +// }, +// notchItem: { +// display: "flex", +// flexDirection: "column", +// alignItems: "center", +// gap: 3, +// flex: 1, +// overflow: "visible", +// }, +// notch: { +// height: 3, +// width: 0.5, +// backgroundColor: "rgba(255,255,255,.3)", +// overflow: "visible", +// }, +// notchNumber: { +// color: "rgba(255,255,255,.4)", +// overflow: "visible", +// minWidth: 24, +// textAlign: "center", +// fontSize: 11, +// lineHeight: 11, +// }, +// weekendNotch: { +// backgroundColor: "rgba(255,255,255,.1)", +// overflow: "visible", +// }, +// weekendLabel: { +// color: "rgba(255,255,255,.25)", +// overflow: "visible", +// }, +// }); diff --git a/apps/mobile/src/components/chat/MessageBubble.tsx b/apps/mobile/src/components/MessageBubble.tsx similarity index 93% rename from apps/mobile/src/components/chat/MessageBubble.tsx rename to apps/mobile/src/components/MessageBubble.tsx index 6fc1bbe..f418ef7 100644 --- a/apps/mobile/src/components/chat/MessageBubble.tsx +++ b/apps/mobile/src/components/MessageBubble.tsx @@ -1,9 +1,9 @@ import React from "react"; import { View, StyleSheet, Image } from "react-native"; -import { Text } from "../Text"; -import { colors, spacing, typography } from "../../design-system/tokens"; -import type { ChatMessage } from "../../types/chat"; -import { getInterFont } from "../../utils/fonts"; +import { Text } from "./Text"; +import { colors, spacing, typography } from "../design-system/tokens"; +import type { ChatMessage } from "../types/chat"; +import { getInterFont } from "../utils/fonts"; interface MessageBubbleProps { message: ChatMessage; @@ -70,7 +70,10 @@ export const MessageBubble: React.FC = ({ // Remove ** and render as bold (nested Text for inline styling) const boldText = part.slice(2, -2); return ( - + {boldText} ); diff --git a/apps/mobile/src/components/NewEnergyChart.tsx b/apps/mobile/src/components/NewEnergyChart.tsx new file mode 100644 index 0000000..1b4a464 --- /dev/null +++ b/apps/mobile/src/components/NewEnergyChart.tsx @@ -0,0 +1,482 @@ +import { StyleSheet, View } from "react-native"; +import React, { useMemo } from "react"; +import { Canvas, Path, Skia, Group, Shadow } from "@shopify/react-native-skia"; +import { line, scaleLinear, curveCatmullRom } from "d3"; +import { Text } from "../design-system"; + +interface ChartDataPoint { + x: number; + y: number; + label: string; + timestamp: string; + originalLevel: string | number; +} + +type TimeDisplayContextType = + | "1day" + | "3day" + | "1week" + | "1month" + | "3month" + | "1year"; + +// NewEnergyChart: Skia-powered energy chart with dynamic time scaling and labeling +interface NewEnergyChartProps { + data: ChartDataPoint[]; // Energy data points to plot with x/y coordinates + timeDisplayContext: TimeDisplayContextType; // Time range from ChartDisplayContext ("1day", "3day", etc.) + chartHeight: number; // Calculated height from Home component screen dimensions + chartMargin: number; // Chart margin (currently unused) + chartWidth: number; // Full screen width from Home component dimensions +} + +export const NewEnergyChart: React.FC = ({ + data, // ChartDataPoint[] from getTimeContextChartData() + timeDisplayContext, // TimeRange from ChartDisplayContext.timeRange + chartHeight, // Calculated: height * 0.55 - insets - 70 - 82 - 8 + chartWidth, // Screen width from useWindowDimensions() +}) => { + // Chart scaling - use full time range for proper positioning + const xDomain = useMemo(() => { + const now = new Date(); // Use current date + let startDate = new Date(now); + + // Calculate full time range based on context + switch (timeDisplayContext) { + case "1day": + startDate.setDate(now.getDate() - 1); + return [startDate.getTime(), now.getTime()]; + case "3day": + startDate.setDate(now.getDate() - 3); + return [startDate.getTime(), now.getTime()]; + case "1week": + startDate.setDate(now.getDate() - 7); + return [startDate.getTime(), now.getTime()]; + case "1month": + startDate.setDate(now.getDate() - 31); + return [startDate.getTime(), now.getTime()]; + case "3month": + startDate.setDate(now.getDate() - 90); + return [startDate.getTime(), now.getTime()]; + case "1year": + startDate.setDate(now.getDate() - 365); + return [startDate.getTime(), now.getTime()]; + default: + return data.length > 0 + ? [ + Math.min(...data.map((d) => d.x)), + Math.max(...data.map((d) => d.x)), + ] + : [0, 1]; + } + }, [timeDisplayContext, data]); + + const yDomain = [0, 10]; // Fixed energy scale 0-10 + + const xScale = useMemo( + () => scaleLinear().domain(xDomain).range([0, chartWidth]), + [xDomain, chartWidth] + ); + + const yScale = useMemo( + () => scaleLinear().domain(yDomain).range([chartHeight, 0]), + [chartHeight] + ); + + // Generate line path + const linePath = useMemo(() => { + if (data.length === 0) return null; + + const lineGenerator = line() + .x((d) => xScale(d.x)) + .y((d) => yScale(d.y)) + .curve(curveCatmullRom.alpha(0.5)); // Smooth Catmull-Rom curves + + const svgPath = lineGenerator(data); + if (!svgPath) return null; + + try { + return Skia.Path.MakeFromSVGString(svgPath); + } catch (error) { + console.warn("Failed to create line path:", error); + return null; + } + }, [data, xScale, yScale]); + + // Generate fill area + const fillPath = useMemo(() => { + if (data.length === 0) return null; + + const lineGenerator = line() + .x((d) => xScale(d.x)) + .y((d) => yScale(d.y)) + .curve(curveCatmullRom.alpha(0.5)); // Match the line curve smoothness + + const svgPath = lineGenerator(data); + if (!svgPath) return null; + + // Add bottom edge to create fill area + const fillPathString = `${svgPath} L${xScale( + xDomain[1] + )},${chartHeight} L${xScale(xDomain[0])},${chartHeight} Z`; + + try { + return Skia.Path.MakeFromSVGString(fillPathString); + } catch (error) { + console.warn("Failed to create fill path:", error); + return null; + } + }, [data, xScale, yScale, xDomain, chartHeight]); + + // Generate time labels based on context + const timeLabels = useMemo(() => { + if (data.length === 0) return []; + const now = new Date(); // Use current date + + switch (timeDisplayContext) { + case "1day": + // Each labeled hour should have 2 notches before and after, spaced accordingly + // Start from hour 1 to give first "3" one notch before (1,2,3) + const hourLabels = []; + + for (let hour = 1; hour < 24; hour++) { + // Hours 1-23 (23 notches) + let label = ""; + + if ( + hour === 3 || + hour === 6 || + hour === 9 || + hour === 12 || + hour === 15 || + hour === 18 || + hour === 21 + ) { + const displayHour = hour === 12 ? 12 : hour > 12 ? hour - 12 : hour; + label = displayHour.toString(); + } + + hourLabels.push({ label }); + } + return hourLabels; + + case "3day": + // 73 total notches: Day1(0-24)=25, Day2(1-24)=24, Day3(1-23)=23, plus midnight markers=1 + const threeDayLabels = []; + + // Day 1: hours 1-24 + for (let hour = 1; hour <= 24; hour++) { + let label = ""; + if (hour === 24) { + label = "12"; // Midnight between day 1 and 2 + } else if (hour === 6 || hour === 12 || hour === 18) { + const displayHour = hour === 12 ? 12 : hour > 12 ? hour - 12 : hour; + label = displayHour.toString(); + } + threeDayLabels.push({ label }); + } + + // Day 2: hours 1-24 + for (let hour = 1; hour <= 24; hour++) { + let label = ""; + if (hour === 24) { + label = "12"; // Midnight between day 2 and 3 + } else if (hour === 6 || hour === 12 || hour === 18) { + const displayHour = hour === 12 ? 12 : hour > 12 ? hour - 12 : hour; + label = displayHour.toString(); + } + threeDayLabels.push({ label }); + } + + // Day 3: hours 1-23 + for (let hour = 1; hour <= 23; hour++) { + let label = ""; + if (hour === 6 || hour === 12 || hour === 18) { + const displayHour = hour === 12 ? 12 : hour > 12 ? hour - 12 : hour; + label = displayHour.toString(); + } + threeDayLabels.push({ label }); + } + + return threeDayLabels; + + case "1week": + // 7 notches: Today and last 6 days, today on far right + // If today is Wednesday: T F S S M T W + const weekLabels = []; + const dayLabels = ["S", "M", "T", "W", "T", "F", "S"]; + const todayDayOfWeek = now.getDay(); // 0=Sunday, 1=Monday, etc. + + for (let i = 0; i < 7; i++) { + const daysBack = 6 - i; // 6, 5, 4, 3, 2, 1, 0 (today) + const dayOfWeek = (todayDayOfWeek - daysBack + 7) % 7; + weekLabels.push({ + label: dayLabels[dayOfWeek], + }); + } + return weekLabels; + + case "1month": + // 31 notches, labels on 4th, 8th, 12th, 16th, 20th, 24th, 28th days + const monthLabels = []; + for (let day = 0; day < 31; day++) { + const daysBack = 30 - day; // 30, 29, 28, ..., 1, 0 (today) + const date = new Date(now); + date.setDate(now.getDate() - daysBack); + let label = ""; + + if ( + day === 3 || + day === 7 || + day === 11 || + day === 15 || + day === 19 || + day === 23 || + day === 27 + ) { + // Labels on 4th, 8th, 12th, 16th, 20th, 24th, 28th days (0-indexed) + const monthNum = date.getMonth() + 1; // 1-12 for months + const dayNum = date.getDate(); + label = `${monthNum}/${dayNum}`; + } + + monthLabels.push({ label }); + } + return monthLabels; + + case "3month": + // 91 notches, labels on 10th, 22nd, 34th, 46th, 58th, 70th, 82nd notches + const threeMonthLabels = []; + for (let day = 0; day < 91; day++) { + const daysBack = 90 - day; // 90, 89, 88, ..., 1, 0 (today) + const date = new Date(now); + date.setDate(now.getDate() - daysBack); + let label = ""; + + if ( + day === 9 || + day === 21 || + day === 33 || + day === 45 || + day === 57 || + day === 69 || + day === 81 + ) { + // Labels on 10th, 22nd, 34th, 46th, 58th, 70th, 82nd notches (0-indexed) + const monthNum = date.getMonth() + 1; // 1-12 for months + const dayNum = date.getDate(); + label = `${monthNum}/${dayNum}`; + } + + threeMonthLabels.push({ label }); + } + return threeMonthLabels; + + case "1year": + // 12 notches, labels on 2nd, 4th, 6th, 8th, 10th, 12th (indices 1, 3, 5, 7, 9, 11) + const yearLabels = []; + const currentMonth = now.getMonth(); // 0-11 (0=Jan, 1=Feb, etc.) + + for (let i = 0; i < 12; i++) { + const monthOffset = i - 11; // -11, -10, ..., -1, 0 (current month) + const month = new Date(now); + month.setMonth(currentMonth + monthOffset); + + let label = ""; + if (i === 1 || i === 3 || i === 5 || i === 7 || i === 9 || i === 11) { + // Labels on 2nd, 4th, 6th, 8th, 10th, 12th positions (0-indexed: 1, 3, 5, 7, 9, 11) + label = month.toLocaleDateString([], { month: "short" }); + } + + yearLabels.push({ label }); + } + return yearLabels; + + default: + return []; + } + }, [timeDisplayContext, xDomain, data]); + + // Get fill color based on context + const getFillColor = () => { + switch (timeDisplayContext) { + case "1day": + return "#B4C5E0"; // Light blue + case "3day": + return "#A8B8D1"; // Slightly darker blue + case "1week": + return "#98A7C0"; // Purple-blue + case "1month": + return "#8A96B0"; // Darker purple + case "3month": + return "#7C85A0"; // Even darker + case "1year": + return "#6E7490"; // Darkest + default: + return "#98A7C0"; + } + }; + + // Validate inputs after all hooks + if (chartWidth <= 0 || chartHeight <= 0 || data.length === 0) { + return null; + } + + return ( + + + {/* Fill area */} + {fillPath && ( + + )} + + {/* Line */} + {linePath && ( + + + + )} + + {/* Current time indicator for real-time contexts */} + {(timeDisplayContext === "1day" || timeDisplayContext === "3day") && + (() => { + const now = new Date(); + const currentTimeX = xScale(now.getTime()); + return currentTimeX >= 0 && currentTimeX <= chartWidth ? ( + + + + ) : null; + })()} + + + {/* Time labels */} + + {timeLabels.map((labelItem, index) => ( + + + {labelItem.label} + + ))} + + + {/* Tooltips for data points (excluding placeholder points) */} + {data + .filter((point) => !point.label.includes("placeholder")) + .map((point, index) => { + const x = xScale(point.x); + const y = yScale(point.y); + const energyLevel = Math.round(point.y).toString(); + + // Format time based on context + const localDate = new Date(point.x); + let timeText = ""; + + switch (timeDisplayContext) { + case "1day": + timeText = localDate + .toLocaleTimeString([], { + hour: "numeric", + minute: "2-digit", + hour12: true, + }) + .toLowerCase() + .replace(" ", ""); + break; + case "3day": + case "1week": + timeText = localDate.toLocaleDateString([], { weekday: "short" }); + break; + case "1month": + case "3month": + case "1year": + timeText = localDate.toLocaleDateString([], { + month: "short", + day: "numeric", + }); + break; + } + + return ( + + {timeText} + {energyLevel} + + ); + })} + + ); +}; + +const styles = StyleSheet.create({ + labelsContainer: { + flexDirection: "row", + height: 17, + flex: 1, + maxHeight: 17, + position: "absolute", + left: 0, + right: 0, + bottom: 54, + }, + labelItem: { + alignItems: "center", + justifyContent: "flex-start", + flex: 1, + height: 17, + }, + notch: { + height: 3, + width: 0.5, + backgroundColor: "rgba(255,255,255,.3)", + marginBottom: 3, + }, + labelText: { + color: "rgba(255,255,255,.4)", + fontSize: 11, + lineHeight: 11, + textAlign: "center", + minWidth: 50, + height: 11, + }, + tooltip: { + position: "absolute", + width: 46, + alignItems: "center", + justifyContent: "center", + }, + tooltipTime: { + fontSize: 8, + textAlign: "center", + lineHeight: 9, + color: "rgba(255,255,255,.4)", + }, + tooltipEnergy: { + fontSize: 13, + textAlign: "center", + lineHeight: 15, + color: "rgba(255,255,255,1)", + fontWeight: "500", + }, +}); + +export default NewEnergyChart; diff --git a/apps/mobile/src/components/SampleLineChart.tsx b/apps/mobile/src/components/SampleLineChart.tsx new file mode 100644 index 0000000..d98b24f --- /dev/null +++ b/apps/mobile/src/components/SampleLineChart.tsx @@ -0,0 +1,162 @@ +import React, { useEffect, useState } from "react"; +import { Canvas, Path, Skia } from "@shopify/react-native-skia"; +import { curveBasis, line, scaleLinear, scalePoint } from "d3"; +import { + SharedValue, + clamp, + runOnJS, + useSharedValue, + withDelay, + withTiming, +} from "react-native-reanimated"; +import { DataType } from "../sample/data"; +import { + Gesture, + GestureDetector, + PanGestureHandlerEventPayload, +} from "react-native-gesture-handler"; +import XAxisText from "./XAxisText"; +import Cursor from "./Cursor"; +import { getYForX, parse } from "react-native-redash"; +import Gradient from "./Gradient"; + +type Props = { + chartWidth: number; + chartHeight: number; + chartMargin: number; + data: DataType[]; + setSelectedDate: React.Dispatch>; + selectedValue: SharedValue; +}; + +const LineChart = ({ + chartHeight, + chartMargin, + chartWidth, + data, + setSelectedDate, + selectedValue, +}: Props) => { + const [showCursor, setShowCursor] = useState(false); + const animationLine = useSharedValue(0); + const animationGradient = useSharedValue({ x: 0, y: 0 }); + const cx = useSharedValue(20); + const cy = useSharedValue(0); + const totalValue = data.reduce((acc, cur) => acc + cur.value, 0); + + useEffect(() => { + // Animate the line and the gradient + animationLine.value = withTiming(1, { duration: 1000 }); + animationGradient.value = withDelay( + 1000, + withTiming({ x: 0, y: chartHeight }, { duration: 500 }) + ); + selectedValue.value = withTiming(totalValue); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // x domain + const xDomain = data.map((dataPoint: DataType) => dataPoint.label); + + // range of the x scale + const xRange = [chartMargin, chartWidth - chartMargin]; + + // Create the x scale + const x = scalePoint().domain(xDomain).range(xRange).padding(0); + + const stepX = x.step(); + + // Find the max and min values of the data + const max = Math.max(...data.map((val) => val.value)); + const min = Math.min(...data.map((val) => val.value)); + // y domain + const yDomain = [min, max]; + + // range of the y scale + const yRange = [chartHeight, 0]; + + // Create the y scale + const y = scaleLinear().domain(yDomain).range(yRange); + + // Create the curved line + const curvedLine = line() + .x((d) => x(d.label)!) + .y((d) => y(d.value)) + .curve(curveBasis)(data); + + const linePath = Skia.Path.MakeFromSVGString(curvedLine!); + + // Parse the path to get the points + const path = parse(linePath!.toSVGString()); + + // handle the gesture event + const handleGestureEvent = (e: PanGestureHandlerEventPayload) => { + "worklet"; + + const index = Math.floor(e.absoluteX / stepX); + runOnJS(setSelectedDate)(data[index].date); + selectedValue.value = withTiming(data[index].value); + const clampValue = clamp( + Math.floor(e.absoluteX / stepX) * stepX + chartMargin, + chartMargin, + chartWidth - chartMargin + ); + + cx.value = clampValue; + // for some device getYForX returns null for the last point + // so we need to floor the value + cy.value = getYForX(path, Math.floor(clampValue))!; + }; + + // Pan gesture handler + const pan = Gesture.Pan() + .onTouchesDown(() => { + runOnJS(setShowCursor)(true); + }) + .onTouchesUp(() => { + runOnJS(setShowCursor)(false); + selectedValue.value = withTiming(totalValue); + runOnJS(setSelectedDate)("Total"); + }) + .onBegin(handleGestureEvent) + .onChange(handleGestureEvent); + + return ( + + + + + {data.map((dataPoint: DataType, index) => ( + + ))} + {showCursor && } + + + ); +}; + +export default LineChart; diff --git a/apps/mobile/src/components/ServerEnvironmentSelector.tsx b/apps/mobile/src/components/ServerEnvironmentSelector.tsx deleted file mode 100644 index 5bada9a..0000000 --- a/apps/mobile/src/components/ServerEnvironmentSelector.tsx +++ /dev/null @@ -1,363 +0,0 @@ -// Server Environment Selector Component - -import React, { useState, useCallback } from "react"; -import { View, StyleSheet, TouchableOpacity, ScrollView } from "react-native"; -import { useServerEnvironment } from "../context/ServerEnvironmentContext"; -import type { - ServerEnvironmentId, - ServerEnvironment, -} from "../context/ServerEnvironmentTypes"; -import { getRobotoMonoFont } from "../utils/fonts"; -import { Text } from "./Text"; -import { colors, spacing } from "../design-system/tokens"; -import { Card } from "./Card"; - -interface ServerEnvironmentSelectorProps { - onEnvironmentSelected?: (environment: ServerEnvironment) => void; - showCurrentUrl?: boolean; - showFeatures?: boolean; - compact?: boolean; -} - -export const ServerEnvironmentSelector: React.FC = - React.memo( - ({ - onEnvironmentSelected, - showCurrentUrl = true, - showFeatures = false, - compact = false, - }) => { - const { - currentEnvironment, - environments, - isLoading, - switchEnvironment, - getCurrentEnvironment, - getCurrentServerUrl, - } = useServerEnvironment(); - - const [localLoading, setLocalLoading] = - useState(null); - - const handleEnvironmentSwitch = useCallback( - async (environmentId: ServerEnvironmentId) => { - if (currentEnvironment === environmentId) return; - - setLocalLoading(environmentId); - try { - await switchEnvironment(environmentId); - const newEnvironment = environments[environmentId]; - onEnvironmentSelected?.(newEnvironment); - } catch (error) { - // Error handling is done in the context - console.error("Failed to switch environment:", error); - } finally { - setLocalLoading(null); - } - }, - [ - currentEnvironment, - switchEnvironment, - environments, - onEnvironmentSelected, - ] - ); - - const renderEnvironmentOption = useCallback( - ( - environmentId: ServerEnvironmentId, - environment: ServerEnvironment - ) => { - const isSelected = currentEnvironment === environmentId; - const isLoadingThis = localLoading === environmentId; - - return ( - handleEnvironmentSwitch(environmentId)} - disabled={isSelected || isLoadingThis || isLoading} - > - - - {isSelected && } - - - - - - - {environment.name} - - {environment.isDefault && ( - - - Default - - - )} - - - {!compact && ( - - {environment.description} - - )} - - - {environment.url} - - - - - - {environment.environment} - - - - {showFeatures && - !compact && - environment.features.length > 0 && ( - - {environment.features - .slice(0, 2) - .map((feature, index) => ( - - - {feature} - - - ))} - {environment.features.length > 2 && ( - - +{environment.features.length - 2} more - - )} - - )} - - - {isLoadingThis && ( - - - Switching... - - - )} - - - ); - }, - [ - currentEnvironment, - localLoading, - isLoading, - handleEnvironmentSwitch, - showFeatures, - compact, - ] - ); - - const getEnvironmentColor = (environment: string): string => { - switch (environment) { - case "production": - return colors.success; - case "staging": - return colors.warning; - case "development": - return colors.info; - case "mason-development": - return colors.primary[500]; - case "custom": - return colors.neutral[500]; - default: - return colors.neutral[500]; - } - }; - - return ( - - {showCurrentUrl && ( - - - Current Server: - - - {getCurrentServerUrl()} - - - {getCurrentEnvironment().name} •{" "} - {getCurrentEnvironment().environment} - - - )} - - - - Select Environment: - - - - {Object.entries(environments).map( - ([environmentId, environment]) => - renderEnvironmentOption( - environmentId as ServerEnvironmentId, - environment - ) - )} - - - - ); - } - ); - -ServerEnvironmentSelector.displayName = "ServerEnvironmentSelector"; - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - currentUrlCard: { - marginBottom: spacing[4], - }, - currentUrl: { - fontFamily: getRobotoMonoFont("regular"), - marginTop: spacing[1], - }, - environmentsList: { - flex: 1, - }, - sectionTitle: { - marginBottom: spacing[3], - }, - environmentsScrollView: { - flex: 1, - }, - environmentOption: { - flexDirection: "row", - alignItems: "flex-start", - paddingVertical: spacing[3], - paddingHorizontal: spacing[3], - marginBottom: spacing[2], - backgroundColor: colors.background.secondary, - borderRadius: 12, - borderWidth: 1, - borderColor: colors.neutral[200], - }, - environmentOptionSelected: { - borderColor: colors.primary[500], - backgroundColor: colors.primary[50], - }, - radioContainer: { - marginRight: spacing[3], - paddingTop: spacing[1], - }, - radioCircle: { - width: 20, - height: 20, - borderRadius: 10, - borderWidth: 2, - borderColor: colors.neutral[400], - alignItems: "center", - justifyContent: "center", - }, - radioCircleSelected: { - borderColor: colors.primary[500], - }, - radioInner: { - width: 10, - height: 10, - borderRadius: 5, - backgroundColor: colors.primary[500], - }, - environmentContent: { - flex: 1, - }, - environmentHeader: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - marginBottom: spacing[1], - }, - defaultBadge: { - paddingHorizontal: spacing[2], - paddingVertical: spacing[1] / 2, - backgroundColor: colors.success, - borderRadius: 8, - }, - environmentDescription: { - marginBottom: spacing[1], - lineHeight: 18, - }, - environmentUrl: { - fontFamily: getRobotoMonoFont("regular"), - marginBottom: spacing[2], - }, - environmentMeta: { - flexDirection: "row", - alignItems: "center", - flexWrap: "wrap", - gap: spacing[1], - }, - environmentBadge: { - paddingHorizontal: spacing[2], - paddingVertical: spacing[1] / 2, - borderRadius: 8, - }, - featuresContainer: { - flexDirection: "row", - alignItems: "center", - flexWrap: "wrap", - gap: spacing[1], - marginLeft: spacing[2], - }, - featureBadge: { - paddingHorizontal: spacing[2], - paddingVertical: spacing[1] / 2, - backgroundColor: colors.neutral[100], - borderRadius: 6, - }, - loadingIndicator: { - marginTop: spacing[2], - paddingTop: spacing[2], - borderTopWidth: 1, - borderTopColor: colors.neutral[200], - }, -}); diff --git a/apps/mobile/src/components/Text.tsx b/apps/mobile/src/components/Text.tsx index 915af07..494cb24 100644 --- a/apps/mobile/src/components/Text.tsx +++ b/apps/mobile/src/components/Text.tsx @@ -75,7 +75,7 @@ export const Text: React.FC = React.memo( fontWeight: typography.fontWeight.semibold, lineHeight: typography.fontSize.xl * typography.lineHeight.pro, }; - case "header": + case "header": return { fontSize: typography.fontSize.base, fontFamily: getInterFont("semiBold"), diff --git a/apps/mobile/src/components/TidesForBusinessModal.tsx b/apps/mobile/src/components/TidesForBusinessModal.tsx new file mode 100644 index 0000000..c945b15 --- /dev/null +++ b/apps/mobile/src/components/TidesForBusinessModal.tsx @@ -0,0 +1,123 @@ +import React from "react"; +import { + Modal, + View, + TouchableOpacity, + StyleSheet, + ScrollView, +} from "react-native"; +import { X } from "lucide-react-native"; +import { colors, spacing, typography } from "../design-system/tokens"; +import { Text } from "../design-system"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +interface TidesForBusinessModalProps { + isVisible: boolean; + onClose: () => void; +} + +export const TidesForBusinessModal: React.FC = ({ + isVisible, + onClose, +}) => { + const insets = useSafeAreaInsets(); + + return ( + + + {/* Header */} + + + Tides for Work + + + + + + + {/* Content */} + + + + Tides for Work is coming soon. Manage your energy and workflow. + + + Tides is focused on helping you realize patterns and flows that + make your life and work feel more full. + + + + + + Features + + + + • Project-based energy-tracking charts + + + • Link tasks with GitHub and more proejct management tools + + + • Project-specific converations and recommendations + + + + + + + Contact Us + + + Interested in early access? Partnerships? Reach out to + hello@tides-app.com + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.backgroundColor, + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: spacing[4], + paddingVertical: spacing[3], + borderBottomWidth: 1, + borderBottomColor: colors.containerBorder, + }, + closeButton: { + padding: spacing[2], + }, + content: { + flex: 1, + paddingHorizontal: spacing[4], + }, + section: { + paddingVertical: spacing[4], + }, + featureList: { + paddingTop: spacing[2], + gap: spacing[1], + }, +}); diff --git a/apps/mobile/src/components/TimeDisplayToggle.tsx b/apps/mobile/src/components/TimeDisplayToggle.tsx new file mode 100644 index 0000000..004a32e --- /dev/null +++ b/apps/mobile/src/components/TimeDisplayToggle.tsx @@ -0,0 +1,189 @@ +import React, { useState } from "react"; +import { View, Pressable } from "react-native"; +import { LucideBriefcaseBusiness } from "lucide-react-native"; +import { colors, typography } from "../design-system/tokens"; +import { Text } from "./Text"; +import { TidesForBusinessModal } from "./TidesForBusinessModal"; +import { useChartDisplayContext } from "../context/ChartDisplayContext"; + +export type NewTimeContextType = + | "1day" + | "3day" + | "1week" + | "1month" + | "3month" + | "1year"; + +// TimeDisplayToggle: Time range selector with business modal +// - variant: "compact" (hidden) or "full" (visible) +// - currentContext/onContextChange: External control props, defaults to ChartDisplayContext +// - Backward compatibility: Props override context values +interface TimeDisplayToggleProps { + showLabels?: boolean; + variant?: "compact" | "full"; + currentContext?: NewTimeContextType; + onContextChange?: (context: NewTimeContextType) => void; + disabled?: boolean; +} + +export const TimeDisplayToggle: React.FC = ({ + variant = "compact", + currentContext: propCurrentContext, + onContextChange: propOnContextChange, + disabled = false, +}) => { + const { timeRange, setTimeRange } = useChartDisplayContext(); + const [isBusinessModalVisible, setIsBusinessModalVisible] = useState(false); + const [previousContext, setPreviousContext] = + useState(null); + + const currentContext = propCurrentContext ?? timeRange; + const onContextChange = propOnContextChange ?? setTimeRange; + + const contextOptions: { + label: string; + value: NewTimeContextType; + description: string; + }[] = [ + { label: "1D", value: "1day", description: "Today's 24 hours" }, + { label: "3D", value: "3day", description: "Last 3 days (72 hours)" }, + { label: "1W", value: "1week", description: "Last 7 days average" }, + { label: "1M", value: "1month", description: "Last 30 days" }, + { label: "3M", value: "3month", description: "Last 90 days" }, + { label: "1Y", value: "1year", description: "Last 365 days" }, + ]; + + const handleContextSelect = (value: NewTimeContextType) => { + if (disabled) return; + onContextChange(value); + }; + + const handleBusinessModalOpen = () => { + setPreviousContext(currentContext); + setIsBusinessModalVisible(true); + }; + + const handleBusinessModalClose = () => { + setIsBusinessModalVisible(false); + if (previousContext && previousContext !== currentContext) { + onContextChange(previousContext); + } + setPreviousContext(null); + }; + + if (variant === "full") { + return ( + <> + + {contextOptions.map((option) => { + const isSelected = + currentContext === option.value && !isBusinessModalVisible; + const isDisabled = disabled; + + return ( + handleContextSelect(option.value)} + disabled={isDisabled} + style={{ + flex: 1, + alignItems: "center", + height: 44, + justifyContent: "center", + }} + > + + + {option.label} + + + + ); + })} + + + + + + + + + + + ); + } + + return null; +}; diff --git a/apps/mobile/src/components/chat/ChatInput.tsx b/apps/mobile/src/components/chat/ChatInput.tsx index 96d682c..46aca88 100644 --- a/apps/mobile/src/components/chat/ChatInput.tsx +++ b/apps/mobile/src/components/chat/ChatInput.tsx @@ -1,527 +1,111 @@ -import React, { useRef, useState, useEffect, useCallback } from "react"; +import React, { useRef } from "react"; import { View, TextInput, TouchableOpacity, Animated, StyleSheet, - LayoutChangeEvent, + Pressable, Text, - ScrollView, } from "react-native"; -// import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { - ArrowUp, - Plus, - HelpCircle, - Zap, - CheckCircle, - Calendar, - Link, - BarChart3, -} from "lucide-react-native"; +import { ArrowUp, Plus } from "lucide-react-native"; import { colors, spacing, typography } from "../../design-system/tokens"; -import { Text as CustomText } from "../Text"; -import { ToolSuggestion } from "./ToolSuggestion"; - -import type { DetectedTool } from "../../config/toolPhrases"; -import { - detectToolSuggestions, - isExactToolTitle, - type DetectedToolSuggestion, -} from "../../utils/toolDetection"; -import { TOOLS_CONFIG } from "../../config/toolsConfig"; +import { useChat } from "../../context/ChatContext"; +import { useChatMessaging } from "../../hooks/useChatMessaging"; -interface ChatInputProps { - inputMessage: string; - setInputMessage: (message: string) => void; - handleSendMessage: () => Promise; - isLoading: boolean; - toolButtonActive: boolean; - rotationAnim: Animated.Value; - toggleToolMenu: () => void; - toolSuggestion?: DetectedTool | null; - showSuggestion?: boolean; - onAcceptSuggestion?: () => void; - onDismissSuggestion?: () => void; - onHeightChange?: (height: number) => void; - onFocusChange?: (focused: boolean) => void; - templateToInject?: string; // Template from tool menu - onTemplateInjected?: () => void; // Callback when template is injected -} - -export const ChatInput: React.FC = ({ - inputMessage, - setInputMessage, - handleSendMessage, - isLoading, - toolButtonActive, - rotationAnim, - toggleToolMenu, - toolSuggestion, - showSuggestion = false, - onAcceptSuggestion, - onDismissSuggestion, - onHeightChange, - onFocusChange, - templateToInject, - onTemplateInjected, -}) => { +export const ChatInput: React.FC = () => { const inputRef = useRef(null); - const [currentHeight, setCurrentHeight] = useState(0); - - // Tool highlighting state - shows overlay when exact tool title is detected - const [highlightedTool, setHighlightedTool] = useState(null); - - // Tool suggestions state - shows dropdown when keywords are detected - const [toolSuggestions, setToolSuggestions] = useState< - DetectedToolSuggestion[] - >([]); - const [_showSuggestions, setShowSuggestions] = useState(false); - - // Unified overlay animation - const overlayTranslateYAnim = useRef(new Animated.Value(100)).current; // Start translated down - const overlayOpacityAnim = useRef(new Animated.Value(0)).current; - const [overlayType, setOverlayType] = useState< - "suggestions" | "instructions" | null - >(null); - - // const insets = useSafeAreaInsets(); - - // Icon mapping for tool categories - const getCategoryIcon = (category: string) => { - switch (category) { - case "Flow Sessions": - return CheckCircle; - case "Context Management": - return Calendar; - case "Energy & Tasks": - return Zap; - case "Analytics & Data": - return BarChart3; - default: - return Link; - } - }; - - // Unified overlay animation control - const showOverlay = (type: "suggestions" | "instructions") => { - setOverlayType(type); - Animated.parallel([ - Animated.timing(overlayOpacityAnim, { - toValue: 1, - duration: 200, - useNativeDriver: true, - }), - Animated.timing(overlayTranslateYAnim, { - toValue: 0, - duration: 200, - useNativeDriver: true, - }), - ]).start(); - }; - - const hideOverlay = useCallback(() => { - Animated.parallel([ - Animated.timing(overlayOpacityAnim, { - toValue: 0, - duration: 150, - useNativeDriver: true, - }), - Animated.timing(overlayTranslateYAnim, { - toValue: 100, - duration: 150, - useNativeDriver: true, - }), - ]).start(() => { - setOverlayType(null); - }); - }, [overlayOpacityAnim, overlayTranslateYAnim]); - - // Enhanced input change handler with unified tool detection - const handleInputChange = (text: string) => { - setInputMessage(text); - - // Check if input starts with exact tool title for overlay highlighting - const exactToolTitle = isExactToolTitle(text); - if (exactToolTitle) { - // User has typed exact tool title - show instructions overlay - setHighlightedTool(exactToolTitle); - setShowSuggestions(false); - setToolSuggestions([]); - showOverlay("instructions"); - } else { - // No exact tool title - detect suggestions based on keywords - setHighlightedTool(null); - const suggestions = detectToolSuggestions(text); - setToolSuggestions(suggestions); - setShowSuggestions(suggestions.length > 0); - - if (suggestions.length > 0) { - showOverlay("suggestions"); - } else { - hideOverlay(); - } - } - }; - - // Render formatted input text with tool highlighting overlay - const renderFormattedText = () => { - if (!inputMessage || !highlightedTool) { - return inputMessage; - } - - // Tool title should be at the beginning of input - const toolTitleLength = highlightedTool.length; - const restOfText = inputMessage.substring(toolTitleLength); - - return ( - - - {highlightedTool} - - {restOfText} - - ); - }; - - // Handle tool suggestion selection - const handleToolSelect = (suggestion: DetectedToolSuggestion) => { - // Set input to just the tool title (no markers) - setInputMessage(suggestion.title); - // Set highlighted tool for overlay and switch to instructions - setHighlightedTool(suggestion.title); - setShowSuggestions(false); - setToolSuggestions([]); - showOverlay("instructions"); - - // Focus input after selection and position cursor after tool title - setTimeout(() => { - inputRef.current?.focus(); - // Position cursor after the tool title - const cursorPosition = suggestion.title.length; - inputRef.current?.setSelection(cursorPosition, cursorPosition); - }, 100); - }; - - // Get tool configuration for the highlighted tool - const getHighlightedToolConfig = () => { - if (!highlightedTool) return null; - - // Find the tool config by matching title (case-insensitive) - const toolEntry = Object.entries(TOOLS_CONFIG).find( - ([_, config]) => - config.title.toLowerCase() === highlightedTool.toLowerCase() - ); - - return toolEntry ? toolEntry[1] : null; - }; - - // Render tool suggestions overlay - const renderToolSuggestions = () => { - if (!toolSuggestions.length) return null; - - return ( - - - {toolSuggestions.map((suggestion, index) => { - const Icon = getCategoryIcon(suggestion.category); - - return ( - handleToolSelect(suggestion)} - activeOpacity={1} - > - - - - - - {suggestion.title} - - - ); - })} - - - ); - }; - - // Render tool instructions overlay - const renderToolInstructions = () => { - const toolConfig = getHighlightedToolConfig(); - if (!toolConfig) return null; - - // const Icon = getCategoryIcon(toolConfig.category); - const hasRequiredParams = toolConfig.requiredParams.length > 0; - const hasOptionalParams = toolConfig.optionalParams.length > 0; - - return ( - - {/* - - */} - - - {toolConfig.title} - - - {hasRequiredParams && ( - - {toolConfig.requiredParams.map((param, index) => { - const highlightColor = colors.inputPlaceholder; - return ( - - {index === 0 ? " " : ", "} - {param.description} - - ); - })} - - )} - - {hasOptionalParams && ( - - {toolConfig.optionalParams.map((param, index) => { - const highlightColor = colors.inputPlaceholder; - - return ( - - {hasRequiredParams || index > 0 ? ", " : " "} - {param.description} - - ); - })} - - )} - - {!hasRequiredParams && !hasOptionalParams && ( - - - - This tool doesn't require any parameters. Just type the tool - name and press enter. - - - )} - - - ); - }; - - // Handle template injection from tool menu - useEffect(() => { - if (templateToInject) { - setInputMessage(templateToInject); - onTemplateInjected?.(); - - // Templates from tool menu use "/" format, not tool titles - // So we don't apply tool highlighting for templates - setHighlightedTool(null); - setShowSuggestions(false); - setToolSuggestions([]); - hideOverlay(); - - // Focus input and move cursor to first parameter placeholder - setTimeout(() => { - inputRef.current?.focus(); - // Move cursor to first "___" placeholder - const firstPlaceholder = templateToInject.indexOf("___"); - if (firstPlaceholder !== -1) { - inputRef.current?.setSelection( - firstPlaceholder, - firstPlaceholder + 3 - ); - } - }, 100); - } - }, [templateToInject, setInputMessage, onTemplateInjected, hideOverlay]); - - const handleLayout = (event: LayoutChangeEvent) => { - const { height } = event.nativeEvent.layout; - if (height !== currentHeight) { - setCurrentHeight(height); - onHeightChange?.(height); - } - }; + const { + inputMessage, + highlightedTool, + toolMenuOpen, + handleInputChange, + setInputFocused, + toggleToolMenu, + rotationAnim, + toolbar, + } = useChat(); + const { sendMessage, canSendMessage } = useChatMessaging(); return ( - - {/* Tool Suggestion */} - {showSuggestion && - toolSuggestion && - onAcceptSuggestion && - onDismissSuggestion && ( - - - - )} - - {overlayType && ( + + - {overlayType === "suggestions" && renderToolSuggestions()} + - )} - - {overlayType === "instructions" && renderToolInstructions()} - - - - - - - + - + + onFocusChange?.(true)} - onBlur={() => onFocusChange?.(false)} + onSubmitEditing={sendMessage} + onFocus={() => setInputFocused(true)} + onBlur={() => setInputFocused(false)} returnKeyType="send" multiline maxLength={500} + removeClippedSubviews={true} /> {highlightedTool && ( - - {renderFormattedText()} + + {highlightedTool} )} - + + - - - - - + + + ); }; const styles = StyleSheet.create({ - inputContainer: { - backgroundColor: colors.containerBackground, - display: "flex", - // borderWidth: 1, - // borderColor: "red", - flexDirection: "column", - alignItems: "flex-end", - justifyContent: "flex-end", - position: "relative", - }, - suggestionContainer: { - position: "absolute", - bottom: 70, - left: 0, - right: 0, - zIndex: 100, - }, mainRow: { - paddingLeft: 12, - paddingRight: 12, - paddingBottom: 12, + padding: 12, paddingTop: 8, backgroundColor: colors.containerBackground, - display: "flex", flexDirection: "row", alignItems: "flex-end", gap: 10, borderTopColor: colors.containerBorder, borderTopWidth: 0.5, shadowColor: "#000", - shadowOffset: { - width: 0, - height: 4, - }, + shadowOffset: { width: 0, height: 4 }, shadowRadius: 20, shadowOpacity: 0.035, }, @@ -532,38 +116,45 @@ const styles = StyleSheet.create({ borderWidth: 0.5, borderColor: colors.containerBorder, flex: 1, - shadowColor: "#000", - shadowOffset: { - width: 0, - height: 4, - }, - shadowRadius: 20, - shadowOpacity: 0.035, - backgroundColor: "white", borderRadius: 18, maxHeight: 100, }, - messageInput: { + inputContainer: { flex: 1, + position: "relative", + }, + messageInput: { paddingLeft: 12, paddingRight: 48, fontSize: typography.fontSize.base, color: colors.titleColor, - paddingTop: 8, - paddingBottom: 8, + paddingVertical: 8, lineHeight: typography.fontSize.base * typography.lineHeight.pro, }, + toolOverlay: { + position: "absolute", + top: 9.5, + left: 12, + pointerEvents: "none", + zIndex: -1 + }, + toolOverlayText: { + fontSize: typography.fontSize.base, + color: colors.inlineBackground, + backgroundColor: colors.inlineBackground, + paddingHorizontal: 0, + paddingVertical: 0, + borderRadius: 2, + }, toolButton: { height: 34, width: 34, backgroundColor: colors.containerBorderSoft, borderRadius: 100, - display: "flex", alignItems: "center", justifyContent: "center", }, sendButton: { - margin: 0, borderRadius: 1000, width: 36, height: 36, @@ -582,145 +173,7 @@ const styles = StyleSheet.create({ width: 28, height: 28, }, - sendButtonDisabled: { - opacity: 0.5, - }, - sendButtonColorDisabled: { - backgroundColor: colors.buttonDisabled, - }, - messageInputWithHighlight: { - color: "transparent", // Make text transparent when highlighting is active - }, - textOverlay: { - position: "absolute", - top: 0, - left: 0, - right: 48, // Account for send button - - paddingLeft: 12, - paddingTop: 8.5, - paddingBottom: 8, - justifyContent: "flex-start", - pointerEvents: "none", - }, - formattedInputText: { - fontSize: typography.fontSize.base, - lineHeight: typography.fontSize.base * typography.lineHeight.pro, - color: colors.titleColor, - }, - toolHighlight: { - backgroundColor: colors.inlineBackground, // Light purple background - }, - normalText: { - color: colors.titleColor, - }, - // Unified overlay styles - unifiedOverlay: { - width: "100%", - backgroundColor: colors.containerBackground, - borderTopColor: colors.containerBorder, - borderTopWidth: 0.5, - shadowColor: "#000", - shadowOffset: { - width: 0, - height: 4, - }, - shadowRadius: 20, - shadowOpacity: 0.035, - zIndex: 0, - overflow: "hidden", - maxHeight: 58, - height: 58, - gap: 1, - }, - overlayContent: { - flex: 1, - }, - overlayHeader: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginBottom: spacing[2], - }, - overlayDismissButton: { - padding: spacing[1], - }, - // Tool suggestions styles - suggestionsScrollView: {}, - suggestionsScrollContent: {}, - suggestionCard: { - backgroundColor: colors.containerBackground, - borderRadius: 0, - padding: 11, - paddingHorizontal: 16, - paddingLeft: 12, - display: "flex", - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - gap: 10, - height: 58, - borderLeftWidth: 0.5, - borderRightWidth: 0.5, - borderColor: colors.containerBorder, - marginRight: -0.5, - }, - - suggestionIconContainer: { - width: 36, - height: 36, - borderRadius: 10, - alignItems: "center", - justifyContent: "center", - backgroundColor: colors.inlineBackground, - }, - - // Tool instructions styles - instructionsContainer: { - position: "absolute", - flex: 1, - paddingHorizontal: spacing[3], - flexDirection: "row", - alignItems: "flex-start", - justifyContent: "center", - - borderWidth: 0.5, - backgroundColor: colors.containerBackground, - borderRadius: 12, - padding: spacing[4], - borderColor: colors.containerBorder, - shadowColor: "#000", - shadowOffset: { - width: 0, - height: 4, - }, - shadowRadius: 20, - shadowOpacity: 0.035, - elevation: 2, - marginHorizontal: spacing[4], - bottom: 66, - }, - instructionsIconContainer: { - width: 32, - height: 32, - borderRadius: 8, - alignItems: "center", - justifyContent: "center", - backgroundColor: colors.inlineBackground, - }, - instructionsText: { - flex: 1, - }, - noParamsContainer: { - alignItems: "center", - paddingVertical: spacing[6], - gap: spacing[3], - }, - noParamsText: { - textAlign: "center", - paddingHorizontal: spacing[4], - }, - mainRowNoShadow: { - shadowOpacity: 0, - }, + sendButtonDisabled: { opacity: 0.5 }, + sendButtonColorDisabled: { backgroundColor: colors.buttonDisabled }, + mainRowNoShadow: { shadowOpacity: 0 }, }); diff --git a/apps/mobile/src/components/chat/ChatToolbar.tsx b/apps/mobile/src/components/chat/ChatToolbar.tsx new file mode 100644 index 0000000..9313ae1 --- /dev/null +++ b/apps/mobile/src/components/chat/ChatToolbar.tsx @@ -0,0 +1,276 @@ +import React from "react"; +import { + View, + Animated, + StyleSheet, + TouchableOpacity, + ScrollView, + Text, +} from "react-native"; +import { + Zap, + CheckCircle, + Calendar, + Link, + BarChart3, +} from "lucide-react-native"; +import { colors, spacing } from "../../design-system/tokens"; +import { Text as CustomText } from "../Text"; +import { TOOLS_CONFIG } from "../../config/toolsConfig"; +import type { DetectedToolSuggestion } from "../../utils/toolDetection"; +import { useChat } from "../../context/ChatContext"; + +interface ChatToolbarProps { + onToolSelect?: (suggestion: DetectedToolSuggestion) => void; +} + +export const ChatToolbar: React.FC = ({ onToolSelect }) => { + const { toolbar, toolSuggestions, highlightedTool } = useChat(); + + const getCategoryIcon = (category: string) => { + const icons: Record = { + "Core Tides": Zap, + "Flow Sessions": CheckCircle, + "Context Management": Calendar, + "Energy & Tasks": Zap, + "Analytics & Data": BarChart3, + }; + return icons[category] || Link; + }; + + const getHighlightedToolConfig = () => { + if (!highlightedTool) return null; + const toolEntry = Object.entries(TOOLS_CONFIG).find( + ([_, config]) => + config.title.toLowerCase() === highlightedTool.toLowerCase() + ); + return toolEntry ? toolEntry[1] : null; + }; + + const renderToolSuggestions = () => { + if (!toolSuggestions.length) return null; + return ( + + {toolSuggestions.map((suggestion, index) => { + const Icon = getCategoryIcon(suggestion.category); + return ( + onToolSelect?.(suggestion)} + > + + + + + {suggestion.title} + + + ); + })} + + ); + }; + + const renderToolList = () => { + const toolsByCategory: Record< + string, + Array<{ toolId: string; config: any }> + > = {}; + Object.entries(TOOLS_CONFIG).forEach(([toolId, config]) => { + if (!toolsByCategory[config.category]) + toolsByCategory[config.category] = []; + toolsByCategory[config.category].push({ toolId, config }); + }); + + return ( + + {Object.entries(toolsByCategory).map(([category, tools]) => ( + + {category} + {tools.map(({ toolId, config }) => { + const Icon = getCategoryIcon(config.category); + return ( + + onToolSelect?.({ + toolId, + title: config.title, + description: config.description, + category: config.category, + confidence: 1.0, + matchedTriggers: [], + }) + } + > + + + + + {config.title} + + {config.description} + + + + ); + })} + + ))} + + ); + }; + + const renderToolInstructions = () => { + const toolConfig = getHighlightedToolConfig(); + if (!toolConfig) return null; + + return ( + + + {toolConfig.title} + + {toolConfig.requiredParams.map((param, index) => ( + + {index === 0 ? " " : ", "} + {param.description} + + ))} + {toolConfig.optionalParams.map((param, index) => ( + + {toolConfig.requiredParams.length || index > 0 ? ", " : " "} + {param.description} + + ))} + + ); + }; + + return ( + + {toolbar === "list" && ( + {renderToolList()} + )} + {toolbar === "suggestions" && ( + + {renderToolSuggestions()} + + )} + {toolbar === "instructions" && ( + + {renderToolInstructions()} + + )} + + ); +}; + +const styles = StyleSheet.create({ + inputContainer: { + backgroundColor: colors.containerBackground, + flexDirection: "column", + alignItems: "flex-end", + justifyContent: "flex-end", + }, + toolMenuContainer: { + width: "100%", + height: 300, + borderTopColor: colors.containerBorder, + borderTopWidth: 0.5, + }, + toolListScrollContent: { paddingVertical: spacing[2] }, + categorySection: { marginBottom: spacing[3] }, + categoryTitle: { + paddingHorizontal: spacing[4], + paddingVertical: spacing[2], + marginBottom: spacing[1], + fontWeight: "bold", + }, + toolListItem: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: spacing[4], + paddingVertical: spacing[3], + borderBottomWidth: 0.5, + borderBottomColor: colors.containerBorder, + }, + toolListIconContainer: { + width: 36, + height: 36, + borderRadius: 10, + alignItems: "center", + justifyContent: "center", + backgroundColor: colors.inlineBackground, + marginRight: spacing[3], + }, + toolListTextContainer: { flex: 1, gap: spacing[1] }, + toolTitle: { fontWeight: "bold" }, + toolDescription: { fontSize: 12 }, + unifiedOverlay: { + width: "100%", + backgroundColor: colors.containerBackground, + borderTopWidth: 0.5, + borderTopColor: colors.containerBorder, + height: 58, + alignItems: "center", + justifyContent: "center", + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowRadius: 20, + shadowOpacity: 0.035, + }, + instructionsContainer: { + position: "absolute", + backgroundColor: colors.containerBackground, + borderRadius: 12, + padding: 12, + paddingVertical: 12, + marginHorizontal: 12, + borderWidth: .5, + borderColor: colors.containerBorderSoft, + bottom: 8, + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowRadius: 8, + shadowOpacity: 0.08, + }, + suggestionCard: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: 10, + height: 58, + paddingHorizontal: 16, + borderLeftWidth: 0.5, + borderRightWidth: 0.5, + borderColor: colors.containerBorder, + marginRight: -0.5, + }, + suggestionIconContainer: { + width: 36, + height: 36, + borderRadius: 10, + alignItems: "center", + justifyContent: "center", + backgroundColor: colors.inlineBackground, + }, + instructionsText: { flex: 1 }, +}); diff --git a/apps/mobile/src/components/chat/ToolSuggestion.tsx b/apps/mobile/src/components/chat/ToolSuggestion.tsx deleted file mode 100644 index 97348bc..0000000 --- a/apps/mobile/src/components/chat/ToolSuggestion.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import { - View, - TouchableOpacity, - Animated, - StyleSheet, -} from "react-native"; -import { X } from "lucide-react-native"; -import { colors, spacing } from "../../design-system/tokens"; -import type { DetectedTool } from "../../config/toolPhrases"; -import { Text } from "../Text"; - -interface ToolSuggestionProps { - suggestion: DetectedTool | null; - onAccept: (tool: DetectedTool) => void; - onDismiss: () => void; - isVisible: boolean; -} - -export const ToolSuggestion: React.FC = ({ - suggestion, - onAccept, - onDismiss, - isVisible, -}) => { - const fadeAnim = useRef(new Animated.Value(0)).current; - const slideAnim = useRef(new Animated.Value(-50)).current; - const scaleAnim = useRef(new Animated.Value(0.95)).current; - - useEffect(() => { - if (isVisible && suggestion) { - // Animate in - Animated.parallel([ - Animated.timing(fadeAnim, { - toValue: 1, - duration: 200, - useNativeDriver: true, - }), - Animated.timing(slideAnim, { - toValue: 0, - duration: 200, - useNativeDriver: true, - }), - Animated.spring(scaleAnim, { - toValue: 1, - friction: 8, - tension: 40, - useNativeDriver: true, - }), - ]).start(); - } else { - // Animate out - Animated.parallel([ - Animated.timing(fadeAnim, { - toValue: 0, - duration: 150, - useNativeDriver: true, - }), - Animated.timing(slideAnim, { - toValue: -50, - duration: 150, - useNativeDriver: true, - }), - Animated.timing(scaleAnim, { - toValue: 0.95, - duration: 150, - useNativeDriver: true, - }), - ]).start(); - } - }, [isVisible, suggestion, fadeAnim, slideAnim, scaleAnim]); - - if (!suggestion || !isVisible) { - return null; - } - - const Icon = suggestion.metadata.icon; - - return ( - - onAccept(suggestion)} - activeOpacity={0.9} - > - - - - - - - {suggestion.metadata.name} - - - Tap to use • {Math.round(suggestion.confidence * 100)}% match - - - - { - e.stopPropagation(); - onDismiss(); - }} - hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} - > - - - - - {/* Confidence indicator bar */} - - 0.8 - ? colors.success - : suggestion.confidence > 0.6 - ? colors.warning - : colors.neutral[300], - }, - ]} - /> - - - ); -}; - -const styles = StyleSheet.create({ - container: { - position: "absolute", - bottom: 0, - left: spacing[4], - right: spacing[4], - zIndex: 100, - elevation: 10, - }, - suggestionCard: { - flexDirection: "row", - alignItems: "center", - backgroundColor: colors.background.primary, - borderRadius: 12, - padding: spacing[3], - shadowColor: "#000", - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 5, - borderWidth: 1, - borderColor: colors.primary[100], - }, - iconContainer: { - width: 36, - height: 36, - borderRadius: 8, - backgroundColor: colors.primary[50], - alignItems: "center", - justifyContent: "center", - marginRight: spacing[3], - }, - textContainer: { - flex: 1, - justifyContent: "center", - }, - dismissButton: { - padding: spacing[1], - marginLeft: spacing[2], - }, - confidenceBar: { - height: 2, - backgroundColor: colors.neutral[100], - borderRadius: 1, - marginTop: -1, - marginHorizontal: 1, - overflow: "hidden", - }, - confidenceFill: { - height: "100%", - borderRadius: 1, - }, -}); \ No newline at end of file diff --git a/apps/mobile/src/components/data/data.ts b/apps/mobile/src/components/data/data.ts deleted file mode 100644 index 792cfac..0000000 --- a/apps/mobile/src/components/data/data.ts +++ /dev/null @@ -1,322 +0,0 @@ -/** - * Sample energy level data for Tides Mobile App - * Matches Cloudflare database format and mobile upload structure - * - * Energy levels can be: - * - String descriptors: 'low', 'medium', 'high', 'completed' - * - Numeric values: 1-10 scale - * - Mixed format as used throughout the app - */ - -export interface EnergyDataPoint { - id: string; - tide_id: string; - energy_level: string | number; - context?: string; - timestamp: string; - timezone: string; -} - -export interface TideEnergyProgress { - tide_id: string; - tide_title: string; - energy_readings: EnergyDataPoint[]; - average_energy: number; - trend: "increasing" | "decreasing" | "stable"; -} - -// Sample energy data points matching the tide_add_energy format -export const sampleEnergyData: EnergyDataPoint[] = [ - { - id: "energy_001", - tide_id: "daily_2025_08_30", - energy_level: "high", - context: "Morning coffee kicked in, feeling very focused", - timestamp: "2025-08-30T09:15:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_002", - tide_id: "daily_2025_08_30", - energy_level: 8, - context: "Mid-morning energy still strong", - timestamp: "2025-08-30T10:30:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_003", - tide_id: "daily_2025_08_30", - energy_level: "medium", - context: "Post-lunch dip, struggling with concentration", - timestamp: "2025-08-30T13:45:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_004", - tide_id: "daily_2025_08_30", - energy_level: 6, - context: "Afternoon recovery, second wind", - timestamp: "2025-08-30T15:20:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_008", - tide_id: "project_mobile_refactor", - energy_level: "high", - context: "Excited about new architecture improvements", - timestamp: "2025-08-30T11:00:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_009", - tide_id: "project_mobile_refactor", - energy_level: 9, - context: "Deep flow state during component refactoring", - timestamp: "2025-08-30T14:15:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_010", - tide_id: "project_mobile_refactor", - energy_level: "completed", - context: "Successfully completed EnergyChart component fix", - timestamp: "2025-08-30T16:30:00.000Z", - timezone: "America/Los_Angeles", - }, - // August data points spread throughout the month - { - id: "energy_011", - tide_id: "daily_2025_08_05", - energy_level: 7, - context: "Strong Monday start", - timestamp: "2025-08-05T13:00:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_012", - tide_id: "daily_2025_08_08", - energy_level: "high", - context: "Peak energy Thursday", - timestamp: "2025-08-08T15:30:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_013", - tide_id: "daily_2025_08_12", - energy_level: 5, - context: "Monday blues", - timestamp: "2025-08-12T14:00:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_014", - tide_id: "daily_2025_08_15", - energy_level: 8, - context: "Mid-month productivity", - timestamp: "2025-08-15T16:00:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_015", - tide_id: "daily_2025_08_18", - energy_level: "medium", - context: "Weekend prep energy", - timestamp: "2025-08-18T12:00:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_016", - tide_id: "daily_2025_08_22", - energy_level: 9, - context: "Thursday high performance", - timestamp: "2025-08-22T14:30:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_017", - tide_id: "daily_2025_08_25", - energy_level: "low", - context: "Sunday recovery", - timestamp: "2025-08-25T17:00:00.000Z", - timezone: "America/Los_Angeles", - }, - { - id: "energy_018", - tide_id: "daily_2025_08_28", - energy_level: 6, - context: "Wednesday steady pace", - timestamp: "2025-08-28T13:15:00.000Z", - timezone: "America/Los_Angeles", - }, - // August 30-31st data points - { - id: "energy_019", - tide_id: "daily_2025_08_30", - energy_level: "high", - context: "Morning coffee kicked in, feeling very focused", - timestamp: "2025-08-30T13:15:00.000Z", // 9:15 AM EDT = 13:15 UTC - timezone: "America/Los_Angeles", - }, - { - id: "energy_020", - tide_id: "daily_2025_08_30", - energy_level: 8, - context: "Mid-morning energy still strong", - timestamp: "2025-08-30T14:30:00.000Z", // 10:30 AM EDT = 14:30 UTC - timezone: "America/Los_Angeles", - }, - { - id: "energy_021", - tide_id: "daily_2025_08_30", - energy_level: "medium", - context: "Post-lunch dip, struggling with concentration", - timestamp: "2025-08-30T17:45:00.000Z", // 1:45 PM EDT = 17:45 UTC - timezone: "America/Los_Angeles", - }, - { - id: "energy_022", - tide_id: "daily_2025_08_30", - energy_level: 6, - context: "Afternoon recovery, second wind", - timestamp: "2025-08-30T19:20:00.000Z", // 3:20 PM EDT = 19:20 UTC - timezone: "America/Los_Angeles", - }, - { - id: "energy_023", - tide_id: "daily_2025_08_31", - energy_level: "medium", - context: "Early morning start, coffee brewing", - timestamp: "2025-08-31T11:00:00.000Z", // 7:00 AM EDT = 11:00 UTC - timezone: "America/Los_Angeles", - }, - { - id: "energy_024", - tide_id: "daily_2025_08_31", - energy_level: 8, - context: "Morning momentum building, tackling chart animations", - timestamp: "2025-08-31T13:30:00.000Z", // 9:30 AM EDT = 13:30 UTC - timezone: "America/Los_Angeles", - }, - { - id: "energy_025", - tide_id: "daily_2025_08_31", - energy_level: "high", - context: "Flow state achieved working on line chart tutorial", - timestamp: "2025-08-31T15:15:00.000Z", // 11:15 AM EDT = 15:15 UTC - timezone: "America/Los_Angeles", - }, - { - id: "energy_026", - tide_id: "daily_2025_08_31", - energy_level: 7, - context: "Post-lunch focus, debugging animation issues", - timestamp: "2025-08-31T18:00:00.000Z", // 2:00 PM EDT = 18:00 UTC - timezone: "America/Los_Angeles", - }, -]; - -// Sample tide progress data for dashboard/chart display -export const sampleTideProgress: TideEnergyProgress[] = [ - { - tide_id: "daily_2025_08_30", - tide_title: "Daily Focus - Aug 30", - energy_readings: sampleEnergyData.filter( - (e) => e.tide_id === "daily_2025_08_30" - ), - average_energy: 7.25, - trend: "decreasing", - }, - { - tide_id: "weekly_2025_w35", - tide_title: "Week 35 - Aug 25-31", - energy_readings: sampleEnergyData.filter( - (e) => e.tide_id === "weekly_2025_w35" - ), - average_energy: 6.67, - trend: "stable", - }, - { - tide_id: "project_mobile_refactor", - tide_title: "Mobile App Refactoring", - energy_readings: sampleEnergyData.filter( - (e) => e.tide_id === "project_mobile_refactor" - ), - average_energy: 8.67, - trend: "increasing", - }, - { - tide_id: "daily_2025_08_31", - tide_title: "Daily Focus - Aug 31", - energy_readings: sampleEnergyData.filter( - (e) => e.tide_id === "daily_2025_08_31" - ), - average_energy: 7.83, - trend: "increasing", - }, -]; - -// Energy level conversion utilities (matches mobile app logic) -export const energyLevelToNumber = (level: string | number): number => { - if (typeof level === "number") return Math.max(1, Math.min(10, level)); - if (typeof level === "string") { - switch (level.toLowerCase()) { - case "drained": - return 2; - case "low": - return 4; - case "steady": - return 6; - case "strong": - return 8; - case "energized": - return 9; - case "peak": - return 10; - // Legacy support - case "medium": - return 6; - case "high": - return 8; - case "completed": - return 10; - default: { - const parsed = parseInt(level, 10); - return isNaN(parsed) ? 6 : Math.max(1, Math.min(10, parsed)); - } - } - } - return 6; // Default steady -}; - -export const numberToEnergyLevel = (num: number): string => { - if (num <= 2) return "drained"; - if (num <= 4) return "low"; - if (num <= 6) return "steady"; - if (num <= 8) return "strong"; - if (num <= 9) return "energized"; - return "peak"; -}; - -// Chart-ready data transformation -export const getChartData = (tideId?: string) => { - const filteredData = tideId - ? sampleEnergyData.filter((d) => d.tide_id === tideId) - : sampleEnergyData; - - return filteredData.map((point) => ({ - x: new Date(point.timestamp).getTime(), - y: energyLevelToNumber(point.energy_level), - label: point.context || "", - timestamp: point.timestamp, - originalLevel: point.energy_level, - })); -}; - -// Export for EnergyChart component -export default { - sampleEnergyData, - sampleTideProgress, - getChartData, - energyLevelToNumber, - numberToEnergyLevel, -}; diff --git a/apps/mobile/src/components/demo/data.ts b/apps/mobile/src/components/demo/data.ts new file mode 100644 index 0000000..13366b7 --- /dev/null +++ b/apps/mobile/src/components/demo/data.ts @@ -0,0 +1,853 @@ +/** + * Sample energy level data for Tides Mobile App + * Matches Cloudflare database format and mobile upload structure + * + * Energy levels can be: + * - String descriptors: 'low', 'medium', 'high', 'completed' + * - Numeric values: 1-10 scale + * - Mixed format as used throughout the app + */ + +export interface EnergyDataPoint { + id: string; + tide_id: string; + energy_level: string | number; + context?: string; + timestamp: string; + timezone: string; +} + +export interface TideEnergyProgress { + tide_id: string; + tide_title: string; + energy_readings: EnergyDataPoint[]; + average_energy: number; + trend: "increasing" | "decreasing" | "stable"; +} + +// Sample energy data points matching the tide_add_energy format +export const sampleEnergyData: EnergyDataPoint[] = [ + { + id: "energy_001", + tide_id: "daily_2025_08_30", + energy_level: "high", + context: "Morning coffee kicked in, feeling very focused", + timestamp: "2025-08-30T09:15:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_002", + tide_id: "daily_2025_08_30", + energy_level: 8, + context: "Mid-morning energy still strong", + timestamp: "2025-08-30T10:30:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_003", + tide_id: "daily_2025_08_30", + energy_level: "medium", + context: "Post-lunch dip, struggling with concentration", + timestamp: "2025-08-30T13:45:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_004", + tide_id: "daily_2025_08_30", + energy_level: 6, + context: "Afternoon recovery, second wind", + timestamp: "2025-08-30T15:20:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_008", + tide_id: "project_mobile_refactor", + energy_level: "high", + context: "Excited about new architecture improvements", + timestamp: "2025-08-30T11:00:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_009", + tide_id: "project_mobile_refactor", + energy_level: 9, + context: "Deep flow state during component refactoring", + timestamp: "2025-08-30T14:15:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_010", + tide_id: "project_mobile_refactor", + energy_level: "completed", + context: "Successfully completed EnergyChart component fix", + timestamp: "2025-08-30T16:30:00.000Z", + timezone: "America/Los_Angeles", + }, + // August data points spread throughout the month + { + id: "energy_011", + tide_id: "daily_2025_08_05", + energy_level: 7, + context: "Strong Monday start", + timestamp: "2025-08-05T13:00:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_012", + tide_id: "daily_2025_08_08", + energy_level: "high", + context: "Peak energy Thursday", + timestamp: "2025-08-08T15:30:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_013", + tide_id: "daily_2025_08_12", + energy_level: 5, + context: "Monday blues", + timestamp: "2025-08-12T14:00:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_014", + tide_id: "daily_2025_08_15", + energy_level: 8, + context: "Mid-month productivity", + timestamp: "2025-08-15T16:00:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_015", + tide_id: "daily_2025_08_18", + energy_level: "medium", + context: "Weekend prep energy", + timestamp: "2025-08-18T12:00:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_016", + tide_id: "daily_2025_08_22", + energy_level: 9, + context: "Thursday high performance", + timestamp: "2025-08-22T14:30:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_017", + tide_id: "daily_2025_08_25", + energy_level: "low", + context: "Sunday recovery", + timestamp: "2025-08-25T17:00:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_018", + tide_id: "daily_2025_08_28", + energy_level: 6, + context: "Wednesday steady pace", + timestamp: "2025-08-28T13:15:00.000Z", + timezone: "America/Los_Angeles", + }, + // August 30-31st data points + { + id: "energy_019", + tide_id: "daily_2025_08_30", + energy_level: "high", + context: "Morning coffee kicked in, feeling very focused", + timestamp: "2025-08-30T13:15:00.000Z", // 9:15 AM EDT = 13:15 UTC + timezone: "America/Los_Angeles", + }, + { + id: "energy_020", + tide_id: "daily_2025_08_30", + energy_level: 8, + context: "Mid-morning energy still strong", + timestamp: "2025-08-30T14:30:00.000Z", // 10:30 AM EDT = 14:30 UTC + timezone: "America/Los_Angeles", + }, + { + id: "energy_021", + tide_id: "daily_2025_08_30", + energy_level: "medium", + context: "Post-lunch dip, struggling with concentration", + timestamp: "2025-08-30T17:45:00.000Z", // 1:45 PM EDT = 17:45 UTC + timezone: "America/Los_Angeles", + }, + { + id: "energy_022", + tide_id: "daily_2025_08_30", + energy_level: 6, + context: "Afternoon recovery, second wind", + timestamp: "2025-08-30T19:20:00.000Z", // 3:20 PM EDT = 19:20 UTC + timezone: "America/Los_Angeles", + }, + { + id: "energy_023", + tide_id: "daily_2025_08_31", + energy_level: "medium", + context: "Early morning start, coffee brewing", + timestamp: "2025-08-31T11:00:00.000Z", // 7:00 AM EDT = 11:00 UTC + timezone: "America/Los_Angeles", + }, + { + id: "energy_024", + tide_id: "daily_2025_08_31", + energy_level: 8, + context: "Morning momentum building, tackling chart animations", + timestamp: "2025-08-31T13:30:00.000Z", // 9:30 AM EDT = 13:30 UTC + timezone: "America/Los_Angeles", + }, + { + id: "energy_025", + tide_id: "daily_2025_08_31", + energy_level: "high", + context: "Flow state achieved working on line chart tutorial", + timestamp: "2025-08-31T15:15:00.000Z", // 11:15 AM EDT = 15:15 UTC + timezone: "America/Los_Angeles", + }, + { + id: "energy_026", + tide_id: "daily_2025_08_31", + energy_level: 7, + context: "Post-lunch focus, debugging animation issues", + timestamp: "2025-08-31T18:00:00.000Z", // 2:00 PM EDT = 18:00 UTC + timezone: "America/Los_Angeles", + }, + // September 1st data points (Sunday) + { + id: "energy_027", + tide_id: "daily_2025_09_01", + energy_level: 6, + context: "Sunday morning reflection, planning week ahead", + timestamp: "2025-09-01T15:00:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_028", + tide_id: "daily_2025_09_01", + energy_level: "medium", + context: "Afternoon reading, steady energy", + timestamp: "2025-09-01T19:30:00.000Z", + timezone: "America/Los_Angeles", + }, + // September 2nd data points (Monday) + { + id: "energy_029", + tide_id: "daily_2025_09_02", + energy_level: 8, + context: "Monday morning momentum, excited for new week", + timestamp: "2025-09-02T13:00:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_030", + tide_id: "daily_2025_09_02", + energy_level: "high", + context: "Productive coding session, implementing new features", + timestamp: "2025-09-02T16:45:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_031", + tide_id: "daily_2025_09_02", + energy_level: 7, + context: "Evening wind-down, reviewing day's progress", + timestamp: "2025-09-02T21:15:00.000Z", + timezone: "America/Los_Angeles", + }, + // September 3rd data points (Tuesday) + { + id: "energy_032", + tide_id: "daily_2025_09_03", + energy_level: "medium", + context: "Tuesday morning start, coffee brewing", + timestamp: "2025-09-03T14:20:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_033", + tide_id: "daily_2025_09_03", + energy_level: 9, + context: "Flow state during chart optimization work", + timestamp: "2025-09-03T17:00:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_034", + tide_id: "daily_2025_09_03", + energy_level: 5, + context: "Post-lunch energy dip, need movement", + timestamp: "2025-09-03T20:30:00.000Z", + timezone: "America/Los_Angeles", + }, + // September 4th data points (Wednesday) - today + { + id: "energy_035", + tide_id: "daily_2025_09_04", + energy_level: "strong", + context: "Wednesday focus, tackling complex problems", + timestamp: "2025-09-04T15:30:00.000Z", + timezone: "America/Los_Angeles", + }, + { + id: "energy_036", + tide_id: "daily_2025_09_04", + energy_level: 8, + context: "Mid-day productivity peak, debugging success", + timestamp: "2025-09-04T18:45:00.000Z", + timezone: "America/Los_Angeles", + }, +]; + +// Sample tide progress data for dashboard/chart display +export const sampleTideProgress: TideEnergyProgress[] = [ + { + tide_id: "daily_2025_08_30", + tide_title: "Daily Focus - Aug 30", + energy_readings: sampleEnergyData.filter( + (e) => e.tide_id === "daily_2025_08_30" + ), + average_energy: 7.25, + trend: "decreasing", + }, + { + tide_id: "weekly_2025_w35", + tide_title: "Week 35 - Aug 25-31", + energy_readings: sampleEnergyData.filter( + (e) => e.tide_id === "weekly_2025_w35" + ), + average_energy: 6.67, + trend: "stable", + }, + { + tide_id: "project_mobile_refactor", + tide_title: "Mobile App Refactoring", + energy_readings: sampleEnergyData.filter( + (e) => e.tide_id === "project_mobile_refactor" + ), + average_energy: 8.67, + trend: "increasing", + }, + { + tide_id: "daily_2025_08_31", + tide_title: "Daily Focus - Aug 31", + energy_readings: sampleEnergyData.filter( + (e) => e.tide_id === "daily_2025_08_31" + ), + average_energy: 7.83, + trend: "increasing", + }, +]; + +// Energy level conversion utilities (matches mobile app logic) +export const energyLevelToNumber = (level: string | number): number => { + if (typeof level === "number") return Math.max(1, Math.min(10, level)); + if (typeof level === "string") { + switch (level.toLowerCase()) { + case "drained": + return 2; + case "low": + return 4; + case "steady": + return 6; + case "strong": + return 8; + case "energized": + return 9; + case "peak": + return 10; + // Legacy support + case "medium": + return 6; + case "high": + return 8; + case "completed": + return 10; + default: { + const parsed = parseInt(level, 10); + return isNaN(parsed) ? 6 : Math.max(1, Math.min(10, parsed)); + } + } + } + return 6; // Default steady +}; + +export const numberToEnergyLevel = (num: number): string => { + if (num <= 2) return "drained"; + if (num <= 4) return "low"; + if (num <= 6) return "steady"; + if (num <= 8) return "strong"; + if (num <= 9) return "energized"; + return "peak"; +}; + +// Chart-ready data transformation +export const getChartData = (tideId?: string) => { + const filteredData = tideId + ? sampleEnergyData.filter((d) => d.tide_id === tideId) + : sampleEnergyData; + + return filteredData.map((point) => ({ + x: new Date(point.timestamp).getTime(), + y: energyLevelToNumber(point.energy_level), + label: point.context || "", + timestamp: point.timestamp, + originalLevel: point.energy_level, + })); +}; + +// New time context aware chart data function with aligned notch positioning +export const getTimeContextChartData = ( + timeContext: "1day" | "3day" | "1week" | "1month" | "3month" | "1year" +) => { + // Use September 4, 2025 as "now" to match our sample data + const now = new Date("2025-09-04T20:00:00.000Z"); + let startDate = new Date(now); + + // Calculate start date based on time context + switch (timeContext) { + case "1day": + startDate.setDate(now.getDate() - 1); + break; + case "3day": + startDate.setDate(now.getDate() - 3); + break; + case "1week": + startDate.setDate(now.getDate() - 7); + break; + case "1month": + startDate.setDate(now.getDate() - 31); + break; + case "3month": + startDate.setDate(now.getDate() - 90); + break; + case "1year": + startDate.setDate(now.getDate() - 365); + break; + } + + // Filter existing hardcoded sample data to the time range + const filteredSampleData = sampleEnergyData.filter((point) => { + const pointTime = new Date(point.timestamp).getTime(); + return pointTime >= startDate.getTime() && pointTime <= now.getTime(); + }); + + const totalDuration = now.getTime() - startDate.getTime(); + + if (timeContext === "1day") { + // Place data points aligned with notch positions (23 notches for hours 1-23) + const dataPoints = filteredSampleData + .map((point) => { + const pointDate = new Date(point.timestamp); + const hour = pointDate.getHours(); + const minutes = pointDate.getMinutes(); + + // Convert to 1-23 hour range (midnight = hour 24, but we skip it in 1day) + let displayHour = hour === 0 ? 24 : hour; + + // Skip hour 24 (midnight) for 1day context, only show hours 1-23 + if (displayHour === 24) return null; + + // Position based on notch index: hour 1 = notch 0, hour 12 = notch 11, hour 23 = notch 22 + const notchIndex = displayHour - 1; + const minuteProgress = minutes / 60; + const notchPosition = (notchIndex + minuteProgress) / 23; + const xPosition = startDate.getTime() + notchPosition * totalDuration; + + return { + x: xPosition, + y: energyLevelToNumber(point.energy_level), + label: point.context || "", + timestamp: point.timestamp, + originalLevel: point.energy_level, + }; + }) + .filter((point) => point !== null) + .sort((a, b) => a.x - b.x); + + // Add placeholder points at far left and far right for natural curve + if (dataPoints.length > 0) { + const firstPoint = dataPoints[0]; + const lastPoint = dataPoints[dataPoints.length - 1]; + + // Add left edge placeholder (use first point's energy level) + dataPoints.unshift({ + x: startDate.getTime(), + y: firstPoint.y, + label: "Start placeholder", + timestamp: new Date(startDate.getTime()).toISOString(), + originalLevel: firstPoint.originalLevel, + }); + + // Add right edge placeholder (use last point's energy level) + dataPoints.push({ + x: startDate.getTime() + totalDuration, + y: lastPoint.y, + label: "End placeholder", + timestamp: new Date(startDate.getTime() + totalDuration).toISOString(), + originalLevel: lastPoint.originalLevel, + }); + } + + return dataPoints; + } + + if (timeContext === "3day") { + // Place data points at their exact timestamps, no aggregation + const dataPoints = filteredSampleData + .map((point) => ({ + x: new Date(point.timestamp).getTime(), + y: energyLevelToNumber(point.energy_level), + label: point.context || "", + timestamp: point.timestamp, + originalLevel: point.energy_level, + })) + .sort((a, b) => a.x - b.x); + + // Add placeholder points at far left and far right for natural curve + if (dataPoints.length > 0) { + const firstPoint = dataPoints[0]; + const lastPoint = dataPoints[dataPoints.length - 1]; + + // Add left edge placeholder (use first point's energy level) + dataPoints.unshift({ + x: startDate.getTime(), + y: firstPoint.y, + label: "Start placeholder", + timestamp: new Date(startDate.getTime()).toISOString(), + originalLevel: firstPoint.originalLevel, + }); + + // Add right edge placeholder (use last point's energy level) + dataPoints.push({ + x: startDate.getTime() + totalDuration, + y: lastPoint.y, + label: "End placeholder", + timestamp: new Date(startDate.getTime() + totalDuration).toISOString(), + originalLevel: lastPoint.originalLevel, + }); + } + + return dataPoints; + } + + if (timeContext === "1week") { + // 7 notches, one for each day + const dailyBuckets = new Map< + number, + { points: EnergyDataPoint[]; energies: number[] } + >(); + + // Initialize all 7 days + for (let i = 0; i < 7; i++) { + dailyBuckets.set(i, { points: [], energies: [] }); + } + + // Group by day index (0-6) + filteredSampleData.forEach((point) => { + const pointDate = new Date(point.timestamp); + const dayIndex = Math.floor( + (pointDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000) + ); + + if (dayIndex >= 0 && dayIndex < 7) { + const bucket = dailyBuckets.get(dayIndex)!; + bucket.points.push(point); + bucket.energies.push(energyLevelToNumber(point.energy_level)); + } + }); + + const result = []; + const dayNames = ["S", "M", "T", "W", "T", "F", "S"]; + + for (let dayIndex = 0; dayIndex < 7; dayIndex++) { + const bucket = dailyBuckets.get(dayIndex)!; + if (bucket.points.length > 0) { + const avgEnergy = + bucket.energies.reduce((sum, e) => sum + e, 0) / + bucket.energies.length; + // Position at center of each day's notch + const notchPosition = (dayIndex + 0.5) / 7; + const xPosition = startDate.getTime() + notchPosition * totalDuration; + + result.push({ + x: xPosition, + y: avgEnergy, + label: `${dayNames[dayIndex]} avg: ${avgEnergy.toFixed(1)}`, + timestamp: new Date(xPosition).toISOString(), + originalLevel: Math.round(avgEnergy), + }); + } + } + + // Add placeholder points at far left and far right for natural curve + if (result.length > 0) { + const firstPoint = result[0]; + const lastPoint = result[result.length - 1]; + + // Add left edge placeholder (use first point's energy level) + result.unshift({ + x: startDate.getTime(), + y: firstPoint.y, + label: "Start placeholder", + timestamp: new Date(startDate.getTime()).toISOString(), + originalLevel: firstPoint.originalLevel, + }); + + // Add right edge placeholder (use last point's energy level) + result.push({ + x: startDate.getTime() + totalDuration, + y: lastPoint.y, + label: "End placeholder", + timestamp: new Date(startDate.getTime() + totalDuration).toISOString(), + originalLevel: lastPoint.originalLevel, + }); + } + + return result; + } + + if (timeContext === "1month") { + // 31 notches, one for each day + const dailyBuckets = new Map< + number, + { points: EnergyDataPoint[]; energies: number[] } + >(); + + // Initialize all 31 days + for (let day = 0; day < 31; day++) { + dailyBuckets.set(day, { points: [], energies: [] }); + } + + // Group by day index (0-30) + filteredSampleData.forEach((point) => { + const pointDate = new Date(point.timestamp); + const dayIndex = Math.floor( + (pointDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000) + ); + + if (dayIndex >= 0 && dayIndex < 31) { + const bucket = dailyBuckets.get(dayIndex)!; + bucket.points.push(point); + bucket.energies.push(energyLevelToNumber(point.energy_level)); + } + }); + + const result = []; + for (let dayIndex = 0; dayIndex < 31; dayIndex++) { + const bucket = dailyBuckets.get(dayIndex)!; + if (bucket.points.length > 0) { + const avgEnergy = + bucket.energies.reduce((sum, e) => sum + e, 0) / + bucket.energies.length; + // Position at center of each day's notch + const notchPosition = (dayIndex + 0.5) / 31; + const xPosition = startDate.getTime() + notchPosition * totalDuration; + + result.push({ + x: xPosition, + y: avgEnergy, + label: `Day ${dayIndex + 1} avg: ${avgEnergy.toFixed(1)}`, + timestamp: new Date(xPosition).toISOString(), + originalLevel: Math.round(avgEnergy), + }); + } + } + + // Add placeholder points at far left and far right for natural curve + if (result.length > 0) { + const firstPoint = result[0]; + const lastPoint = result[result.length - 1]; + + // Add left edge placeholder (use first point's energy level) + result.unshift({ + x: startDate.getTime(), + y: firstPoint.y, + label: "Start placeholder", + timestamp: new Date(startDate.getTime()).toISOString(), + originalLevel: firstPoint.originalLevel, + }); + + // Add right edge placeholder (use last point's energy level) + result.push({ + x: startDate.getTime() + totalDuration, + y: lastPoint.y, + label: "End placeholder", + timestamp: new Date(startDate.getTime() + totalDuration).toISOString(), + originalLevel: lastPoint.originalLevel, + }); + } + + return result; + } + + if (timeContext === "3month") { + // 91 notches, one for each day + const dailyBuckets = new Map< + number, + { points: EnergyDataPoint[]; energies: number[] } + >(); + + // Initialize all 91 days + for (let day = 0; day < 91; day++) { + dailyBuckets.set(day, { points: [], energies: [] }); + } + + // Group by day index (0-90) + filteredSampleData.forEach((point) => { + const pointDate = new Date(point.timestamp); + const dayIndex = Math.floor( + (pointDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000) + ); + + if (dayIndex >= 0 && dayIndex < 91) { + const bucket = dailyBuckets.get(dayIndex)!; + bucket.points.push(point); + bucket.energies.push(energyLevelToNumber(point.energy_level)); + } + }); + + const result = []; + for (let dayIndex = 0; dayIndex < 91; dayIndex++) { + const bucket = dailyBuckets.get(dayIndex)!; + if (bucket.points.length > 0) { + const avgEnergy = + bucket.energies.reduce((sum, e) => sum + e, 0) / + bucket.energies.length; + // Position at center of each day's notch + const notchPosition = (dayIndex + 0.5) / 91; + const xPosition = startDate.getTime() + notchPosition * totalDuration; + + result.push({ + x: xPosition, + y: avgEnergy, + label: `Day ${dayIndex + 1} avg: ${avgEnergy.toFixed(1)}`, + timestamp: new Date(xPosition).toISOString(), + originalLevel: Math.round(avgEnergy), + }); + } + } + + // Add placeholder points at far left and far right for natural curve + if (result.length > 0) { + const firstPoint = result[0]; + const lastPoint = result[result.length - 1]; + + // Add left edge placeholder (use first point's energy level) + result.unshift({ + x: startDate.getTime(), + y: firstPoint.y, + label: "Start placeholder", + timestamp: new Date(startDate.getTime()).toISOString(), + originalLevel: firstPoint.originalLevel, + }); + + // Add right edge placeholder (use last point's energy level) + result.push({ + x: startDate.getTime() + totalDuration, + y: lastPoint.y, + label: "End placeholder", + timestamp: new Date(startDate.getTime() + totalDuration).toISOString(), + originalLevel: lastPoint.originalLevel, + }); + } + + return result; + } + + if (timeContext === "1year") { + // 12 notches, one for each month + const monthlyBuckets = new Map< + number, + { points: EnergyDataPoint[]; energies: number[] } + >(); + + // Initialize all 12 months + for (let month = 0; month < 12; month++) { + monthlyBuckets.set(month, { points: [], energies: [] }); + } + + // Group by month index (0-11) + filteredSampleData.forEach((point) => { + const pointDate = new Date(point.timestamp); + const monthIndex = Math.floor( + (pointDate.getTime() - startDate.getTime()) / + (30.44 * 24 * 60 * 60 * 1000) + ); // Avg days per month + + if (monthIndex >= 0 && monthIndex < 12) { + const bucket = monthlyBuckets.get(monthIndex)!; + bucket.points.push(point); + bucket.energies.push(energyLevelToNumber(point.energy_level)); + } + }); + + const result = []; + for (let monthIndex = 0; monthIndex < 12; monthIndex++) { + const bucket = monthlyBuckets.get(monthIndex)!; + if (bucket.points.length > 0) { + const avgEnergy = + bucket.energies.reduce((sum, e) => sum + e, 0) / + bucket.energies.length; + // Position at center of each month's notch + const notchPosition = (monthIndex + 0.5) / 12; + const xPosition = startDate.getTime() + notchPosition * totalDuration; + + result.push({ + x: xPosition, + y: avgEnergy, + label: `Month ${monthIndex + 1} avg: ${avgEnergy.toFixed(1)}`, + timestamp: new Date(xPosition).toISOString(), + originalLevel: Math.round(avgEnergy), + }); + } + } + + // Add placeholder points at far left and far right for natural curve + if (result.length > 0) { + const firstPoint = result[0]; + const lastPoint = result[result.length - 1]; + + // Add left edge placeholder (use first point's energy level) + result.unshift({ + x: startDate.getTime(), + y: firstPoint.y, + label: "Start placeholder", + timestamp: new Date(startDate.getTime()).toISOString(), + originalLevel: firstPoint.originalLevel, + }); + + // Add right edge placeholder (use last point's energy level) + result.push({ + x: startDate.getTime() + totalDuration, + y: lastPoint.y, + label: "End placeholder", + timestamp: new Date(startDate.getTime() + totalDuration).toISOString(), + originalLevel: lastPoint.originalLevel, + }); + } + + return result; + } + + // Fallback: return individual data points (shouldn't reach here) + return filteredSampleData + .map((point) => ({ + x: new Date(point.timestamp).getTime(), + y: energyLevelToNumber(point.energy_level), + label: point.context || "", + timestamp: point.timestamp, + originalLevel: point.energy_level, + })) + .sort((a, b) => a.x - b.x); +}; + +// Export for EnergyChart component +export default { + sampleEnergyData, + sampleTideProgress, + getChartData, + getTimeContextChartData, + energyLevelToNumber, + numberToEnergyLevel, +}; diff --git a/apps/mobile/src/components/tools/ToolCallDisplay.tsx b/apps/mobile/src/components/tools/ToolCallDisplay.tsx deleted file mode 100644 index 3f789c3..0000000 --- a/apps/mobile/src/components/tools/ToolCallDisplay.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React from "react"; -import { View, StyleSheet } from "react-native"; -import { Text } from "../Text"; -import { Card } from "../Card"; -import { colors, spacing } from "../../design-system/tokens"; -import type { MCPToolCall } from "../../types/chat"; - -interface ToolCallDisplayProps { - toolCall: MCPToolCall; -} - -export const ToolCallDisplay: React.FC = ({ toolCall }) => { - const getStatusColor = () => { - switch (toolCall.status) { - case "completed": - return colors.success; - case "failed": - return colors.error; - case "executing": - return colors.warning; - default: - return colors.neutral[500]; - } - }; - - const getStatusIcon = () => { - switch (toolCall.status) { - case "completed": - return "✓"; - case "failed": - return "✗"; - case "executing": - return "⏳"; - default: - return "⏸"; - } - }; - - return ( - - - - {getStatusIcon()} {toolCall.name} - - - {toolCall.status} - - - - {Object.keys(toolCall.parameters).length > 0 && ( - - - Parameters: - - {Object.entries(toolCall.parameters).map(([key, value]) => ( - - • {key}: {String(value)} - - ))} - - )} - - {toolCall.error && ( - - Error: {toolCall.error} - - )} - - ); -}; - -const styles = StyleSheet.create({ - toolCallCard: { - marginVertical: spacing[2], - borderLeftWidth: 3, - }, - toolCallHeader: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginBottom: spacing[2], - }, - toolCallParams: { - marginTop: spacing[2], - }, - toolCallError: { - marginTop: spacing[2], - backgroundColor: colors.error + "10", - padding: spacing[2], - borderRadius: 4, - }, -}); \ No newline at end of file diff --git a/apps/mobile/src/components/tools/ToolMenu.tsx b/apps/mobile/src/components/tools/ToolMenu.tsx deleted file mode 100644 index 7bd2ffe..0000000 --- a/apps/mobile/src/components/tools/ToolMenu.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import React from "react"; -import { - View, - TouchableOpacity, - ScrollView, - Animated, - StyleSheet, -} from "react-native"; -import { - CheckCircle, - Zap, - Link, - FileText, - BarChart3, - Users, - ArrowUpDown, - Calendar, - Copy, -} from "lucide-react-native"; -import { Text } from "../Text"; -import { colors, spacing } from "../../design-system/tokens"; - -interface ToolMenuProps { - showToolMenu: boolean; - menuHeightAnim: Animated.Value; - handleToolSelect: ( - toolName: string, - customParameters?: Record - ) => Promise; - handleAgentCommand: (command: string) => Promise; - toggleToolMenu: () => void; - scrollable?: boolean; - getToolAvailability: (toolName: string) => { - available: boolean; - reason: string; - }; - onCopyConversation: () => void; -} - -interface ToolButtonProps { - toolName?: string; - icon: any; - title: string; - handleToolSelect?: (toolName: string) => Promise; - getToolAvailability?: (toolName: string) => { - available: boolean; - reason: string; - }; - onPress?: () => void; - disabled?: boolean; -} - -const ToolButton: React.FC = ({ - toolName, - icon: Icon, - title, - handleToolSelect, - getToolAvailability, - onPress, - disabled = false, -}) => { - const availability = - toolName && getToolAvailability - ? getToolAvailability(toolName) - : { available: true, reason: "" }; - const isDisabled = Boolean(disabled) || (toolName && !availability.available); - - const handlePress = () => { - if (onPress) { - onPress(); - } else if (handleToolSelect && toolName) { - handleToolSelect(toolName); - } - }; - - return ( - - - - - - - {title} - - - - ); -}; - -export const ToolMenu: React.FC = ({ - showToolMenu, - menuHeightAnim, - handleToolSelect, - handleAgentCommand: _handleAgentCommand, - toggleToolMenu: _toggleToolMenu, - scrollable = true, - getToolAvailability, - onCopyConversation, -}) => { - if (!showToolMenu) { - return null; - } - - return ( - - - {/* Flow Sessions */} - - - FLOW SESSIONS - - - - - - {/* Context Management */} - - - CONTEXT MANAGEMENT - - - - - - - - {/* Energy & Tasks */} - - - ENERGY & TASKS - - - - - - - - - - {/* Analytics & Data */} - - - ANALYTICS & DATA - - - - - - - - - - {/* Utilities */} - - - UTILITIES - - - - - - - ); -}; - -const styles = StyleSheet.create({ - toolMenu: { - backgroundColor: colors.background.secondary, - width: "100%", - overflow: "hidden", // Important for smooth height animation - borderTopWidth: 0.5, - borderTopColor: colors.neutral[200], - shadowColor: "#000", - shadowOffset: { - width: 0, - height: -0.5, - }, - shadowRadius: 0.5, - shadowOpacity: 0.03, - maxHeight: 500, - position: "absolute", - bottom: 0, - left: 0, - right: 0, - paddingBottom: 58.5, - }, - toolMenuScroll: { - flex: 1, - }, - toolMenuItem: { - flexDirection: "row", - alignItems: "center", - paddingHorizontal: spacing[4], - height: 56, - borderTopWidth: 1, - borderTopColor: colors.neutral[200], - }, - toolMenuItemIcon: { - width: 24, - height: 24, - borderRadius: 0, - alignItems: "center", - justifyContent: "center", - marginRight: spacing[2], - }, - toolMenuItemContent: { - display: "flex", - flexDirection: "row", - alignItems: "center", - }, - toolMenuItemTitle: {}, - toolMenuItemDisabled: { - opacity: 0.5, - }, - menuSection: { - marginBottom: spacing[3], - }, - sectionHeader: { - paddingHorizontal: spacing[4], - paddingVertical: spacing[2], - fontWeight: "600", - letterSpacing: 0.5, - }, -}); diff --git a/apps/mobile/src/config/toolPhrases.ts b/apps/mobile/src/config/toolPhrases.ts index c974d91..6612d19 100644 --- a/apps/mobile/src/config/toolPhrases.ts +++ b/apps/mobile/src/config/toolPhrases.ts @@ -132,20 +132,8 @@ export const TOOL_PHRASES: ToolPhrase[] = [ /^(create|new)\s+my\s+tide/i, ], priority: 10, - extractParams: (match) => { - const text = match[0].toLowerCase(); - const params: Record = {}; - - // Extract flow type from context - if (text.includes("work")) params.flowType = "work"; - else if (text.includes("personal")) params.flowType = "personal"; - else if (text.includes("daily")) params.flowType = "daily"; - else if (text.includes("project")) params.flowType = "project"; - - return params; - }, }, - + // Start Flow variations { toolId: "startTideFlow", @@ -161,22 +149,23 @@ export const TOOL_PHRASES: ToolPhrase[] = [ extractParams: (match) => { const text = match[0].toLowerCase(); const params: Record = {}; - + // Extract intensity if (text.includes("gentle")) params.intensity = "gentle"; else if (text.includes("moderate")) params.intensity = "moderate"; - else if (text.includes("intense") || text.includes("strong")) params.intensity = "strong"; - + else if (text.includes("intense") || text.includes("strong")) + params.intensity = "strong"; + // Extract duration if mentioned const durationMatch = text.match(/(\d+)\s*min/); if (durationMatch) { params.duration = parseInt(durationMatch[1], 10); } - + return params; }, }, - + // List/Show Tides { toolId: "listTides", @@ -189,7 +178,7 @@ export const TOOL_PHRASES: ToolPhrase[] = [ ], priority: 8, }, - + // Add Energy { toolId: "addEnergyToTide", @@ -204,15 +193,16 @@ export const TOOL_PHRASES: ToolPhrase[] = [ extractParams: (match) => { const text = match[0].toLowerCase(); const params: Record = {}; - + if (text.includes("low")) params.energyLevel = "low"; - else if (text.includes("moderate") || text.includes("medium")) params.energyLevel = "moderate"; + else if (text.includes("moderate") || text.includes("medium")) + params.energyLevel = "moderate"; else if (text.includes("high")) params.energyLevel = "high"; - + return params; }, }, - + // Link Task { toolId: "linkTaskToTide", @@ -224,7 +214,7 @@ export const TOOL_PHRASES: ToolPhrase[] = [ ], priority: 6, }, - + // View Task Links { toolId: "getTaskLinks", @@ -236,7 +226,7 @@ export const TOOL_PHRASES: ToolPhrase[] = [ ], priority: 5, }, - + // Get Report { toolId: "getTideReport", @@ -249,7 +239,7 @@ export const TOOL_PHRASES: ToolPhrase[] = [ ], priority: 4, }, - + // View Participants { toolId: "getTideParticipants", @@ -261,7 +251,7 @@ export const TOOL_PHRASES: ToolPhrase[] = [ ], priority: 3, }, - + // Agent Commands - Insights { toolId: "getInsights", @@ -273,7 +263,7 @@ export const TOOL_PHRASES: ToolPhrase[] = [ ], priority: 8, }, - + // Agent Commands - Analyze { toolId: "analyzeTides", @@ -285,7 +275,7 @@ export const TOOL_PHRASES: ToolPhrase[] = [ ], priority: 7, }, - + // Agent Commands - Recommendations { toolId: "getRecommendations", @@ -308,4 +298,4 @@ export interface DetectedTool { confidence: number; extractedParams?: Record; matchedPattern?: string; -} \ No newline at end of file +} diff --git a/apps/mobile/src/config/toolsConfig.ts b/apps/mobile/src/config/toolsConfig.ts index 3a81bcf..60b180e 100644 --- a/apps/mobile/src/config/toolsConfig.ts +++ b/apps/mobile/src/config/toolsConfig.ts @@ -22,97 +22,94 @@ export interface ToolConfig { } export const TOOLS_CONFIG: Record = { - // Flow Sessions - tide_smart_flow: { - title: "Start Flow", - description: "Create a Pomodoro-style flow session", - category: "Flow Sessions", - requiredParams: [], - optionalParams: [ + // Core Tide Management + tide_create: { + title: "Create Tide", + description: "Create a new tidal workflow for productivity", + category: "Core Tides", + requiredParams: [ { - name: "work_context", - description: "what you're working on", - example: "implementing auth, reviewing PRs, writing docs", + name: "name", + description: "name for your tide", + example: "Morning Writing, Mobile Refactor, Weekly Sprint", type: "text", }, + ], + optionalParams: [ { - name: "initial_energy", - description: "starting energy level", - example: "high, medium, low, 8", + name: "description", + description: "detailed purpose of this tide", + example: "Daily writing practice, Q4 mobile app refactor", type: "text", }, - { - name: "duration", - description: "session length in minutes", - example: "25, 45, 90", - type: "number", - }, - { - name: "intensity", - description: "work intensity", - example: "gentle, moderate, strong", - type: "select", - options: ["gentle", "moderate", "strong"], - }, ], triggers: [ - "start flow", - "begin session", - "pomodoro", - "focus session", - "work session", - "start working", - "deep work", - "flow state", - "productivity session", - "time block", - "concentration", - "focused time", + "create tide", + "new tide", + "start tide", + "make tide", + "begin tide", + "new workflow", + "create workflow", + "start project", + "new project", + "new habit", + "start habit", ], }, - // Context Management - tide_switch_context: { - title: "Switch Context", - description: "Switch between daily, weekly, monthly views", - category: "Context Management", + tide_flow: { + title: "Start Flow", + description: "Begin focused work session in a tide", + category: "Core Tides", requiredParams: [ { - name: "context", - description: "which view you want", - example: "daily, weekly, monthly", - type: "select", - options: ["daily", "weekly", "monthly"], + name: "tide_id", + description: "which tide to flow in", + example: "tide_1234567890_abc, use 'current' for active tide", + type: "text", }, ], optionalParams: [ { - name: "date", - description: "target date (ISO format)", - example: "2025-08-25, 2025-01-15", + name: "intensity", + description: "work intensity level", + example: "gentle, moderate, strong", + type: "select", + options: ["gentle", "moderate", "strong"], + }, + { + name: "duration", + description: "session length in minutes", + example: "25, 45, 90", + type: "number", + }, + { + name: "initial_energy", + description: "starting energy level", + example: "high, medium, low, 8", type: "text", }, { - name: "create_if_missing", - description: "create context if it doesn't exist", - example: "true, false", - type: "select", - options: ["true", "false"], + name: "work_context", + description: "what you're focusing on", + example: "code review, writing docs, fixing bugs", + type: "text", }, ], triggers: [ - "switch context", - "change view", - "daily view", - "weekly view", - "monthly view", - "context switch", - "change context", - "switch to", - "view daily", - "view weekly", - "view monthly", - "change period", + "start flow", + "begin flow", + "flow session", + "work session", + "pomodoro", + "focus session", + "start timer", + "begin session", + "tide flow", + "flow tide", + "work flow", + "focus time", ], }, diff --git a/apps/mobile/src/context/AuthContext.tsx b/apps/mobile/src/context/AuthContext.tsx index 92b07b6..bc95499 100644 --- a/apps/mobile/src/context/AuthContext.tsx +++ b/apps/mobile/src/context/AuthContext.tsx @@ -30,7 +30,6 @@ interface AuthProviderProps { export function AuthProvider({ children }: AuthProviderProps) { const [state, dispatch] = useReducer(authReducer, initialAuthState); - useEffect(() => { loggingService.info("AuthContext", "Initializing auth context", undefined); @@ -39,22 +38,34 @@ export function AuthProvider({ children }: AuthProviderProps) { try { // Step 1: Check for stored API key const apiKey = await secureStorage.getItem("api_key"); - loggingService.info("AuthContext", "Retrieved API key from SecureStorage", { hasApiKey: !!apiKey }); - + loggingService.info( + "AuthContext", + "Retrieved API key from SecureStorage", + { hasApiKey: !!apiKey } + ); + if (!apiKey) { // No API key - user needs to authenticate - loggingService.info("AuthContext", "No API key found, user needs to authenticate", {}); + loggingService.info( + "AuthContext", + "No API key found, user needs to authenticate", + {} + ); dispatch({ type: "CLEAR_AUTH" }); return; } - - loggingService.info("AuthContext", "Found stored API key, verifying with Supabase", { - apiKeyLength: apiKey.length - }); - + + loggingService.info( + "AuthContext", + "Found stored API key, verifying with Supabase", + { + apiKeyLength: apiKey.length, + } + ); + // Step 2: Verify with Supabase const verification = await authService.verifyStoredAuth(); - + if (verification.isValid) { // Valid user - set authenticated state let user = verification.user; @@ -63,23 +74,29 @@ export function AuthProvider({ children }: AuthProviderProps) { const userId = extractUserIdFromApiKey(apiKey); if (userId) { user = { id: userId } as any; - loggingService.info("AuthContext", "Created user object from API key", { userId }); + loggingService.info( + "AuthContext", + "Created user object from API key", + { userId } + ); } } dispatch({ type: "SET_AUTH_SUCCESS", - payload: { - user, - session: null, - apiKey: apiKey - } + payload: { + user, + session: null, + apiKey: apiKey, + }, }); - + if (verification.isOffline) { loggingService.info("AuthContext", "Running in offline mode", {}); // Could add offline indicator to state if needed } else { - loggingService.info("AuthContext", "User verification successful", { userId: user?.id }); + loggingService.info("AuthContext", "User verification successful", { + userId: user?.id, + }); } } else { // User no longer valid - clear auth data @@ -89,8 +106,13 @@ export function AuthProvider({ children }: AuthProviderProps) { } } catch (error) { // Handle any unexpected errors - loggingService.error("AuthContext", "Auth initialization failed", { error }); - dispatch({ type: "SET_ERROR", payload: "Failed to verify authentication" }); + loggingService.error("AuthContext", "Auth initialization failed", { + error, + }); + dispatch({ + type: "SET_ERROR", + payload: "Failed to verify authentication", + }); } }; @@ -124,11 +146,11 @@ export function AuthProvider({ children }: AuthProviderProps) { try { const result = await authService.signInWithEmail(email, password); - console.log('[AuthContext] Sign in service result:', { + console.log("[AuthContext] Sign in service result:", { hasUser: !!result.user, hasSession: !!result.session, hasError: !!result.error, - userId: result.user?.id + userId: result.user?.id, }); if (result.error) { @@ -137,31 +159,33 @@ export function AuthProvider({ children }: AuthProviderProps) { // Manually update auth state since auth listener is disabled for API key auth if (result.user) { - console.log('[AuthContext] Getting API key for signed in user...'); + console.log("[AuthContext] Getting API key for signed in user..."); const apiKey = await authService.getApiKey(); - console.log('[AuthContext] Retrieved API key:', { - hasApiKey: !!apiKey, + console.log("[AuthContext] Retrieved API key:", { + hasApiKey: !!apiKey, apiKeyLength: apiKey?.length, - apiKeyPrefix: apiKey ? apiKey.substring(0, 8) + '...' : 'null' + apiKeyPrefix: apiKey ? apiKey.substring(0, 8) + "..." : "null", }); - - console.log('[AuthContext] Dispatching SET_AUTH_SUCCESS...'); + + console.log("[AuthContext] Dispatching SET_AUTH_SUCCESS..."); dispatch({ type: "SET_AUTH_SUCCESS", - payload: { - user: result.user, - session: result.session, - apiKey - } + payload: { + user: result.user, + session: result.session, + apiKey, + }, }); - console.log('[AuthContext] Auth state updated successfully'); + console.log("[AuthContext] Auth state updated successfully"); } else { - console.log('[AuthContext] No user in result, cannot update auth state'); + console.log( + "[AuthContext] No user in result, cannot update auth state" + ); } loggingService.info("AuthContext", "Sign in successful", undefined); } catch (error) { - console.error('[AuthContext] Sign in error:', error); + console.error("[AuthContext] Sign in error:", error); loggingService.error("AuthContext", "Sign in failed", { error }); dispatch({ type: "SET_ERROR", payload: "Failed to sign in" }); throw error; @@ -177,11 +201,11 @@ export function AuthProvider({ children }: AuthProviderProps) { try { const result = await authService.signUpWithEmail(email, password); - console.log('[AuthContext] Sign up service result:', { + console.log("[AuthContext] Sign up service result:", { hasUser: !!result.user, hasSession: !!result.session, hasError: !!result.error, - userId: result.user?.id + userId: result.user?.id, }); if (result.error) { @@ -190,31 +214,37 @@ export function AuthProvider({ children }: AuthProviderProps) { // Manually update auth state since auth listener is disabled for API key auth if (result.user) { - console.log('[AuthContext] Getting API key for signed up user...'); + console.log("[AuthContext] Getting API key for signed up user..."); const apiKey = await authService.getApiKey(); - console.log('[AuthContext] Retrieved API key:', { - hasApiKey: !!apiKey, + console.log("[AuthContext] Retrieved API key:", { + hasApiKey: !!apiKey, apiKeyLength: apiKey?.length, - apiKeyPrefix: apiKey ? apiKey.substring(0, 8) + '...' : 'null' + apiKeyPrefix: apiKey ? apiKey.substring(0, 8) + "..." : "null", }); - - console.log('[AuthContext] Dispatching SET_AUTH_SUCCESS for sign up...'); + + console.log( + "[AuthContext] Dispatching SET_AUTH_SUCCESS for sign up..." + ); dispatch({ type: "SET_AUTH_SUCCESS", - payload: { - user: result.user, - session: result.session, - apiKey - } + payload: { + user: result.user, + session: result.session, + apiKey, + }, }); - console.log('[AuthContext] Auth state updated successfully after sign up'); + console.log( + "[AuthContext] Auth state updated successfully after sign up" + ); } else { - console.log('[AuthContext] No user in sign up result, cannot update auth state'); + console.log( + "[AuthContext] No user in sign up result, cannot update auth state" + ); } loggingService.info("AuthContext", "Sign up successful", undefined); } catch (error) { - console.error('[AuthContext] Sign up error:', error); + console.error("[AuthContext] Sign up error:", error); loggingService.error("AuthContext", "Sign up failed", { error }); dispatch({ type: "SET_ERROR", payload: "Failed to sign up" }); throw error; @@ -230,7 +260,7 @@ export function AuthProvider({ children }: AuthProviderProps) { try { await authService.signOut(); // Manually clear auth state since auth listener is disabled for API key auth - console.log('[AuthContext] Clearing auth state after sign out'); + console.log("[AuthContext] Clearing auth state after sign out"); dispatch({ type: "CLEAR_AUTH" }); loggingService.info("AuthContext", "Sign out successful", undefined); } catch (error) { diff --git a/apps/mobile/src/context/ChartDisplayContext.tsx b/apps/mobile/src/context/ChartDisplayContext.tsx new file mode 100644 index 0000000..3116779 --- /dev/null +++ b/apps/mobile/src/context/ChartDisplayContext.tsx @@ -0,0 +1,458 @@ +import React, { + createContext, + useContext, + useState, + useEffect, + useCallback, + ReactNode, + useMemo, +} from "react"; +import { useTimeContext } from "./TimeContext"; +import { formatMonthDay } from "../utils/dateFormatters"; + +// ChartDisplayContext: Centralized time range management and display formatting +// Reactive to TimeContext updates, provides chart parameters to Home/NewEnergyChart/ChartHeader/TimeDisplayToggle + +export type TimeRange = + | "1day" + | "3day" + | "1week" + | "1month" + | "3month" + | "1year"; +export type ChartGranularity = "hour" | "day" | "week" | "month"; + +interface ChartDisplayContextValue { + // Core date management: Calculated date boundaries and time range state + dateStart: Date; // Start of selected time range (calculated from shortcuts) + dateEnd: Date; // End of selected time range (usually now or end of selected period) + timeRange: TimeRange; // Current selected time range shortcut ("1day" | "3day" | "1week" | "1month" | "3month" | "1year") + isToday: boolean; // True when viewing today only (1day range ending now) + isLive: boolean; // True when end date is current time (real-time data) + rangeInDays: number; // Total days in selected range (1, 3, 7, 30, 90, 365) + rangeInHours: number; // Total hours in selected range (for granularity decisions) + + // Display options: Chart rendering configuration based on range + showHourlyMarkers: boolean; // Show hourly time markers (true for 1day view) + showDayBoundaries: boolean; // Show day divider lines (true for multi-day views) + chartGranularity: ChartGranularity; // Data point granularity ('hour' | 'day' | 'week' | 'month') + dataPointDensity: number; // Expected data points per time unit + + // Formatted labels: Ready-to-display strings for chart components + headerTopLabel: string; // ChartHeader top line ("Today", "Last Week", "Mon - Wed") + headerBottomLabel: string; // ChartHeader bottom line ("Aug 31st", "Aug 29th - Aug 31st") + chartAxisLabels: string[]; // X-axis time labels array for chart ticks + periodDescription: string; // Human readable period ("Today", "This Week", etc.) + + // Actions: Time range control functions + setTimeRange: (range: TimeRange) => void; // Change time range shortcut + setCustomRange: (start: Date, end: Date) => void; // Set custom date range + refreshRange: () => void; // Recalculate current range (for live updates) + goToPreviousPeriod: () => void; // Navigate to previous time period + goToNextPeriod: () => void; // Navigate to next time period + + // Utilities: Date manipulation helpers for components + isDateInRange: (date: Date) => boolean; // Check if date falls within current range + formatDateForRange: (date: Date) => string; // Format date appropriately for current range + getTimeLabel: (date: Date) => string; // Get time label for chart axis + getDataPointsInRange: ( + data: T[], + getTimestamp: (item: T) => number + ) => T[]; // Filter data to range +} + +const ChartDisplayContext = createContext( + undefined +); + +interface ChartDisplayContextProviderProps { + children: ReactNode; + initialRange?: TimeRange; + autoRefresh?: boolean; // Auto-refresh live ranges +} + +export const ChartDisplayContextProvider: React.FC< + ChartDisplayContextProviderProps +> = ({ + children, + initialRange = "1day", // Default time range shortcut + autoRefresh = true, // Enable reactive updates from TimeContext +}) => { + const timeContext = useTimeContext(); // Access to timeInfo.localTime and formatting functions + const { timeInfo } = timeContext; + + const [timeRange, setTimeRangeState] = useState(initialRange); + const [customStart, setCustomStart] = useState(null); // Custom range start (overrides shortcuts) + const [customEnd, setCustomEnd] = useState(null); // Custom range end (overrides shortcuts) + const [isCustomRange, setIsCustomRange] = useState(false); // Flag: using custom vs predefined range + + // dateRange: Core date boundary calculation from TimeContext.localTime or shortcuts + const dateRange = useMemo(() => { + const now = timeInfo?.localTime || new Date(); + + if (isCustomRange && customStart && customEnd) { + return { start: customStart, end: customEnd }; + } + + const start = new Date(now); + + switch (timeRange) { + case "1day": + start.setHours(0, 0, 0, 0); // Start of today + return { start, end: new Date(now) }; + + case "3day": + start.setDate(now.getDate() - 2); + start.setHours(0, 0, 0, 0); + return { start, end: new Date(now) }; + + case "1week": + start.setDate(now.getDate() - 6); + start.setHours(0, 0, 0, 0); + return { start, end: new Date(now) }; + + case "1month": + start.setDate(now.getDate() - 29); + start.setHours(0, 0, 0, 0); + return { start, end: new Date(now) }; + + case "3month": + start.setDate(now.getDate() - 89); + start.setHours(0, 0, 0, 0); + return { start, end: new Date(now) }; + + case "1year": + start.setDate(now.getDate() - 364); + start.setHours(0, 0, 0, 0); + return { start, end: new Date(now) }; + + default: + return { start, end: new Date(now) }; + } + }, [timeRange, timeInfo?.localTime, isCustomRange, customStart, customEnd]); + + const dateStart = dateRange.start; // Boundary: Start of time range for chart display + const dateEnd = dateRange.end; // Boundary: End of time range for chart display + + // Range metrics: Duration calculations for display logic + const rangeInMs = dateEnd.getTime() - dateStart.getTime(); + const rangeInHours = Math.ceil(rangeInMs / (1000 * 60 * 60)); // For granularity decisions + const rangeInDays = Math.ceil(rangeInMs / (1000 * 60 * 60 * 24)); // For display options + + // Range flags: Special display modes and real-time detection + const isToday = useMemo(() => { + // True when viewing today only (1day range ending now) + if (timeRange !== "1day") return false; + const now = timeInfo?.localTime || new Date(); + const today = new Date(now); + today.setHours(0, 0, 0, 0); + return dateStart.getTime() === today.getTime(); + }, [timeRange, dateStart, timeInfo?.localTime]); + + const isLive = useMemo(() => { + // True when end date is current time (real-time data) + const now = timeInfo?.localTime || new Date(); + const timeDiff = Math.abs(dateEnd.getTime() - now.getTime()); + return timeDiff < 5 * 60 * 1000; // Within 5 minutes of now + }, [dateEnd, timeInfo?.localTime]); + + // displayOptions: Chart rendering configuration based on range length + const displayOptions = useMemo(() => { + const showHourlyMarkers = timeRange === "1day"; + const showDayBoundaries = rangeInDays > 1; + + let chartGranularity: ChartGranularity; + let dataPointDensity: number; + + if (rangeInHours <= 24) { + chartGranularity = "hour"; + dataPointDensity = 4; // Every 15 minutes + } else if (rangeInDays <= 7) { + chartGranularity = "day"; + dataPointDensity = 8; // 8 points per day + } else if (rangeInDays <= 90) { + chartGranularity = "day"; + dataPointDensity = 1; // 1 point per day + } else { + chartGranularity = "week"; + dataPointDensity = 1; // 1 point per week + } + + return { + showHourlyMarkers, + showDayBoundaries, + chartGranularity, + dataPointDensity, + }; + }, [timeRange, rangeInHours, rangeInDays]); + + // Label formatting + const headerLabels = useMemo(() => { + const startDate = new Date(dateStart); + const endDate = new Date(dateEnd); + + let topLabel: string; + let bottomLabel: string; + let periodDescription: string; + + switch (timeRange) { + case "1day": + if (isToday) { + topLabel = startDate.toLocaleDateString("en-US", { weekday: "long" }); + bottomLabel = + startDate.getFullYear() !== new Date().getFullYear() + ? `${formatMonthDay(startDate)}, ${startDate.getFullYear()}` + : formatMonthDay(startDate); + periodDescription = "Today"; + } else { + topLabel = startDate.toLocaleDateString("en-US", { weekday: "long" }); + bottomLabel = + startDate.getFullYear() !== new Date().getFullYear() + ? `${formatMonthDay(startDate)}, ${startDate.getFullYear()}` + : formatMonthDay(startDate); + periodDescription = "Single Day"; + } + break; + + case "3day": + const startDay = startDate.toLocaleDateString("en-US", { + weekday: "short", + }); + const endDay = endDate.toLocaleDateString("en-US", { + weekday: "short", + }); + topLabel = `${startDay} - ${endDay}`; + + bottomLabel = `${formatMonthDay(startDate)} - ${formatMonthDay( + endDate + )}`; + periodDescription = "Last 3 Days"; + break; + + case "1week": + topLabel = "Last Week"; + bottomLabel = `${formatMonthDay(startDate)} - ${formatMonthDay( + endDate + )}`; + periodDescription = "Last Week"; + break; + + case "1month": + topLabel = "Last Month"; + bottomLabel = `${formatMonthDay(startDate)} - ${formatMonthDay( + endDate + )}`; + periodDescription = "Last Month"; + break; + + case "3month": + topLabel = "Last Three Months"; + bottomLabel = `${formatMonthDay(startDate)} - ${formatMonthDay( + endDate + )}`; + periodDescription = "Last 3 Months"; + break; + + case "1year": + topLabel = "Last Year"; + bottomLabel = `${formatMonthDay(startDate)} - ${formatMonthDay( + endDate + )}`; + periodDescription = "Last Year"; + break; + + default: + topLabel = "Custom Range"; + bottomLabel = `${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`; + periodDescription = "Custom Period"; + } + + return { topLabel, bottomLabel, periodDescription }; + }, [dateStart, dateEnd, timeRange, isToday]); + + // Chart axis labels + const chartAxisLabels = useMemo(() => { + const labels: string[] = []; + const interval = rangeInMs / 6; // 6 labels across the range + + for (let i = 0; i <= 6; i++) { + const labelTime = new Date(dateStart.getTime() + interval * i); + + if (displayOptions.chartGranularity === "hour") { + labels.push( + timeContext.formatTime?.(labelTime) || + labelTime.toLocaleTimeString("en-US", { + hour: "numeric", + hour12: true, + }) + ); + } else { + labels.push(formatMonthDay(labelTime)); + } + } + + return labels; + }, [dateStart, rangeInMs, displayOptions.chartGranularity, timeContext]); + + // Actions + const setTimeRange = useCallback((range: TimeRange) => { + setTimeRangeState(range); + setIsCustomRange(false); + setCustomStart(null); + setCustomEnd(null); + }, []); + + const setCustomRange = useCallback((start: Date, end: Date) => { + setCustomStart(start); + setCustomEnd(end); + setIsCustomRange(true); + }, []); + + const refreshRange = useCallback(() => { + // Trigger recalculation by updating a dependency + setTimeRangeState((current) => current); + }, []); + + const goToPreviousPeriod = useCallback(() => { + if (isCustomRange) return; // Can't navigate custom ranges + + const newEnd = new Date(dateStart); + const periodLength = dateEnd.getTime() - dateStart.getTime(); + const newStart = new Date(newEnd.getTime() - periodLength); + + setCustomRange(newStart, newEnd); + }, [dateStart, dateEnd, isCustomRange, setCustomRange]); + + const goToNextPeriod = useCallback(() => { + if (isCustomRange) return; // Can't navigate custom ranges + + const now = timeInfo?.localTime || new Date(); + const periodLength = dateEnd.getTime() - dateStart.getTime(); + const newStart = new Date(dateEnd); + const newEnd = new Date(newStart.getTime() + periodLength); + + // Don't go into the future + if (newEnd.getTime() > now.getTime()) { + setTimeRange(timeRange); // Reset to live range + } else { + setCustomRange(newStart, newEnd); + } + }, [ + dateStart, + dateEnd, + isCustomRange, + timeInfo?.localTime, + timeRange, + setCustomRange, + setTimeRange, + ]); + + // Utilities + const isDateInRange = useCallback( + (date: Date) => { + const timestamp = date.getTime(); + return timestamp >= dateStart.getTime() && timestamp <= dateEnd.getTime(); + }, + [dateStart, dateEnd] + ); + + const formatDateForRange = useCallback( + (date: Date) => { + if (displayOptions.chartGranularity === "hour") { + return ( + timeContext.formatTime?.(date) || date.toLocaleTimeString("en-US") + ); + } + return timeContext.formatDate?.(date) || date.toLocaleDateString("en-US"); + }, + [displayOptions.chartGranularity, timeContext] + ); + + const getTimeLabel = useCallback( + (date: Date) => { + switch (displayOptions.chartGranularity) { + case "hour": + return date.toLocaleTimeString("en-US", { + hour: "numeric", + hour12: true, + }); + case "day": + return formatMonthDay(date); + case "week": + return `Week ${Math.ceil(date.getDate() / 7)}`; + default: + return date.toLocaleDateString("en-US"); + } + }, + [displayOptions.chartGranularity] + ); + + const getDataPointsInRange = useCallback( + (data: T[], getTimestamp: (item: T) => number): T[] => { + const startTime = dateStart.getTime(); + const endTime = dateEnd.getTime(); + + return data.filter((item) => { + const timestamp = getTimestamp(item); + return timestamp >= startTime && timestamp <= endTime; + }); + }, + [dateStart, dateEnd] + ); + + // Auto-refresh for live ranges - react to TimeContext updates instead of separate interval + useEffect(() => { + if (autoRefresh && isLive) { + refreshRange(); // React to TimeContext timestamp changes + } + }, [timeInfo?.timestamp, autoRefresh, isLive, refreshRange]); + + const value: ChartDisplayContextValue = { + // Core date management + dateStart, + dateEnd, + timeRange, + isToday, + isLive, + rangeInDays, + rangeInHours, + + // Display options + ...displayOptions, + + // Formatted labels + headerTopLabel: headerLabels.topLabel, + headerBottomLabel: headerLabels.bottomLabel, + chartAxisLabels, + periodDescription: headerLabels.periodDescription, + + // Actions + setTimeRange, + setCustomRange, + refreshRange, + goToPreviousPeriod, + goToNextPeriod, + + // Utilities + isDateInRange, + formatDateForRange, + getTimeLabel, + getDataPointsInRange, + }; + + return ( + + {children} + + ); +}; + +export const useChartDisplayContext = (): ChartDisplayContextValue => { + const context = useContext(ChartDisplayContext); + if (!context) { + throw new Error( + "useChartDisplayContext must be used within a ChartDisplayContextProvider" + ); + } + return context; +}; diff --git a/apps/mobile/src/context/ChatContext.tsx b/apps/mobile/src/context/ChatContext.tsx index 045514d..a4b00b8 100644 --- a/apps/mobile/src/context/ChatContext.tsx +++ b/apps/mobile/src/context/ChatContext.tsx @@ -2,1094 +2,185 @@ import React, { createContext, useContext, useReducer, - useMemo, - useCallback, - useEffect, + useRef, ReactNode, } from "react"; -import { agentService } from "../services/agentService"; -import { useAuth } from "./AuthContext"; -import { useMCP } from "./MCPContext"; -import { extractUserIdFromApiKey } from "../utils/apiKeyUtils"; -import type { - ChatState, - ChatAction, - ChatMessage, - MCPToolCall, - AvailableMCPTool, -} from "../types/chat"; -import { loggingService } from "../services/loggingService"; +import { Animated, Easing } from "react-native"; +import type { DetectedToolSuggestion } from "../utils/toolDetection"; +import { + detectToolSuggestions, + isExactToolTitle, +} from "../utils/toolDetection"; + +interface ChatState { + isLoading: boolean; + error: string | null; + data: any | null; + inputMessage: string; + isInputFocused: boolean; + toolSuggestions: DetectedToolSuggestion[]; + highlightedTool: string | null; + toolbar: "suggestions" | "instructions" | "list" | null; + toolMenuOpen: boolean; +} + +type ChatAction = + | { type: "SET_LOADING"; payload: boolean } + | { type: "SET_ERROR"; payload: string | null } + | { type: "SET_DATA"; payload: any } + | { type: "RESET_STATE" } + | { type: "SET_INPUT_MESSAGE"; payload: string } + | { type: "SET_INPUT_FOCUSED"; payload: boolean } + | { type: "SET_TOOL_SUGGESTIONS"; payload: DetectedToolSuggestion[] } + | { type: "SET_HIGHLIGHTED_TOOL"; payload: string | null } + | { + type: "SET_TOOLBAR"; + payload: "suggestions" | "instructions" | "list" | null; + } + | { type: "TOGGLE_TOOL_MENU" } + | { type: "SET_TOOL_MENU_OPEN"; payload: boolean }; -const initialChatState: ChatState = { - messages: [], +const initialState: ChatState = { isLoading: false, error: null, - conversationContext: { - userId: "", - sessionId: "", - activeConversationId: "", - currentTideId: undefined, - mcpConnectionStatus: false, - agentConnectionStatus: false, - }, - pendingToolCalls: [], - agentStatus: "idle", - connectionStatus: { - mcp: false, - agent: false, - }, + data: null, + inputMessage: "", + isInputFocused: false, + toolSuggestions: [], + highlightedTool: null, + toolbar: null, + toolMenuOpen: false, }; function chatReducer(state: ChatState, action: ChatAction): ChatState { switch (action.type) { - case "ADD_MESSAGE": - return { - ...state, - messages: [...state.messages, action.payload], - isLoading: false, - }; - case "SET_LOADING": - return { - ...state, - isLoading: action.payload, - }; - + return { ...state, isLoading: action.payload }; case "SET_ERROR": - return { - ...state, - error: action.payload, - isLoading: false, - }; - - case "SET_AGENT_STATUS": - return { - ...state, - agentStatus: action.payload, - }; - - case "ADD_TOOL_CALL": - return { - ...state, - pendingToolCalls: [...state.pendingToolCalls, action.payload], - }; - - case "UPDATE_TOOL_CALL": - return { - ...state, - pendingToolCalls: state.pendingToolCalls.map((call) => - call.id === action.payload.id - ? { ...call, ...action.payload.updates } - : call - ), - }; - - case "SET_CONNECTION_STATUS": - return { - ...state, - connectionStatus: action.payload, - }; - - case "CLEAR_MESSAGES": - return { - ...state, - messages: [], - error: null, - }; - - case "SET_CONVERSATION_CONTEXT": - return { - ...state, - conversationContext: { - ...state.conversationContext, - ...action.payload, - }, - }; - - case "RESET_CHAT": - return { - ...initialChatState, - conversationContext: { - ...initialChatState.conversationContext, - userId: state.conversationContext.userId, - }, - }; - + return { ...state, error: action.payload }; + case "SET_DATA": + return { ...state, data: action.payload, error: null }; + case "RESET_STATE": + return initialState; + case "SET_INPUT_MESSAGE": + return { ...state, inputMessage: action.payload }; + case "SET_INPUT_FOCUSED": + return { ...state, isInputFocused: action.payload }; + case "SET_TOOL_SUGGESTIONS": + return { ...state, toolSuggestions: action.payload }; + case "SET_HIGHLIGHTED_TOOL": + return { ...state, highlightedTool: action.payload }; + case "SET_TOOLBAR": + return { ...state, toolbar: action.payload }; + case "TOGGLE_TOOL_MENU": + return { ...state, toolMenuOpen: !state.toolMenuOpen }; + case "SET_TOOL_MENU_OPEN": + return { ...state, toolMenuOpen: action.payload }; default: return state; } } interface ChatContextType extends ChatState { - // Message handling - sendMessage: (content: string) => Promise; - sendToolMessage: (toolName: string, parameters: any) => Promise; - addSystemMessage: (content: string) => void; - clearMessages: () => void; - - // Tool execution - executeMCPTool: (toolName: string, parameters: any) => Promise; - getAvailableTools: () => AvailableMCPTool[]; - - // Agent interaction - sendAgentMessage: ( - message: string, - context?: { tideId?: string } - ) => Promise; - - // Connection management - checkConnections: () => Promise; + setData: (data: any) => void; + setError: (error: string | null) => void; + resetState: () => void; + setInputMessage: (message: string) => void; + setInputFocused: (focused: boolean) => void; + handleInputChange: (text: string) => void; + setToolSuggestions: (suggestions: DetectedToolSuggestion[]) => void; + setHighlightedTool: (tool: string | null) => void; + setToolbar: (toolbar: "suggestions" | "instructions" | "list" | null) => void; + toggleToolMenu: () => void; + setToolMenuOpen: (open: boolean) => void; + rotationAnim: Animated.Value; } const ChatContext = createContext(undefined); -interface ChatProviderProps { - children: ReactNode; -} - -export function ChatProvider({ children }: ChatProviderProps) { - const { apiKey } = useAuth(); - const { - isConnected: mcpConnected, - createTide, - startTideFlow, - addEnergyToTide, - getTideReport, - linkTaskToTide, - getTaskLinks, - getTideParticipants, - refreshTides, - tides, - getCurrentServerUrl, - } = useMCP(); - const [state, dispatch] = useReducer(chatReducer, initialChatState); - - // Generate unique IDs for messages and tool calls - const generateId = useCallback(() => { - return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; - }, []); - - // Initialize conversation context when API key changes - useEffect(() => { - if (apiKey) { - const userId = extractUserIdFromApiKey(apiKey); - if (userId) { - const sessionId = generateId(); - const conversationId = generateId(); - - dispatch({ - type: "SET_CONVERSATION_CONTEXT", - payload: { - userId, - sessionId, - activeConversationId: conversationId, - mcpConnectionStatus: mcpConnected, - }, - }); - - loggingService.info("ChatContext", "Conversation context initialized", { - userId, - sessionId, - conversationId, - }); - } else { - loggingService.warn("ChatContext", "Could not extract user ID from API key", { - apiKeyPrefix: apiKey.substring(0, 15) + '...' - }); - } - } - }, [apiKey, generateId, mcpConnected]); - - // Configure agentService with current server URL and MCP tool executor - useEffect(() => { - if (getCurrentServerUrl) { - agentService.setUrlProvider(getCurrentServerUrl); - loggingService.info("ChatContext", "AgentService configured with MCP URL provider"); - } - }, [getCurrentServerUrl]); - - - // Update connection statuses - useEffect(() => { - dispatch({ - type: "SET_CONNECTION_STATUS", - payload: { - mcp: mcpConnected, - agent: false, // Will be updated when AgentService is implemented - }, - }); - - dispatch({ - type: "SET_CONVERSATION_CONTEXT", - payload: { - mcpConnectionStatus: mcpConnected, - }, - }); - }, [mcpConnected]); - - const executeMCPTool = useCallback( - async (toolName: string, parameters: any): Promise => { - const toolCallId = generateId(); - const toolCall: MCPToolCall = { - id: toolCallId, - name: toolName, - parameters, - timestamp: new Date(), - status: "pending", - }; - - dispatch({ type: "ADD_TOOL_CALL", payload: toolCall }); - dispatch({ type: "SET_LOADING", payload: true }); - - loggingService.info("ChatContext", "Executing MCP tool", { - toolName, - toolCallId, - parameters, - }); - - try { - dispatch({ - type: "UPDATE_TOOL_CALL", - payload: { id: toolCallId, updates: { status: "executing" } }, - }); - - let result: any; - - // Route to appropriate MCP tool based on name - switch (toolName) { - case "tide_create": - case "createTide": - result = await createTide( - parameters.name, - parameters.description, - parameters.flowType - ); - break; - case "tide_flow": - case "startTideFlow": - result = await startTideFlow( - parameters.tideId, - parameters.intensity, - parameters.duration, - parameters.initialEnergy, - parameters.workContext - ); - break; - case "tide_smart_flow": - // Import mcpService for smart flow - const { mcpService } = await import('../services/mcpService'); - result = await mcpService.startSmartFlow( - parameters.intensity, - parameters.duration, - parameters.workContext - ); - break; - case "tide_add_energy": - case "addEnergyToTide": - result = await addEnergyToTide( - parameters.tideId, - parameters.energyLevel, - parameters.context - ); - break; - case "tide_get_report": - case "getTideReport": - result = await getTideReport(parameters.tideId, parameters.format); - break; - case "tide_link_task": - case "linkTaskToTide": - result = await linkTaskToTide( - parameters.tideId, - parameters.taskUrl, - parameters.taskTitle, - parameters.taskType - ); - break; - case "tide_list_task_links": - case "getTaskLinks": - result = await getTaskLinks(parameters.tideId); - break; - case "tides_get_participants": - case "getTideParticipants": - result = await getTideParticipants( - parameters.statusFilter, - parameters.dateFrom, - parameters.dateTo, - parameters.limit - ); - break; - case "tide_list": - // Refresh tides and return the current list - await refreshTides(); - result = { - tides: tides, - message: `Found ${tides.length} tides`, - count: tides.length - }; - break; - default: - throw new Error(`Unknown tool: ${toolName}`); - } - - dispatch({ - type: "UPDATE_TOOL_CALL", - payload: { - id: toolCallId, - updates: { - status: "completed", - result, - }, - }, - }); - - // Add tool result message - const resultMessage: ChatMessage = { - id: generateId(), - type: "tool_result", - content: `Tool "${toolName}" executed successfully`, - timestamp: new Date(), - metadata: { - toolName, - toolResult: result, - conversationId: state.conversationContext.activeConversationId, - }, - }; - - dispatch({ type: "ADD_MESSAGE", payload: resultMessage }); - dispatch({ type: "SET_LOADING", payload: false }); - - loggingService.info("ChatContext", "MCP tool executed successfully", { - toolName, - toolCallId, - result, - }); - } catch (error) { - dispatch({ - type: "UPDATE_TOOL_CALL", - payload: { - id: toolCallId, - updates: { - status: "failed", - error: error instanceof Error ? error.message : "Unknown error", - }, - }, - }); - - const errorMessage: ChatMessage = { - id: generateId(), - type: "system", - content: `Tool execution failed: ${ - error instanceof Error ? error.message : "Unknown error" - }`, - timestamp: new Date(), - metadata: { - toolName, - error: true, - conversationId: state.conversationContext.activeConversationId, - }, - }; - - dispatch({ type: "ADD_MESSAGE", payload: errorMessage }); - dispatch({ - type: "SET_ERROR", - payload: `Failed to execute tool: ${toolName}`, - }); - dispatch({ type: "SET_LOADING", payload: false }); - - loggingService.error("ChatContext", "MCP tool execution failed", { - error, - toolName, - toolCallId, - }); - } - }, - [ - state.conversationContext, - generateId, - createTide, - startTideFlow, - addEnergyToTide, - getTideReport, - linkTaskToTide, - getTaskLinks, - getTideParticipants, - refreshTides, - tides, - ] - ); - - // Configure agentService with MCP tool execution capability - useEffect(() => { - // Create a tool executor that uses our existing executeMCPTool function - const mcpToolExecutor = async (toolName: string, parameters: any) => { - // Execute the tool using existing MCP infrastructure - return await executeMCPTool(toolName, parameters); - }; - - agentService.setMCPToolExecutor(mcpToolExecutor); - loggingService.info("ChatContext", "AgentService configured with MCP tool executor"); - }, [executeMCPTool]); - - // Handle slash commands for direct tool execution - const handleSlashCommand = useCallback( - async (command: string): Promise => { - const parts = command.substring(1).split(' '); // Remove '/' and split - const toolName = parts[0]; - const args = parts.slice(1); - - loggingService.info("ChatContext", "Processing slash command", { - toolName, - argsCount: args.length, - }); - - // Map slash commands to tool names - let mappedTool: string | undefined; - - // Handle different command patterns - if (toolName === 'tide') { - switch (args[0]) { - case 'create': - mappedTool = 'tide_create'; - break; - case 'list': - mappedTool = 'tide_list'; - break; - case 'report': - mappedTool = 'tide_get_report'; - break; - case 'flow': - mappedTool = 'tide_smart_flow'; - break; - default: - // If no valid subcommand, show error - mappedTool = undefined; - } - } else if (toolName === 'task') { - switch (args[0]) { - case 'link': - mappedTool = 'tide_link_task'; - break; - case 'list': - mappedTool = 'tide_list_task_links'; - break; - default: - mappedTool = undefined; - } - } else if (toolName === 'energy') { - mappedTool = 'tide_add_energy'; - } else if (toolName === 'participants') { - mappedTool = 'tides_get_participants'; - } else if (toolName === 'help') { - mappedTool = 'help'; - } - - if (mappedTool === 'help') { - const helpMessage: ChatMessage = { - id: generateId(), - type: "system", - content: `Available commands: -• /tide list - Show all your tides -• /tide create [name] - Create a new tide -• /tide report [id] - Get tide report -• /tide flow [id] - Start flow session -• /energy [level] - Add energy (low/medium/high) to most recent tide -• /energy [level] [tideId] - Add energy to specific tide -• /task link [tideId] [url] [title] - Link task to tide -• /task list [tideId] - List linked tasks -• /participants - Get tide participants -• Just type naturally - I can understand regular conversation too!`, - timestamp: new Date(), - metadata: { - conversationId: state.conversationContext.activeConversationId, - helpCommand: true, - }, - }; - dispatch({ type: "ADD_MESSAGE", payload: helpMessage }); - dispatch({ type: "SET_LOADING", payload: false }); - return; - } - - if (mappedTool) { - // Build parameters based on the command - let parameters: any = {}; - - if (mappedTool === 'tide_create') { - parameters = { - name: args.slice(1).join(' ') || 'New Tide', - description: `Created via chat command`, - flowType: 'daily' - }; - } else if (mappedTool === 'tide_get_report' && args[1]) { - parameters = { - tideId: args[1], - format: 'json' - }; - } else if (mappedTool === 'tide_add_energy') { - // Check if user provided a tide ID as second argument - let tideId = args[1]; - let energyLevel = args[0]; - - // ADR-004: Use context-based tide operations - no dependency on user-created tides - if (!tideId) { - if (state.conversationContext.currentTideId) { - tideId = state.conversationContext.currentTideId; - } else { - // Use current context tide (daily/weekly/monthly) - always available - // This will be resolved by the MCP service to the current context - tideId = 'current-context'; - loggingService.info("ChatContext", "Using current context tide for energy update (ADR-004 compliant)", { - contextBasedApproach: true, - fallbackTide: tideId - }); - } - } - - parameters = { - tideId: tideId, - energyLevel: energyLevel || 'medium', - context: 'Chat command - context-based tide system', - // ADR-004: Add context metadata - useContextTide: tideId === 'current-context', - timestamp: new Date().toISOString(), - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone - }; - } else if (mappedTool === 'tide_flow' && args[1]) { - parameters = { - tideId: args[1], - intensity: 'moderate', - duration: 25 - }; - } else if (mappedTool === 'tide_link_task' && args[1]) { - parameters = { - tideId: args[1], - taskUrl: args[2] || 'https://example.com/task', - taskTitle: args.slice(3).join(' ') || 'Task', - taskType: 'general' - }; - } else if (mappedTool === 'tide_list_task_links' && args[1]) { - parameters = { - tideId: args[1] - }; - } else if (mappedTool === 'tides_get_participants') { - parameters = { - limit: 10 - }; - } - - await executeMCPTool(mappedTool, parameters); - } else { - // Provide more specific error messages for known commands with invalid subcommands - let errorContent = `Unknown command: /${toolName}`; - - if (toolName === 'tide' && args[0]) { - errorContent = `Invalid tide subcommand: '${args[0]}'. Valid options are: create, list, report, flow`; - } else if (toolName === 'task' && args[0]) { - errorContent = `Invalid task subcommand: '${args[0]}'. Valid options are: link, list`; - } else if (!mappedTool) { - errorContent = `Unknown command: /${command.substring(1).split(' ')[0]}. Type '/help' to see available commands.`; - } - - const errorMessage: ChatMessage = { - id: generateId(), - type: "system", - content: errorContent, - timestamp: new Date(), - metadata: { - conversationId: state.conversationContext.activeConversationId, - error: true, - }, - }; - dispatch({ type: "ADD_MESSAGE", payload: errorMessage }); - dispatch({ type: "SET_LOADING", payload: false }); - } - }, - [state.conversationContext, generateId, executeMCPTool] - ); - - const sendMessage = useCallback( - async (content: string): Promise => { - if (!content.trim()) return; - - const messageId = generateId(); - const userMessage: ChatMessage = { - id: messageId, - type: "user", - content: content.trim(), - timestamp: new Date(), - metadata: { - conversationId: state.conversationContext.activeConversationId, - userId: state.conversationContext.userId, - }, - }; - - dispatch({ type: "ADD_MESSAGE", payload: userMessage }); - dispatch({ type: "SET_LOADING", payload: true }); - - loggingService.info("ChatContext", "Processing message with AI enhancement", { - messageId, - content: content.substring(0, 50) + "...", - }); - - try { - // Check if message starts with slash command - if (content.startsWith('/')) { - loggingService.info("ChatContext", "Detected slash command, routing to handleSlashCommand", { - command: content - }); - try { - await handleSlashCommand(content); - return; - } catch (slashError) { - loggingService.error("ChatContext", "Slash command failed, falling back to AI", { - error: slashError, - command: content - }); - // Don't return - let it fall through to AI processing - } - } - - // Use enhanced agent service for natural language processing - try { - const agentResponse = await agentService.sendMessage(content, { - tideId: state.conversationContext.currentTideId, - }); - - const assistantMessage: ChatMessage = { - id: generateId(), - type: "assistant", - content: agentResponse.content, - timestamp: new Date(), - metadata: { - conversationId: state.conversationContext.activeConversationId, - agentResponse: true, - agentId: agentResponse.agentId, - responseType: agentResponse.type, - suggestedTools: agentResponse.suggestedTools, - }, - }; - - dispatch({ type: "ADD_MESSAGE", payload: assistantMessage }); - - // If the agent suggested a tool call, show suggestions - if (agentResponse.toolCall) { - const toolSuggestionMessage: ChatMessage = { - id: generateId(), - type: "system", - content: `I can execute "${agentResponse.toolCall.name}" for you. Would you like me to proceed?`, - timestamp: new Date(), - metadata: { - conversationId: state.conversationContext.activeConversationId, - toolSuggestion: agentResponse.toolCall, - }, - }; - dispatch({ type: "ADD_MESSAGE", payload: toolSuggestionMessage }); - } - - } catch (agentError) { - loggingService.warn("ChatContext", "Agent service unavailable, using fallback", agentError); - - // Fallback to basic response - const fallbackMessage: ChatMessage = { - id: generateId(), - type: "assistant", - content: `I understand your message about "${content}". I'm having trouble accessing my AI analysis tools right now. You can use direct commands like '/tide list' or '/tide create' to manage your flows.`, - timestamp: new Date(), - metadata: { - conversationId: state.conversationContext.activeConversationId, - fallbackResponse: true, - }, - }; - dispatch({ type: "ADD_MESSAGE", payload: fallbackMessage }); - } - - dispatch({ type: "SET_LOADING", payload: false }); - - } catch (error) { - loggingService.error("ChatContext", "Failed to process message", { - error, - messageId, - }); - - const errorMessage: ChatMessage = { - id: generateId(), - type: "system", - content: "I'm having trouble processing your message right now. Please try again or use direct commands like '/tide list'.", - timestamp: new Date(), - metadata: { - conversationId: state.conversationContext.activeConversationId, - error: true, - }, - }; - - dispatch({ type: "ADD_MESSAGE", payload: errorMessage }); - dispatch({ type: "SET_ERROR", payload: "Failed to process message" }); - dispatch({ type: "SET_LOADING", payload: false }); - } - }, - [state.conversationContext, generateId, handleSlashCommand] - ); - - const sendToolMessage = useCallback( - async (toolName: string, parameters: any): Promise => { - // Add user message for tool execution request - const userMessage: ChatMessage = { - id: generateId(), - type: "user", - content: `Execute tool: ${toolName}`, - timestamp: new Date(), - metadata: { - toolName, - conversationId: state.conversationContext.activeConversationId, - }, - }; - - dispatch({ type: "ADD_MESSAGE", payload: userMessage }); - - // Execute the tool - await executeMCPTool(toolName, parameters); - }, - [state.conversationContext, generateId, executeMCPTool] - ); - - const addSystemMessage = useCallback( - (content: string): void => { - const systemMessage: ChatMessage = { - id: generateId(), - type: "system", - content, - timestamp: new Date(), - metadata: { - conversationId: state.conversationContext.activeConversationId, - }, - }; - - dispatch({ type: "ADD_MESSAGE", payload: systemMessage }); - - loggingService.info("ChatContext", "System message added", { content }); - }, - [state.conversationContext, generateId] - ); - - const clearMessages = useCallback((): void => { - dispatch({ type: "CLEAR_MESSAGES" }); - - loggingService.info("ChatContext", "Messages cleared", {}); - }, []); - - const getAvailableTools = useCallback((): AvailableMCPTool[] => { - return [ - { - name: "createTide", - description: "Create a new tide workflow", - parameters: [ - { - name: "name", - type: "string", - required: true, - description: "Name of the tide", - }, - { - name: "description", - type: "string", - required: false, - description: "Description of the tide", - }, - { - name: "flowType", - type: "string", - required: false, - description: "Type of flow: daily, weekly, project, seasonal", - }, - ], - }, - { - name: "startTideFlow", - description: "Start a flow session for a tide", - parameters: [ - { - name: "tideId", - type: "string", - required: true, - description: "ID of the tide", - }, - { - name: "intensity", - type: "string", - required: false, - description: "Flow intensity: low, moderate, high", - }, - { - name: "duration", - type: "number", - required: false, - description: "Duration in minutes", - }, - { - name: "initialEnergy", - type: "string", - required: false, - description: "Initial energy level: low, medium, high", - }, - { - name: "workContext", - type: "string", - required: false, - description: "Context for the work session", - }, - ], - }, - { - name: "addEnergyToTide", - description: "Add energy measurement to a tide", - parameters: [ - { - name: "tideId", - type: "string", - required: true, - description: "ID of the tide", - }, - { - name: "energyLevel", - type: "string", - required: true, - description: "Energy level: low, medium, high", - }, - { - name: "context", - type: "string", - required: false, - description: "Context for the energy update", - }, - ], - }, - { - name: "getTideReport", - description: "Get a report for a tide", - parameters: [ - { - name: "tideId", - type: "string", - required: true, - description: "ID of the tide", - }, - { - name: "format", - type: "string", - required: false, - description: "Report format: json, markdown, csv", - }, - ], - }, - { - name: "linkTaskToTide", - description: "Link an external task to a tide", - parameters: [ - { - name: "tideId", - type: "string", - required: true, - description: "ID of the tide", - }, - { - name: "taskUrl", - type: "string", - required: true, - description: "URL of the task", - }, - { - name: "taskTitle", - type: "string", - required: true, - description: "Title of the task", - }, - { - name: "taskType", - type: "string", - required: false, - description: "Type of task", - }, - ], - }, - { - name: "getTaskLinks", - description: "Get all task links for a tide", - parameters: [ - { - name: "tideId", - type: "string", - required: true, - description: "ID of the tide", - }, - ], - }, - { - name: "getTideParticipants", - description: "Get tide participants information", - parameters: [ - { - name: "statusFilter", - type: "string", - required: false, - description: "Filter by status", - }, - { - name: "dateFrom", - type: "string", - required: false, - description: "Start date filter", - }, - { - name: "dateTo", - type: "string", - required: false, - description: "End date filter", - }, - { - name: "limit", - type: "number", - required: false, - description: "Limit number of results", - }, - ], - }, - ]; - }, []); - - const sendAgentMessage = useCallback( - async (message: string, context?: { tideId?: string }): Promise => { - if (!message.trim()) return; - - // Add user message to chat - const userMessage: ChatMessage = { - id: generateId(), - type: "user", - content: `${message.trim()}`, - timestamp: new Date(), - metadata: { - conversationId: state.conversationContext.activeConversationId, - userId: state.conversationContext.userId, - isAgentMessage: true, - }, - }; - - dispatch({ type: "ADD_MESSAGE", payload: userMessage }); - dispatch({ type: "SET_AGENT_STATUS", payload: "thinking" }); - dispatch({ type: "SET_LOADING", payload: true }); - - loggingService.info("ChatContext", "Sending message to agent", { - message: message.substring(0, 50) + "...", - tideId: context?.tideId, - }); - - try { - // Send message to agent service with tide context - const agentResponse = await agentService.sendMessage(message, context); - - // Add successful agent response - const assistantMessage: ChatMessage = { - id: generateId(), - type: "assistant", - content: agentResponse.content, - timestamp: new Date(), - metadata: { - conversationId: state.conversationContext.activeConversationId, - agentResponse: true, - agentId: agentResponse.agentId, - responseType: agentResponse.type, - }, - }; - - dispatch({ type: "ADD_MESSAGE", payload: assistantMessage }); - dispatch({ type: "SET_AGENT_STATUS", payload: "idle" }); - } catch (error) { - loggingService.error("ChatContext", "Failed to send message to agent", { - error, - message: message.substring(0, 50), - }); - - const errorMessage: ChatMessage = { - id: generateId(), - type: "system", - content: `Failed to communicate with agent: ${ - error instanceof Error ? error.message : "Unknown error" - }`, - timestamp: new Date(), - metadata: { - conversationId: state.conversationContext.activeConversationId, - error: true, - }, - }; - - dispatch({ type: "ADD_MESSAGE", payload: errorMessage }); - dispatch({ type: "SET_AGENT_STATUS", payload: "idle" }); - dispatch({ - type: "SET_ERROR", - payload: "Failed to communicate with agent", - }); - } finally { - dispatch({ type: "SET_LOADING", payload: false }); - } - }, - [state.conversationContext, generateId] - ); - - const checkConnections = useCallback(async (): Promise => { - loggingService.info("ChatContext", "Checking connections", {}); - - try { - // MCP connection is already handled by MCPContext - // Agent connection will be implemented with AgentService - - dispatch({ - type: "SET_CONNECTION_STATUS", - payload: { - mcp: mcpConnected, - agent: false, // Placeholder - }, - }); - } catch (error) { - loggingService.error("ChatContext", "Failed to check connections", { - error, - }); - dispatch({ type: "SET_ERROR", payload: "Failed to check connections" }); +export function ChatProvider({ children }: { children: ReactNode }) { + const [state, dispatch] = useReducer(chatReducer, initialState); + const rotationAnim = useRef(new Animated.Value(0)).current; + + const setData = (data: any) => dispatch({ type: "SET_DATA", payload: data }); + const setError = (error: string | null) => + dispatch({ type: "SET_ERROR", payload: error }); + const resetState = () => dispatch({ type: "RESET_STATE" }); + const setInputMessage = (message: string) => + dispatch({ type: "SET_INPUT_MESSAGE", payload: message }); + const setInputFocused = (focused: boolean) => + dispatch({ type: "SET_INPUT_FOCUSED", payload: focused }); + const setToolSuggestions = (suggestions: DetectedToolSuggestion[]) => + dispatch({ type: "SET_TOOL_SUGGESTIONS", payload: suggestions }); + const setHighlightedTool = (tool: string | null) => + dispatch({ type: "SET_HIGHLIGHTED_TOOL", payload: tool }); + const setToolbar = ( + toolbar: "suggestions" | "instructions" | "list" | null + ) => dispatch({ type: "SET_TOOLBAR", payload: toolbar }); + + const animateRotation = (open: boolean) => { + Animated.timing(rotationAnim, { + toValue: open ? 1 : 0, + duration: 250, + easing: Easing.out(Easing.ease), + useNativeDriver: true, + }).start(); + }; + + const toggleToolMenu = () => { + const newValue = !state.toolMenuOpen; + dispatch({ type: "SET_TOOL_MENU_OPEN", payload: newValue }); + dispatch({ type: "SET_TOOLBAR", payload: newValue ? "list" : null }); + animateRotation(newValue); + }; + + const setToolMenuOpen = (open: boolean) => { + dispatch({ type: "SET_TOOL_MENU_OPEN", payload: open }); + dispatch({ type: "SET_TOOLBAR", payload: open ? "list" : null }); + animateRotation(open); + }; + + const handleInputChange = (text: string) => { + setInputMessage(text); + const exactToolTitle = isExactToolTitle(text); + if (exactToolTitle) { + setHighlightedTool(exactToolTitle); + setToolSuggestions([]); + setToolbar("instructions"); + if (state.toolMenuOpen) setToolMenuOpen(false); + } else { + setHighlightedTool(null); + const suggestions = detectToolSuggestions(text); + setToolSuggestions(suggestions); + setToolbar(suggestions.length > 0 ? "suggestions" : null); } - }, [mcpConnected]); - - // Memoize context value to prevent unnecessary re-renders - const contextValue = useMemo( - () => ({ - ...state, - sendMessage, - sendToolMessage, - addSystemMessage, - clearMessages, - executeMCPTool, - getAvailableTools, - sendAgentMessage, - checkConnections, - }), - [ - state, - sendMessage, - sendToolMessage, - addSystemMessage, - clearMessages, - executeMCPTool, - getAvailableTools, - sendAgentMessage, - checkConnections, - ] - ); + }; + + const contextValue: ChatContextType = { + ...state, + setData, + setError, + resetState, + setInputMessage, + setInputFocused, + handleInputChange, + setToolSuggestions, + setHighlightedTool, + setToolbar, + toggleToolMenu, + setToolMenuOpen, + rotationAnim, + }; return ( {children} ); } -export function useChat(): ChatContextType { +export function useChat() { const context = useContext(ChatContext); if (context === undefined) { - throw new Error("useChat must be used within a ChatProvider"); + throw new Error("useChat must be used within an ChatProvider"); } return context; } diff --git a/apps/mobile/src/context/MCPContext.tsx b/apps/mobile/src/context/MCPContext.tsx index 29aa918..3902031 100644 --- a/apps/mobile/src/context/MCPContext.tsx +++ b/apps/mobile/src/context/MCPContext.tsx @@ -13,70 +13,32 @@ import { mcpService } from "../services/mcpService"; import { authService } from "../services/authService"; import { loggingService } from "../services/loggingService"; import { useAuth } from "./AuthContext"; -import { useServerEnvironment } from "./ServerEnvironmentContext"; import { mcpReducer, initialMCPState, type MCPState } from "./mcpTypes"; -import type { - Tide, - EnergyUpdate, - TaskLinksResponse, - EnergyLevel, - FlowIntensity, - TideCreateResponse, +import { FlowSessionResponse, - TideReportResponse, TaskLinkResponse, -} from "../types"; - -// MCPState is now imported from mcpTypes.ts + TideCreateResponse, + TideReportResponse, + EnergyUpdateResponse, + TaskLinksListResponse, +} from "../types/api"; +import { EnergyLevel, FlowIntensity, Tide } from "../types/models"; interface MCPContextType extends MCPState { // Connection management checkConnection: () => Promise; - updateServerUrl: (url: string) => Promise; getCurrentServerUrl: () => string; // Tide management createTide: ( name: string, - description?: string, - flowType?: "daily" | "weekly" | "project" | "seasonal" + description?: string ) => Promise; refreshTides: () => Promise; selectTide: (tide: Tide | null) => void; - - // Hierarchical context management - getOrCreateDailyTide: ( + getOrCreateTide: ( timezone?: string ) => Promise; - switchTideContext: ( - contextType: "daily" | "weekly" | "monthly" | "project", - date?: string - ) => Promise<{ - success: boolean; - tide?: Tide; - context?: string; - created?: boolean; - error?: string; - }>; - listTideContexts: (date?: string) => Promise<{ - success: boolean; - contexts?: Array<{ - context: string; - tide_id?: string; - tide_name?: string; - flow_count: number; - total_minutes: number; - available: boolean; - }>; - error?: string; - }>; - getTodaysSummary: (date?: string) => Promise<{ - success: boolean; - contexts?: Array; - total_flow_sessions?: number; - total_minutes?: number; - error?: string; - }>; // Flow session management startTideFlow: ( @@ -86,31 +48,13 @@ interface MCPContextType extends MCPState { initialEnergy?: "low" | "medium" | "high", workContext?: string ) => Promise; - startHierarchicalFlow: ( - intensity?: "gentle" | "moderate" | "strong", - duration?: number, - initialEnergy?: "low" | "medium" | "high", - workContext?: string, - date?: string - ) => Promise<{ - success: boolean; - session_id?: string; - contexts?: Array<{ - context: string; - tide_id: string; - tide_name: string; - session_id: string; - created: boolean; - }>; - error?: string; - }>; // Energy tracking addEnergyToTide: ( tideId: string, energyLevel: EnergyLevel, context?: string - ) => Promise; + ) => Promise; // Reports and analytics getTideReport: ( @@ -125,7 +69,7 @@ interface MCPContextType extends MCPState { taskTitle: string, taskType?: string ) => Promise; - getTaskLinks: (tideId: string) => Promise; + getTaskLinks: (tideId: string) => Promise; // Participants getTideParticipants: ( @@ -144,21 +88,22 @@ interface MCPProviderProps { export function MCPProvider({ children }: MCPProviderProps) { const { apiKey } = useAuth(); - const { getCurrentServerUrl: getEnvironmentServerUrl, currentEnvironment } = - useServerEnvironment(); const [state, dispatch] = useReducer(mcpReducer, initialMCPState); - // Configure authService and mcpService with current server URL + // Configure authService and mcpService with hardcoded server URL useEffect(() => { - if (getEnvironmentServerUrl) { - authService.setUrlProvider(getEnvironmentServerUrl); - mcpService.setUrlProvider(getEnvironmentServerUrl); - loggingService.info( - "MCPContext", - "AuthService and MCPService configured with environment URL provider" - ); - } - }, [getEnvironmentServerUrl]); + const hardcodedUrl = "https://tides-006.mpazbot.workers.dev"; + const urlProvider = () => hardcodedUrl; + + authService.setUrlProvider(urlProvider); + mcpService.setUrlProvider(urlProvider); + + loggingService.info( + "MCPContext", + "AuthService and MCPService configured with hardcoded URL", + { url: hardcodedUrl } + ); + }, []); const checkConnection = useCallback(async (): Promise => { loggingService.info("MCPContext", "Checking MCP connection", undefined); @@ -196,35 +141,9 @@ export function MCPProvider({ children }: MCPProviderProps) { } }, [apiKey]); - const updateServerUrl = useCallback(async (url: string): Promise => { - loggingService.info("MCPContext", "Updating server URL", { url }); - dispatch({ type: "SET_LOADING", payload: true }); - - try { - // Update AuthService URL - await authService.setWorkerUrl(url); - - // Update MCPService URL - await mcpService.updateServerUrl(url); - - // Reset connection state - dispatch({ type: "RESET_CONNECTION" }); - - loggingService.info("MCPContext", "Server URL updated successfully", { - url, - }); - } catch (error) { - loggingService.error("MCPContext", "Failed to update server URL", { - error, - url, - }); - dispatch({ type: "SET_ERROR", payload: "Failed to update server URL" }); - } - }, []); - const getCurrentServerUrl = useCallback((): string => { - return getEnvironmentServerUrl(); - }, [getEnvironmentServerUrl]); + return "https://tides-006.mpazbot.workers.dev"; + }, []); const refreshTides = useCallback(async (): Promise => { if (!state.isConnected) { @@ -259,20 +178,12 @@ export function MCPProvider({ children }: MCPProviderProps) { }, [state.isConnected]); const createTide = useCallback( - async ( - name: string, - description?: string, - flowType?: "daily" | "weekly" | "project" | "seasonal" - ): Promise => { - loggingService.info("MCPContext", "Creating tide", { name, flowType }); + async (name: string, description?: string): Promise => { + loggingService.info("MCPContext", "Creating tide"); dispatch({ type: "SET_LOADING", payload: true }); try { - const response = await mcpService.createTide( - name, - description, - flowType - ); + const response = await mcpService.createTide(name, description); if (response.success && response.tide_id) { // Create tide object from response const newTide: Tide = { @@ -281,14 +192,7 @@ export function MCPProvider({ children }: MCPProviderProps) { status: (response.status as "active" | "completed" | "paused") || "active", - flow_type: - (response.flow_type as - | "daily" - | "weekly" - | "project" - | "seasonal") || - flowType || - "project", + created_at: response.created_at || new Date().toISOString(), updated_at: new Date().toISOString(), description: response.description || description, @@ -332,7 +236,7 @@ export function MCPProvider({ children }: MCPProviderProps) { tideId: string, energyLevel: EnergyLevel, context?: string - ): Promise => { + ): Promise => { loggingService.info("MCPContext", "Adding energy to tide", { tideId, energyLevel, @@ -515,7 +419,7 @@ export function MCPProvider({ children }: MCPProviderProps) { ); const getTaskLinks = useCallback( - async (tideId: string): Promise => { + async (tideId: string): Promise => { loggingService.info("MCPContext", "Getting task links", { tideId }); dispatch({ type: "SET_LOADING", payload: true }); @@ -593,15 +497,14 @@ export function MCPProvider({ children }: MCPProviderProps) { [] ); - // Hierarchical context management functions - const getOrCreateDailyTide = useCallback(async (timezone?: string) => { - loggingService.info("MCPContext", "Getting or creating daily tide", { + const getOrCreateTide = useCallback(async (timezone?: string) => { + loggingService.info("MCPContext", "Getting or creating tide", { timezone, }); dispatch({ type: "SET_LOADING", payload: true }); try { - const response = await mcpService.callTool("tide_get_or_create_daily", { + const response = await mcpService.callTool("tide_get_or_create", { timezone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, }); @@ -614,7 +517,7 @@ export function MCPProvider({ children }: MCPProviderProps) { } loggingService.info( "MCPContext", - response.created ? "Created daily tide" : "Retrieved daily tide", + response.created ? "Created tide" : "Retrieved tide", { tideId: response.tide?.id, tideName: response.tide?.name, @@ -622,242 +525,23 @@ export function MCPProvider({ children }: MCPProviderProps) { ); return response; } else { - throw new Error(response.error || "Failed to get or create daily tide"); + throw new Error(response.error || "Failed to get or create tide"); } } catch (error) { - loggingService.error("MCPContext", "Failed to get or create daily tide", { + loggingService.error("MCPContext", "Failed to get or create tide", { error, timezone, }); dispatch({ type: "SET_ERROR", - payload: "Failed to get or create daily tide.", - }); - throw error; - } - }, []); - - const switchTideContext = useCallback( - async ( - contextType: "daily" | "weekly" | "monthly" | "project", - date?: string - ) => { - loggingService.info("MCPContext", "Switching tide context", { - contextType, - date, - }); - dispatch({ type: "SET_LOADING", payload: true }); - - try { - const response = await mcpService.callTool("tide_switch_context", { - context_type: contextType, - date: date || new Date().toISOString().split("T")[0], - }); - - if (response.success) { - // Update selected tide if a tide was returned - if (response.tide) { - dispatch({ type: "SELECT_TIDE", payload: response.tide }); - // Add to tides list if newly created - if (response.created) { - dispatch({ type: "ADD_TIDE", payload: response.tide }); - } - } - - loggingService.info("MCPContext", "Context switched successfully", { - contextType, - tideId: response.tide?.id, - created: response.created, - }); - return response; - } else { - throw new Error(response.error || "Failed to switch context"); - } - } catch (error) { - loggingService.error("MCPContext", "Failed to switch tide context", { - error, - contextType, - date, - }); - dispatch({ type: "SET_ERROR", payload: "Failed to switch context." }); - throw error; - } - }, - [] - ); - - const listTideContexts = useCallback(async (date?: string) => { - loggingService.info("MCPContext", "Listing tide contexts", { date }); - dispatch({ type: "SET_LOADING", payload: true }); - - try { - const response = await mcpService.callTool("tide_list_contexts", { - date: date || new Date().toISOString().split("T")[0], - }); - - if (response.success) { - loggingService.info("MCPContext", "Listed tide contexts", { - contextsCount: response.contexts?.length || 0, - date, - }); - return response; - } else { - throw new Error(response.error || "Failed to list contexts"); - } - } catch (error) { - loggingService.error("MCPContext", "Failed to list tide contexts", { - error, - date, - }); - dispatch({ type: "SET_ERROR", payload: "Failed to list contexts." }); - throw error; - } - }, []); - - const getTodaysSummary = useCallback(async (date?: string) => { - loggingService.info("MCPContext", "Getting today's summary", { date }); - dispatch({ type: "SET_LOADING", payload: true }); - - try { - const response = await mcpService.callTool("tide_get_todays_summary", { - date: date || new Date().toISOString().split("T")[0], - }); - - if (response.success) { - loggingService.info("MCPContext", "Retrieved today's summary", { - totalSessions: response.total_flow_sessions, - totalMinutes: response.total_minutes, - contextsCount: response.contexts?.length || 0, - }); - return response; - } else { - throw new Error(response.error || "Failed to get today's summary"); - } - } catch (error) { - loggingService.error("MCPContext", "Failed to get today's summary", { - error, - date, - }); - dispatch({ - type: "SET_ERROR", - payload: "Failed to get today's summary.", + payload: "Failed to get or create tide.", }); throw error; } }, []); - const startHierarchicalFlow = useCallback( - async ( - intensity?: "gentle" | "moderate" | "strong", - duration?: number, - initialEnergy?: "low" | "medium" | "high", - workContext?: string, - date?: string - ) => { - loggingService.info("MCPContext", "Starting hierarchical flow", { - intensity, - duration, - initialEnergy, - workContext, - date, - }); - dispatch({ type: "SET_LOADING", payload: true }); - - try { - const response = await mcpService.callTool( - "tide_start_hierarchical_flow", - { - intensity: intensity || "moderate", - duration: duration || 25, - initial_energy: initialEnergy || "medium", - work_context: workContext || "General work", - date: date || new Date().toISOString().split("T")[0], - } - ); - - if (response.success) { - // Refresh tides to get updated data - await refreshTides(); - - loggingService.info("MCPContext", "Hierarchical flow started", { - sessionId: response.session_id, - contextsCount: response.contexts?.length || 0, - }); - return response; - } else { - throw new Error( - response.error || "Failed to start hierarchical flow" - ); - } - } catch (error) { - loggingService.error( - "MCPContext", - "Failed to start hierarchical flow", - { - error, - intensity, - duration, - initialEnergy, - workContext, - } - ); - dispatch({ - type: "SET_ERROR", - payload: "Failed to start hierarchical flow.", - }); - throw error; - } - }, - [refreshTides] - ); - - // Effect to handle environment changes - useEffect(() => { - const handleEnvironmentChange = async () => { - const newServerUrl = getEnvironmentServerUrl(); - - loggingService.info( - "MCPContext", - "Environment changed, updating server URL", - { - environment: currentEnvironment, - serverUrl: newServerUrl, - } - ); - - try { - // Update AuthService URL - await authService.setWorkerUrl(newServerUrl); - - // Update MCPService URL - await mcpService.updateServerUrl(newServerUrl); - - // Reset connection state to force re-connection - dispatch({ type: "RESET_CONNECTION" }); - - loggingService.info( - "MCPContext", - "Server URL updated for environment change", - { - environment: currentEnvironment, - serverUrl: newServerUrl, - } - ); - } catch (error) { - loggingService.error( - "MCPContext", - "Failed to update server URL for environment change", - { error, environment: currentEnvironment, serverUrl: newServerUrl } - ); - dispatch({ - type: "SET_ERROR", - payload: "Failed to update server URL for new environment", - }); - } - }; - - handleEnvironmentChange(); - }, [currentEnvironment, getEnvironmentServerUrl]); + // Environment changes disabled - using hardcoded URL + // useEffect removed to avoid conflicts with hardcoded configuration // Effect to check connection when API key changes useEffect(() => { @@ -976,9 +660,9 @@ export function MCPProvider({ children }: MCPProviderProps) { () => ({ ...state, checkConnection, - updateServerUrl, getCurrentServerUrl, createTide, + getOrCreateTide, refreshTides, selectTide, startTideFlow, @@ -987,19 +671,13 @@ export function MCPProvider({ children }: MCPProviderProps) { linkTaskToTide, getTaskLinks, getTideParticipants, - // Hierarchical context management - getOrCreateDailyTide, - switchTideContext, - listTideContexts, - getTodaysSummary, - startHierarchicalFlow, }), [ state, checkConnection, - updateServerUrl, getCurrentServerUrl, createTide, + getOrCreateTide, refreshTides, selectTide, startTideFlow, @@ -1008,12 +686,6 @@ export function MCPProvider({ children }: MCPProviderProps) { linkTaskToTide, getTaskLinks, getTideParticipants, - // Hierarchical context management - getOrCreateDailyTide, - switchTideContext, - listTideContexts, - getTodaysSummary, - startHierarchicalFlow, ] ); diff --git a/apps/mobile/src/context/ServerEnvironmentContext.tsx b/apps/mobile/src/context/ServerEnvironmentContext.tsx deleted file mode 100644 index 3dad36a..0000000 --- a/apps/mobile/src/context/ServerEnvironmentContext.tsx +++ /dev/null @@ -1,314 +0,0 @@ -// Server Environment Context for centralized server configuration management - -import React, { - createContext, - useContext, - useEffect, - useReducer, - useMemo, - useCallback, - ReactNode, -} from "react"; -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { loggingService } from "../services/loggingService"; -import { authService } from "../services/authService"; -import type { - ServerEnvironmentState, - ServerEnvironmentAction, - ServerEnvironmentId, - ServerEnvironment, -} from "./ServerEnvironmentTypes"; -import { - SERVER_ENVIRONMENTS, - DEFAULT_ENVIRONMENT, -} from "./ServerEnvironmentTypes"; - -const STORAGE_KEY = "tides_server_environment"; - -// Initial state -const initialState: ServerEnvironmentState = { - currentEnvironment: DEFAULT_ENVIRONMENT, - environments: SERVER_ENVIRONMENTS, - isLoading: false, - error: null, - lastSwitched: null, -}; - -// Reducer -function serverEnvironmentReducer( - state: ServerEnvironmentState, - action: ServerEnvironmentAction -): ServerEnvironmentState { - switch (action.type) { - case "SET_ENVIRONMENT": - return { - ...state, - currentEnvironment: action.payload, - error: null, - }; - case "SET_LOADING": - return { - ...state, - isLoading: action.payload, - }; - case "SET_ERROR": - return { - ...state, - error: action.payload, - isLoading: false, - }; - case "ENVIRONMENT_SWITCHED": - return { - ...state, - currentEnvironment: action.payload.environmentId, - lastSwitched: action.payload.timestamp, - isLoading: false, - error: null, - }; - case "RESET_STATE": - return initialState; - default: - return state; - } -} - -// Context type -interface ServerEnvironmentContextType extends ServerEnvironmentState { - switchEnvironment: (environmentId: ServerEnvironmentId) => Promise; - getCurrentEnvironment: () => ServerEnvironment; - getCurrentServerUrl: () => string; - getEnvironmentById: (id: ServerEnvironmentId) => ServerEnvironment; - resetToDefault: () => Promise; -} - -const ServerEnvironmentContext = createContext< - ServerEnvironmentContextType | undefined ->(undefined); - -interface ServerEnvironmentProviderProps { - children: ReactNode; - onEnvironmentChange?: (environment: ServerEnvironment) => void; -} - -export function ServerEnvironmentProvider({ - children, - onEnvironmentChange, -}: ServerEnvironmentProviderProps) { - const [state, dispatch] = useReducer(serverEnvironmentReducer, initialState); - - // Load saved environment on mount - useEffect(() => { - const loadSavedEnvironment = async () => { - try { - loggingService.info( - "ServerEnvironmentContext", - "Loading saved environment preference", - undefined - ); - - const savedEnvironmentId = await AsyncStorage.getItem(STORAGE_KEY); - - if (savedEnvironmentId && savedEnvironmentId in SERVER_ENVIRONMENTS) { - const environmentId = savedEnvironmentId as ServerEnvironmentId; - dispatch({ type: "SET_ENVIRONMENT", payload: environmentId }); - - // Initialize AuthService with the saved environment URL - const serverUrl = SERVER_ENVIRONMENTS[environmentId].url; - await authService.setWorkerUrl(serverUrl); - - loggingService.info( - "ServerEnvironmentContext", - "Loaded saved environment and initialized AuthService", - { environmentId, serverUrl } - ); - } else { - // Initialize AuthService with default environment URL - const defaultServerUrl = SERVER_ENVIRONMENTS[DEFAULT_ENVIRONMENT].url; - await authService.setWorkerUrl(defaultServerUrl); - - loggingService.info( - "ServerEnvironmentContext", - "No saved environment found, using default and initialized AuthService", - { - defaultEnvironment: DEFAULT_ENVIRONMENT, - serverUrl: defaultServerUrl, - } - ); - } - } catch (error) { - loggingService.error( - "ServerEnvironmentContext", - "Failed to load saved environment", - { error } - ); - // Continue with default environment - } - }; - - loadSavedEnvironment(); - }, []); - - // Switch environment function - const switchEnvironment = useCallback( - async (environmentId: ServerEnvironmentId): Promise => { - if (!(environmentId in SERVER_ENVIRONMENTS)) { - const error = `Invalid environment ID: ${environmentId}`; - loggingService.error( - "ServerEnvironmentContext", - "Invalid environment switch attempt", - { environmentId } - ); - dispatch({ type: "SET_ERROR", payload: error }); - throw new Error(error); - } - - if (state.currentEnvironment === environmentId) { - loggingService.info( - "ServerEnvironmentContext", - "Environment already active", - { environmentId } - ); - return; - } - - dispatch({ type: "SET_LOADING", payload: true }); - - try { - loggingService.info( - "ServerEnvironmentContext", - "Switching environment", - { - from: state.currentEnvironment, - to: environmentId, - environment: SERVER_ENVIRONMENTS[environmentId], - } - ); - - // Save to AsyncStor - await AsyncStorage.setItem(STORAGE_KEY, environmentId); - - // Update AuthService with new URL - const newServerUrl = SERVER_ENVIRONMENTS[environmentId].url; - await authService.setWorkerUrl(newServerUrl); - - // Update state - const timestamp = new Date().toISOString(); - dispatch({ - type: "ENVIRONMENT_SWITCHED", - payload: { environmentId, timestamp }, - }); - - // Notify callback if provided - if (onEnvironmentChange) { - onEnvironmentChange(SERVER_ENVIRONMENTS[environmentId]); - } - - loggingService.info( - "ServerEnvironmentContext", - "Environment switched successfully", - { - environmentId, - environment: SERVER_ENVIRONMENTS[environmentId].name, - url: SERVER_ENVIRONMENTS[environmentId].url, - } - ); - } catch (error) { - loggingService.error( - "ServerEnvironmentContext", - "Failed to switch environment", - { error, environmentId } - ); - - dispatch({ - type: "SET_ERROR", - payload: "Failed to switch environment", - }); - - throw error; - } - }, - [state.currentEnvironment, onEnvironmentChange] - ); - - // Get current environment - const getCurrentEnvironment = useCallback((): ServerEnvironment => { - return SERVER_ENVIRONMENTS[state.currentEnvironment]; - }, [state.currentEnvironment]); - - // Get current server URL - const getCurrentServerUrl = useCallback((): string => { - return SERVER_ENVIRONMENTS[state.currentEnvironment].url; - }, [state.currentEnvironment]); - - // Get environment by ID - const getEnvironmentById = useCallback( - (id: ServerEnvironmentId): ServerEnvironment => { - return SERVER_ENVIRONMENTS[id]; - }, - [] - ); - - // Reset to default environment - const resetToDefault = useCallback(async (): Promise => { - loggingService.info( - "ServerEnvironmentContext", - "Resetting to default environment", - { defaultEnvironment: DEFAULT_ENVIRONMENT } - ); - - try { - await AsyncStorage.removeItem(STORAGE_KEY); - await switchEnvironment(DEFAULT_ENVIRONMENT); - - loggingService.info( - "ServerEnvironmentContext", - "Reset to default environment completed", - { defaultEnvironment: DEFAULT_ENVIRONMENT } - ); - } catch (error) { - loggingService.error( - "ServerEnvironmentContext", - "Failed to reset to default environment", - { error } - ); - throw error; - } - }, [switchEnvironment]); - - // Memoize context value - const contextValue = useMemo( - () => ({ - ...state, - switchEnvironment, - getCurrentEnvironment, - getCurrentServerUrl, - getEnvironmentById, - resetToDefault, - }), - [ - state, - switchEnvironment, - getCurrentEnvironment, - getCurrentServerUrl, - getEnvironmentById, - resetToDefault, - ] - ); - - return ( - - {children} - - ); -} - -// Hook to use server environment context -export function useServerEnvironment(): ServerEnvironmentContextType { - const context = useContext(ServerEnvironmentContext); - if (context === undefined) { - throw new Error( - "useServerEnvironment must be used within a ServerEnvironmentProvider" - ); - } - return context; -} diff --git a/apps/mobile/src/context/ServerEnvironmentTypes.ts b/apps/mobile/src/context/ServerEnvironmentTypes.ts deleted file mode 100644 index d22e507..0000000 --- a/apps/mobile/src/context/ServerEnvironmentTypes.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Server Environment Types for Tides Mobile App - -export type ServerEnvironmentId = - | "env001" - | "env002" - | "env003" - | "env006" - -export interface ServerEnvironment { - id: ServerEnvironmentId; - name: string; - description: string; - url: string; - environment: string; - features: string[]; - isDefault?: boolean; -} - -export interface ServerEnvironmentState { - currentEnvironment: ServerEnvironmentId; - environments: Record; - isLoading: boolean; - error: string | null; - lastSwitched: string | null; -} - -export type ServerEnvironmentAction = - | { type: "SET_ENVIRONMENT"; payload: ServerEnvironmentId } - | { type: "SET_LOADING"; payload: boolean } - | { type: "SET_ERROR"; payload: string | null } - | { - type: "ENVIRONMENT_SWITCHED"; - payload: { environmentId: ServerEnvironmentId; timestamp: string }; - } - | { type: "RESET_STATE" }; - -export const SERVER_ENVIRONMENTS: Record< - ServerEnvironmentId, - ServerEnvironment -> = { - env001: { - id: "env001", - name: "Production", - description: "Production environment with full D1 and AI capabilities", - url: "https://tides-001.mpazbot.workers.dev", - environment: "production", // As per wrangler.jsonc vars.ENVIRONMENT - features: ["D1 Database", "Durable Objects", "AI Binding", "R2 Storage"], - isDefault: true, - }, - env002: { - id: "env002", - name: "Staging", - description: "Staging environment with demo mode and dual databases", - url: "https://tides-002.mpazbot.workers.dev", - environment: "staging", - features: [ - "D1 Database", - "Supabase DB", - "KV Storage", - "Demo Mode", - "Durable Objects", - ], - }, - env003: { - id: "env003", - name: "Development", - description: "Development environment for testing new features", - url: "https://tides-003.mpazbot.workers.dev", - environment: "development", // As per wrangler.jsonc vars.ENVIRONMENT - features: ["D1 Database", "Durable Objects", "AI Binding"], - }, - env006: { - id: "env006", - name: "Mason Development (Working)", - description: "Mason's development environment with complete auth setup", - url: "https://tides-006.mpazbot.workers.dev", - environment: "mason-development", - features: ["D1 Database", "API Key Authentication", "Supabase Auth", "Durable Objects", "Working MCP Flow"], - }, -}; - -export const DEFAULT_ENVIRONMENT: ServerEnvironmentId = "env001"; diff --git a/apps/mobile/src/context/TideContext.tsx b/apps/mobile/src/context/TideContext.tsx new file mode 100644 index 0000000..d968bd1 --- /dev/null +++ b/apps/mobile/src/context/TideContext.tsx @@ -0,0 +1,160 @@ +import React, { + createContext, + useContext, + useReducer, + useEffect, + useCallback, + ReactNode, +} from "react"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { mcpService } from "../services/mcpService"; +import type { + TideState, + TideAction, + TideContextType, + Message, +} from "./tideTypes"; +import { TIDE_CONFIG } from "./tideTypes"; + +const initialState: TideState = { + currentTide: null, + messages: [], + isLoading: false, + error: null, +}; + +function tideReducer(state: TideState, action: TideAction): TideState { + switch (action.type) { + case "SET_CURRENT_TIDE": + return { ...state, currentTide: action.payload, error: null }; + case "SET_MESSAGES": + return { ...state, messages: action.payload }; + case "ADD_MESSAGE": + const newMessages = [...state.messages, action.payload]; + return { + ...state, + messages: + newMessages.length > TIDE_CONFIG.MAX_MESSAGES_PER_TIDE + ? newMessages.slice(-TIDE_CONFIG.MAX_MESSAGES_PER_TIDE) + : newMessages, + }; + case "SET_LOADING": + return { ...state, isLoading: action.payload }; + case "SET_ERROR": + return { ...state, error: action.payload, isLoading: false }; + case "RESET_STATE": + return initialState; + default: + return state; + } +} + +const TideContext = createContext(undefined); + +export function TideProvider({ children }: { children: ReactNode }) { + const [state, dispatch] = useReducer(tideReducer, initialState); + + const executeTideAction = useCallback( + async (action: () => Promise, errorMsg: string) => { + dispatch({ type: "SET_LOADING", payload: true }); + try { + const response = await action(); + const tide = + response.success && + (response.tides?.[0] || response.tide || response.result?.tide); + if (tide) { + dispatch({ type: "SET_CURRENT_TIDE", payload: tide }); + await AsyncStorage.setItem("current_tide_id", tide.id); + await loadMessages(tide.id); + } + } catch { + dispatch({ type: "SET_ERROR", payload: errorMsg }); + } finally { + dispatch({ type: "SET_LOADING", payload: false }); + } + }, + [] + ); + + const loadCurrentTide = useCallback( + () => + executeTideAction( + () => mcpService.getOrCreateTide(), + "Failed to load tide" + ), + [executeTideAction] + ); + + const createNewTide = useCallback( + (name: string, description?: string) => { + return executeTideAction( + () => mcpService.createTide(name, description), + "Failed to create tide" + ); + }, + [executeTideAction] + ); + + useEffect(() => { + loadCurrentTide(); + }, [loadCurrentTide]); + + const addMessage = useCallback( + (messageData: Omit) => { + if (!state.currentTide) return; + dispatch({ + type: "ADD_MESSAGE", + payload: { + ...messageData, + id: `msg_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + timestamp: new Date().toISOString(), + tideId: state.currentTide.id, + }, + }); + }, + [state.currentTide] + ); + + const loadMessages = useCallback(async (tideId: string) => { + const messagesJson = await AsyncStorage.getItem(`tide_messages_${tideId}`); + const messages = messagesJson + ? JSON.parse(messagesJson).filter((msg: Message) => msg.tideId === tideId) + : []; + dispatch({ type: "SET_MESSAGES", payload: messages }); + }, []); + + const saveMessages = useCallback(async () => { + if (state.currentTide && state.messages.length > 0) { + await AsyncStorage.setItem( + `tide_messages_${state.currentTide.id}`, + JSON.stringify(state.messages) + ); + } + }, [state.currentTide, state.messages]); + + useEffect(() => { + if (state.currentTide && state.messages.length > 0) saveMessages(); + }, [state.messages, state.currentTide, saveMessages]); + + return ( + dispatch({ type: "RESET_STATE" }), + setError: (error: string | null) => + dispatch({ type: "SET_ERROR", payload: error }), + }} + > + {children} + + ); +} + +export function useTide() { + const context = useContext(TideContext); + if (!context) throw new Error("useTide must be used within a TideProvider"); + return context; +} diff --git a/apps/mobile/src/context/TimeContext.tsx b/apps/mobile/src/context/TimeContext.tsx index 4f4f6a1..ef36f1a 100644 --- a/apps/mobile/src/context/TimeContext.tsx +++ b/apps/mobile/src/context/TimeContext.tsx @@ -1,107 +1,176 @@ -import React, { createContext, useContext, useState, ReactNode, useCallback } from "react"; -import { useContextTide } from "../hooks/useContextTide"; +import React, { + createContext, + useContext, + useState, + useEffect, + useCallback, + ReactNode, + useRef, +} from "react"; +import * as RNLocalize from "react-native-localize"; +import * as SunCalc from "suncalc"; +import Geolocation from "@react-native-community/geolocation"; +import { LocationInfo } from "../types/charts"; -export type TimeContextType = "daily" | "weekly" | "monthly" | "project"; +interface TimeInfo { + localTime: Date; + timezone: string; + formattedTime: string; + formattedDate: string; + timestamp: number; +} -interface TimeContextValue { - currentContext: TimeContextType; - setCurrentContext: (context: TimeContextType) => void; - dateOffset: number; - setDateOffset: (offset: number) => void; - navigateBackward: () => void; - navigateForward: () => void; - resetToPresent: () => void; - isAtPresent: boolean; - // Context-aware tide integration - contextSwitchingDisabled: boolean; - getCurrentContextTideId: () => string | null; +interface SolarInfo { + sunrise: Date; + sunset: Date; + solarNoon: Date; + goldenHour: Date; + azimuth: number; + altitude: number; } -const TimeContext = createContext(undefined); +interface LocationData extends LocationInfo { + city?: string; + region?: string; + country?: string; + formattedAddress?: string; +} -interface TimeContextProviderProps { - children: ReactNode; +interface TimeContextValue { + timeInfo: TimeInfo | null; + locationInfo: LocationData | null; + solarInfo: SolarInfo | null; + loading: boolean; + error: string | null; + permissions: "granted" | "denied" | "not-requested" | "requesting"; + refreshLocation: () => Promise; + getTimeOfDay: () => "morning" | "afternoon" | "evening" | "night"; } -export const TimeContextProvider: React.FC = ({ +const TimeContext = createContext(undefined); + +export const TimeContextProvider: React.FC<{ children: ReactNode }> = ({ children, }) => { - const [currentContext, setCurrentContext] = useState("daily"); - const [dateOffset, setDateOffsetState] = useState(0); - - // Integration with context tide system - const { - switchContext, - contextSwitchingDisabled, - getCurrentContextTideId, - } = useContextTide(); - - // Navigation functions - const navigateBackward = useCallback(() => { - setDateOffsetState(prev => prev + 1); - }, []); + const [timeInfo, setTimeInfo] = useState(null); + const [locationInfo, setLocationInfo] = useState(null); + const [solarInfo, setSolarInfo] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [permissions, setPermissions] = useState< + "granted" | "denied" | "not-requested" | "requesting" + >("not-requested"); + const intervalRef = useRef(null); - const navigateForward = useCallback(() => { - setDateOffsetState(prev => Math.max(0, prev - 1)); + const calculateTimeInfo = useCallback((): TimeInfo => { + const now = new Date(); + return { + localTime: now, + timezone: RNLocalize.getTimeZone(), + formattedTime: now.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + }), + formattedDate: now.toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }), + timestamp: now.getTime(), + }; }, []); - const resetToPresent = useCallback(() => { - setDateOffsetState(0); - }, []); + const fetchLocation = useCallback(async () => { + setLoading(true); + try { + const position = await new Promise((resolve, reject) => { + Geolocation.getCurrentPosition(resolve, reject, { timeout: 10000 }); + }); - const setDateOffset = useCallback((offset: number) => { - // Ensure offset can't be negative (no future dates) - setDateOffsetState(Math.max(0, offset)); - }, []); + const { latitude, longitude } = position.coords; + const now = new Date(); + const sunTimes = SunCalc.getTimes(now, latitude, longitude); + const sunPosition = SunCalc.getPosition(now, latitude, longitude); - // Enhanced context switching with tide system integration - const setCurrentContextWithReset = useCallback((context: TimeContextType) => { - // Handle project type separately (existing functionality) - if (context === 'project') { - setCurrentContext(context); - setDateOffsetState(0); - return; + const currentTime = now.getTime(); + let timeOfDay: "morning" | "afternoon" | "evening" | "night" = "night"; + if ( + currentTime >= sunTimes.sunrise.getTime() && + currentTime < sunTimes.solarNoon.getTime() + ) { + timeOfDay = "morning"; + } else if ( + currentTime >= sunTimes.solarNoon.getTime() && + currentTime < sunTimes.goldenHour.getTime() + ) { + timeOfDay = "afternoon"; + } else if ( + currentTime >= sunTimes.goldenHour.getTime() && + currentTime < sunTimes.sunset.getTime() + ) { + timeOfDay = "evening"; + } + + setLocationInfo({ + latitude, + longitude, + sunrise: sunTimes.sunrise, + sunset: sunTimes.sunset, + timeOfDay, + }); + setSolarInfo({ + sunrise: sunTimes.sunrise, + sunset: sunTimes.sunset, + solarNoon: sunTimes.solarNoon, + goldenHour: sunTimes.goldenHour, + azimuth: sunPosition.azimuth, + altitude: sunPosition.altitude, + }); + setPermissions("granted"); + } catch (err) { + setError("Location access failed"); + setPermissions("denied"); + } finally { + setLoading(false); } + }, []); - // For daily/weekly/monthly: Switch UI immediately, sync in background - setCurrentContext(context); - setDateOffsetState(0); - - // Background sync with tide system (non-blocking) - switchContext(context as 'daily' | 'weekly' | 'monthly').catch(error => { - console.error('Failed to switch tide context:', error); - // UI is already switched, so this is just logging for now - // Could add error recovery here if needed - }); - }, [switchContext]); + const getTimeOfDay = useCallback( + () => locationInfo?.timeOfDay || "morning", + [locationInfo?.timeOfDay] + ); - const isAtPresent = dateOffset === 0; + useEffect(() => { + setTimeInfo(calculateTimeInfo()); + intervalRef.current = setInterval( + () => setTimeInfo(calculateTimeInfo()), + 30000 + ); + fetchLocation(); + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, [calculateTimeInfo, fetchLocation]); const value: TimeContextValue = { - currentContext, - setCurrentContext: setCurrentContextWithReset, - dateOffset, - setDateOffset, - navigateBackward, - navigateForward, - resetToPresent, - isAtPresent, - // Context-aware tide integration - contextSwitchingDisabled, - getCurrentContextTideId, + timeInfo, + locationInfo, + solarInfo, + loading, + error, + permissions, + refreshLocation: fetchLocation, + getTimeOfDay, }; - return ( - - {children} - - ); + return {children}; }; -export const useTimeContext = (): TimeContextValue => { +export const useTimeContext = () => { const context = useContext(TimeContext); - if (!context) { + if (!context) throw new Error("useTimeContext must be used within a TimeContextProvider"); - } return context; -}; \ No newline at end of file +}; diff --git a/apps/mobile/src/context/mcpTypes.ts b/apps/mobile/src/context/mcpTypes.ts index 23afdfe..092ee04 100644 --- a/apps/mobile/src/context/mcpTypes.ts +++ b/apps/mobile/src/context/mcpTypes.ts @@ -1,6 +1,6 @@ // MCP context types and reducer patterns for state management optimization -import type { Tide } from "../types"; +import type { Tide } from "../types/models"; export interface MCPState { isConnected: boolean; diff --git a/apps/mobile/src/context/tideTypes.ts b/apps/mobile/src/context/tideTypes.ts new file mode 100644 index 0000000..635f5a6 --- /dev/null +++ b/apps/mobile/src/context/tideTypes.ts @@ -0,0 +1,55 @@ +import type { Tide } from "../types/models"; + +// Message types for TideContext +export interface Message { + id: string; + type: "user" | "assistant" | "system" | "tool_result"; + content: string; + timestamp: string; + tideId: string; + metadata?: MessageMetadata; +} + +export interface MessageMetadata { + toolName?: string; + toolResult?: any; + agentResponse?: boolean; + error?: boolean; + conversationId?: string; + suggestedTools?: string[]; +} + +export interface TideState { + currentTide: Tide | null; + messages: Message[]; + isLoading: boolean; + error: string | null; +} + +// Tide context actions +export type TideAction = + | { type: "SET_CURRENT_TIDE"; payload: Tide } + | { type: "SET_MESSAGES"; payload: Message[] } + | { type: "ADD_MESSAGE"; payload: Message } + | { type: "SET_LOADING"; payload: boolean } + | { type: "SET_ERROR"; payload: string | null } + | { type: "RESET_STATE" }; + +export interface TideContextType extends TideState { + createNewTide: (name: string, description?: string) => Promise; + loadCurrentTide: () => Promise; + addMessage: (message: Omit) => void; + resetState: () => void; + setError: (error: string | null) => void; +} + +// Storage key helpers +export const STORAGE_KEYS = { + TIDE_MESSAGES: (tideId: string) => `tide_messages_${tideId}`, + CURRENT_TIDE_ID: "current_tide_id", +} as const; + +// Configuration constants +export const TIDE_CONFIG = { + MAX_MESSAGES_PER_TIDE: 100, +} as const; diff --git a/apps/mobile/src/design-system/index.ts b/apps/mobile/src/design-system/index.ts index c7b158f..b1d009d 100644 --- a/apps/mobile/src/design-system/index.ts +++ b/apps/mobile/src/design-system/index.ts @@ -15,5 +15,3 @@ export { Notification } from "../components/Notification"; export { SafeArea } from "../components/SafeArea"; export { Stack } from "../components/Stack"; export { Text } from "../components/Text"; - -// Note: ServerEnvironmentSelector is not exported as it's specific, not part of design system diff --git a/apps/mobile/src/design-system/tokens.ts b/apps/mobile/src/design-system/tokens.ts index 7731da4..a163cb3 100644 --- a/apps/mobile/src/design-system/tokens.ts +++ b/apps/mobile/src/design-system/tokens.ts @@ -4,7 +4,7 @@ export const colors = { tableIcon: "#8C9EB1", titleColor: "#0A2540", textColor: "#425466", - backgroundColor: "#FAF5F0", + backgroundColor: "#F6F9FC", inputBackground: "#F6F9FC", checkboxInputBackground: "#E7ECF1", inputPlaceholder: "#727F96", diff --git a/apps/mobile/src/hooks/useChatInput.ts b/apps/mobile/src/hooks/useChatInput.ts deleted file mode 100644 index 6906a50..0000000 --- a/apps/mobile/src/hooks/useChatInput.ts +++ /dev/null @@ -1,391 +0,0 @@ -import { useState, useCallback, useEffect, useRef } from "react"; -import { loggingService } from "../services/loggingService"; -import { phraseDetectionService } from "../services/phraseDetectionService"; -import type { DetectedTool } from "../config/toolPhrases"; - -interface UseChatInputReturn { - // State - inputMessage: string; - toolSuggestion: DetectedTool | null; - showSuggestion: boolean; - - // Actions - setInputMessage: (message: string) => void; - handleSendMessage: () => Promise; - acceptSuggestion: () => void; - dismissSuggestion: () => void; -} - -interface UseChatInputProps { - getCurrentContextTideId?: () => string | null; // Context-aware tide ID - isConnected: boolean; - getCurrentServerUrl: () => string; - sendMessage: (message: string) => Promise; - runDebugTests?: () => Promise; - testEdgeCases?: () => Promise; - setDebugTestResults?: (results: string[]) => void; - executeMCPTool?: (toolName: string, params: Record) => Promise; -} - -export const useChatInput = ({ - getCurrentContextTideId, - isConnected, - getCurrentServerUrl, - sendMessage, - runDebugTests, - testEdgeCases, - setDebugTestResults, - executeMCPTool, -}: UseChatInputProps): UseChatInputReturn => { - // State management - const [inputMessage, setInputMessage] = useState(""); - const [toolSuggestion, setToolSuggestion] = useState(null); - const [showSuggestion, setShowSuggestion] = useState(false); - - // Debounce timer ref - const detectionTimerRef = useRef(null); - - // Detect tool intent when input changes - useEffect(() => { - if (detectionTimerRef.current) { - clearTimeout(detectionTimerRef.current); - } - - if (!inputMessage || inputMessage.length < 3) { - setToolSuggestion(null); - setShowSuggestion(false); - return; - } - - // Debounce detection for 300ms - detectionTimerRef.current = setTimeout(() => { - const detected = phraseDetectionService.detectToolIntent(inputMessage); - - if (detected) { - setToolSuggestion(detected); - setShowSuggestion(true); - - loggingService.info("ChatInput", "Tool suggestion detected", { - input: inputMessage.substring(0, 50), - toolId: detected.toolId, - confidence: detected.confidence, - }); - } else { - setToolSuggestion(null); - setShowSuggestion(false); - } - }, 300); - - return () => { - if (detectionTimerRef.current) { - clearTimeout(detectionTimerRef.current); - } - }; - }, [inputMessage]); - - // Parse tool parameter templates - const parseToolTemplate = useCallback((message: string) => { - // Check if message matches tool template pattern like "/flow [param: value]" - const templateMatch = message.match(/^\/(\w+)\s+(.+)$/); - if (!templateMatch) return null; - - const [, toolName, paramString] = templateMatch; - const params: Record = {}; - const missingParams: string[] = []; - - // Extract parameters in [key: value] format - const paramMatches = paramString.match(/\[([^:]+):\s*([^\]]+)\]/g); - if (!paramMatches) return null; - - paramMatches.forEach(match => { - const paramMatch = match.match(/\[([^:]+):\s*([^\]]+)\]/); - if (paramMatch) { - const [, key, value] = paramMatch; - const trimmedKey = key.trim(); - const trimmedValue = value.trim(); - - if (trimmedValue === '___' || trimmedValue === '') { - missingParams.push(trimmedKey); - } else { - params[trimmedKey] = trimmedValue; - } - } - }); - - return { - toolName, - params, - missingParams, - isComplete: missingParams.length === 0 - }; - }, []); - - // Map template parameters to MCP tool parameters - const mapTemplateParamsToMCP = useCallback((toolName: string, templateParams: Record) => { - const contextTideId = getCurrentContextTideId?.(); - const now = new Date(); - - switch (toolName) { - case 'tide_smart_flow': - return { - tideId: contextTideId, - intensity: templateParams.energy === 'low' ? 'gentle' : - templateParams.energy === 'medium' ? 'moderate' : - templateParams.energy === 'high' ? 'intense' : 'moderate', - duration: parseInt(templateParams.duration, 10) || 25, - workContext: templateParams.what || 'focus work', - initialEnergy: templateParams.energy || 'medium', - timestamp: now.toISOString(), - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }; - case 'tide_add_energy': - return { - tideId: contextTideId, - energyLevel: templateParams.level || 'medium', - context: templateParams.context || `Energy added at ${now.toLocaleTimeString()}`, - timestamp: now.toISOString(), - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }; - case 'tide_link_task': - return { - tideId: contextTideId, - taskUrl: templateParams.url || `https://task-${Date.now()}`, - taskTitle: templateParams.task || 'New Task', - taskType: templateParams.type || 'general', - timestamp: now.toISOString(), - }; - case 'tide_get_report': - return { - tideId: contextTideId, - period: templateParams.period || 'today', - format: templateParams.format || 'summary', - }; - default: - return templateParams; - } - }, [getCurrentContextTideId]); - - const handleSendMessage = useCallback(async () => { - if (!inputMessage.trim()) return; - - const message = inputMessage.trim(); - setInputMessage(""); - setToolSuggestion(null); - setShowSuggestion(false); - - // Check for debug commands (keep these local) - if (message === "/debug" && runDebugTests) { - runDebugTests(); - return; - } else if (message === "/debug edge" && testEdgeCases) { - testEdgeCases(); - return; - } else if (message === "/debug hide" && setDebugTestResults) { - setDebugTestResults([]); - return; - } - - // Check if message is a tool template - const parsedTemplate = parseToolTemplate(message); - if (parsedTemplate) { - if (parsedTemplate.isComplete && executeMCPTool) { - // Execute tool directly with complete parameters - const toolName = parsedTemplate.toolName === 'flow' ? 'tide_smart_flow' : - parsedTemplate.toolName === 'energy' ? 'tide_add_energy' : - parsedTemplate.toolName === 'link' ? 'tide_link_task' : - parsedTemplate.toolName === 'report' ? 'tide_get_report' : - parsedTemplate.toolName; - - // Map template params to MCP tool params - const mcpParams = mapTemplateParamsToMCP(toolName, parsedTemplate.params); - - loggingService.info("ChatInput", "Executing complete tool template", { - toolName, - params: mcpParams, - }); - - try { - await executeMCPTool(toolName, mcpParams); - return; - } catch (error) { - loggingService.error("ChatInput", "Tool execution failed", { error, toolName, params: mcpParams }); - } - } else { - // Send to agent for parameter gathering - loggingService.info("ChatInput", "Tool template incomplete, routing to agent", { - toolName: parsedTemplate.toolName, - missingParams: parsedTemplate.missingParams, - providedParams: parsedTemplate.params, - }); - } - } - - // For all other messages, automatically query the agent with context-aware tide information - const contextTideId = getCurrentContextTideId?.(); - const context = { - // Current context tide (daily/weekly/monthly) - ...(contextTideId && { - contextTideId, - contextType: "hierarchical", // Indicate this is from context system - }), - - // Current app state - currentScreen: "Home", - contextBasedSystem: true, // Flag to indicate new context-based architecture - - // Connection state - isConnected, - currentServerUrl: getCurrentServerUrl(), - - // Timestamp for context - requestedAt: new Date().toISOString(), - }; - - loggingService.info("Chat", "Sending message to agent with context-aware information", { - messageLength: message.length, - contextKeys: Object.keys(context), - contextTideId, - hasContextTide: !!contextTideId, - }); - - await sendMessage(message); - }, [ - inputMessage, - sendMessage, - runDebugTests, - testEdgeCases, - setDebugTestResults, - getCurrentContextTideId, - isConnected, - getCurrentServerUrl, - parseToolTemplate, - mapTemplateParamsToMCP, - executeMCPTool, - ]); - - // Accept the tool suggestion - const acceptSuggestion = useCallback(() => { - if (!toolSuggestion || !executeMCPTool) return; - - loggingService.info("ChatInput", "Tool suggestion accepted", { - toolId: toolSuggestion.toolId, - extractedParams: toolSuggestion.extractedParams, - }); - - // Clear input and suggestion - setInputMessage(""); - setToolSuggestion(null); - setShowSuggestion(false); - - // Generate default params for the tool - const now = new Date(); - const dateString = now.toLocaleDateString(); - const timeString = now.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - }); - - let params = { ...toolSuggestion.extractedParams }; - - // Add smart defaults based on tool type - switch (toolSuggestion.toolId) { - case "createTide": - params = { - name: params.name || `Tide ${dateString} ${timeString}`, - description: params.description || `Created on ${dateString} at ${timeString}`, - flowType: params.flowType || "daily", - ...params, - }; - break; - case "startTideFlow": - // Use current context tide - if (!params.tideId) { - params.tideId = getCurrentContextTideId?.(); - } - params = { - intensity: params.intensity || "moderate", - duration: params.duration || 25, - initialEnergy: params.initialEnergy || "moderate", - workContext: params.workContext || "Quick flow session", - ...params, - }; - break; - case "addEnergyToTide": - // Use current context tide - if (!params.tideId) { - params.tideId = getCurrentContextTideId?.(); - } - params = { - energyLevel: params.energyLevel || "moderate", - context: params.context || `Energy added at ${timeString}`, - ...params, - }; - break; - case "linkTaskToTide": - // Use current context tide - if (!params.tideId) { - params.tideId = getCurrentContextTideId?.(); - } - params = { - taskUrl: params.taskUrl || `https://example.com/task-${Date.now()}`, - taskTitle: params.taskTitle || `Task created ${timeString}`, - taskType: params.taskType || "general", - ...params, - }; - break; - case "getTaskLinks": - case "getTideReport": - // Use current context tide - if (!params.tideId) { - params.tideId = getCurrentContextTideId?.(); - } - break; - case "getTideParticipants": - params = { - statusFilter: params.statusFilter || "active", - limit: params.limit || 10, - ...params, - }; - break; - } - - // Map agent commands to actual execution - if (["getInsights", "analyzeTides", "getRecommendations"].includes(toolSuggestion.toolId)) { - // For agent commands, send as a message instead - const commandMap: Record = { - getInsights: "get insights", - analyzeTides: "analyze my tides", - getRecommendations: "recommend actions", - }; - - const command = commandMap[toolSuggestion.toolId]; - if (command) { - sendMessage(command); - } - } else { - // Execute MCP tool - executeMCPTool(toolSuggestion.toolId, params); - } - }, [toolSuggestion, executeMCPTool, sendMessage, getCurrentContextTideId]); - - // Dismiss the suggestion - const dismissSuggestion = useCallback(() => { - setToolSuggestion(null); - setShowSuggestion(false); - - loggingService.info("ChatInput", "Tool suggestion dismissed"); - }, []); - - return { - // State - inputMessage, - toolSuggestion, - showSuggestion, - - // Actions - setInputMessage, - handleSendMessage, - acceptSuggestion, - dismissSuggestion, - }; -}; \ No newline at end of file diff --git a/apps/mobile/src/hooks/useChatMessaging.ts b/apps/mobile/src/hooks/useChatMessaging.ts new file mode 100644 index 0000000..d1ff23d --- /dev/null +++ b/apps/mobile/src/hooks/useChatMessaging.ts @@ -0,0 +1,49 @@ +import { useCallback, useState } from "react"; +import { useChat } from "../context/ChatContext"; +import { useTide } from "../context/TideContext"; +import { agentService } from "../services/agentService"; + +export function useChatMessaging() { + const chat = useChat(); + const tide = useTide(); + const [isLoading, setIsLoading] = useState(false); + + const buildPayload = useCallback(() => { + const message = chat.inputMessage?.trim(); + if (!message || !tide.currentTide?.id) return null; + return { + message, + tideId: tide.currentTide.id, + context: { + ...(chat.highlightedTool && { tide_tool: chat.highlightedTool }), + tide: tide.currentTide, + timeContext: { timestamp: new Date().toISOString(), timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }, + conversationHistory: tide.messages.slice(-5).map(msg => ({ role: msg.type === "user" ? "user" : "assistant", content: msg.content, timestamp: msg.timestamp })), + toolSuggestions: chat.toolSuggestions.map(s => s.title), + } + }; + }, [chat.inputMessage, chat.highlightedTool, chat.toolSuggestions, tide.currentTide, tide.messages]); + + const sendMessage = useCallback(async () => { + const payload = buildPayload(); + if (!payload) return; + + setIsLoading(true); + try { + const response = await agentService.sendMessage(payload.message, { tideId: payload.tideId, userPreferences: payload.context }); + tide.addMessage({ type: "user", content: payload.message, metadata: { toolName: payload.context.tide_tool } }); + tide.addMessage({ type: "assistant", content: response.content, metadata: { agentResponse: true, suggestedTools: response.suggestedTools } }); + chat.setInputMessage(""); + chat.setHighlightedTool(null); + chat.setToolbar(null); + chat.setToolSuggestions([]); + chat.setError(null); + } catch (error) { + chat.setError(error instanceof Error ? error.message : "Failed to send message"); + } finally { + setIsLoading(false); + } + }, [buildPayload, tide, chat]); + + return { sendMessage, isLoading, canSendMessage: !!chat.inputMessage?.trim() && !!tide.currentTide?.id && !isLoading }; +} diff --git a/apps/mobile/src/hooks/useContextTide.ts b/apps/mobile/src/hooks/useContextTide.ts deleted file mode 100644 index 0b7f9df..0000000 --- a/apps/mobile/src/hooks/useContextTide.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { useState, useCallback, useEffect } from 'react'; -import { useDailyTide } from './useDailyTide'; -import { mcpService } from '../services/mcpService'; -import { loggingService } from '../services/loggingService'; - -type TideContext = 'daily' | 'weekly' | 'monthly'; - -interface ContextTide { - id: string; - name: string; - context: TideContext; - created_at: string; - status: 'active'; -} - -interface UseContextTideReturn { - // Current state - currentContext: TideContext; - currentContextTide: ContextTide | null; - isToolExecuting: boolean; - contextSwitchingDisabled: boolean; - - // Context operations - switchContext: (newContext: TideContext) => Promise; - getCurrentContextTideId: () => string | null; - - // Tool execution state - setToolExecuting: (executing: boolean) => void; -} - -export const useContextTide = (): UseContextTideReturn => { - // State management - const [currentContext, setCurrentContext] = useState('daily'); - const [currentContextTide, setCurrentContextTide] = useState(null); - const [isToolExecuting, setIsToolExecuting] = useState(false); - - // Get daily tide (always exists) - const { dailyTide, isReady: dailyTideReady } = useDailyTide(); - - // Context switching disabled during tool execution - const contextSwitchingDisabled = isToolExecuting; - - // Get or create context tide - const getOrCreateContextTide = useCallback(async (context: TideContext): Promise => { - try { - loggingService.info('useContextTide', `Getting/creating ${context} tide`); - - let response; - switch (context) { - case 'daily': - // Daily tide always exists via useDailyTide - if (dailyTide) { - return { - id: dailyTide.id, - name: dailyTide.name, - context: 'daily', - created_at: dailyTide.created_at, - status: 'active' - }; - } - throw new Error('Daily tide not available'); - - case 'weekly': - response = await mcpService.callTool('tide_switch_context', { - context: 'weekly', - create_if_missing: true, - }); - break; - - case 'monthly': - response = await mcpService.callTool('tide_switch_context', { - context: 'monthly', - create_if_missing: true, - }); - break; - } - - if (response?.success && response?.tide) { - return { - id: response.tide.id, - name: response.tide.name, - context, - created_at: response.tide.created_at, - status: 'active' - }; - } - - throw new Error(`Failed to get/create ${context} tide`); - } catch (error) { - loggingService.error('useContextTide', `Failed to get/create ${context} tide`, { error }); - throw error; - } - }, [dailyTide]); - - // Switch context (disabled during tool execution) - const switchContext = useCallback(async (newContext: TideContext) => { - if (isToolExecuting) { - loggingService.warn('useContextTide', 'Context switching disabled during tool execution'); - return; - } - - const previousContext = currentContext; - const previousContextTide = currentContextTide; - - try { - loggingService.info('useContextTide', `Switching to ${newContext} context`); - - // Optimistic update - switch UI immediately - setCurrentContext(newContext); - - // Create optimistic tide object for immediate UI feedback - const optimisticTide: ContextTide = { - id: `temp-${newContext}-${Date.now()}`, - name: `${newContext.charAt(0).toUpperCase() + newContext.slice(1)} Tide`, - context: newContext, - created_at: new Date().toISOString(), - status: 'active' - }; - setCurrentContextTide(optimisticTide); - - // Get actual context tide from server - const contextTide = await getOrCreateContextTide(newContext); - - // Replace optimistic data with real data - setCurrentContextTide(contextTide); - - loggingService.info('useContextTide', `Successfully switched to ${newContext} context`, { - tideId: contextTide.id, - tideName: contextTide.name - }); - } catch (error) { - loggingService.error('useContextTide', `Failed to switch to ${newContext} context`, { error }); - - // Rollback to previous context on error - setCurrentContext(previousContext); - setCurrentContextTide(previousContextTide); - } - }, [isToolExecuting, getOrCreateContextTide, currentContext, currentContextTide]); - - // Get current context tide ID - const getCurrentContextTideId = useCallback((): string | null => { - return currentContextTide?.id || null; - }, [currentContextTide]); - - // Set tool execution state - const setToolExecuting = useCallback((executing: boolean) => { - setIsToolExecuting(executing); - loggingService.info('useContextTide', `Tool execution state: ${executing ? 'started' : 'stopped'}`); - }, []); - - // Initialize with daily context on mount - useEffect(() => { - if (dailyTideReady && dailyTide && !currentContextTide) { - setCurrentContext('daily'); - setCurrentContextTide({ - id: dailyTide.id, - name: dailyTide.name, - context: 'daily', - created_at: dailyTide.created_at, - status: 'active' - }); - } - }, [dailyTideReady, dailyTide, currentContextTide]); - - // Reset to daily context on app restart (useEffect runs once) - useEffect(() => { - loggingService.info('useContextTide', 'App started - defaulting to daily context'); - }, []); - - return { - // Current state - currentContext, - currentContextTide, - isToolExecuting, - contextSwitchingDisabled, - - // Context operations - switchContext, - getCurrentContextTideId, - - // Tool execution state - setToolExecuting, - }; -}; \ No newline at end of file diff --git a/apps/mobile/src/hooks/useDailyTide.ts b/apps/mobile/src/hooks/useDailyTide.ts deleted file mode 100644 index ae1216d..0000000 --- a/apps/mobile/src/hooks/useDailyTide.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { useMCP } from "../context/MCPContext"; -import { loggingService } from "../services/loggingService"; -import type { Tide } from "../types"; - -interface UseDailyTideReturn { - // State - dailyTide: Tide | null; - isReady: boolean; - loading: boolean; - error: string | null; - wasCreatedToday: boolean; - - // Actions - refreshDailyTide: () => Promise; - renameDailyTide: (newName: string) => Promise; -} - -/** - * Hook for managing automatic daily tides - * Ensures a daily tide exists for the current day and provides - * methods to interact with it - */ -export const useDailyTide = (): UseDailyTideReturn => { - const { isConnected, getOrCreateDailyTide } = useMCP(); - const [dailyTide, setDailyTide] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [wasCreatedToday, setWasCreatedToday] = useState(false); - const [isReady, setIsReady] = useState(false); - - // Initialize or get daily tide - const initializeDailyTide = useCallback(async () => { - if (!isConnected) { - loggingService.debug( - "useDailyTide", - "Not connected, skipping initialization" - ); - return; - } - - try { - setLoading(true); - setError(null); - - loggingService.info("useDailyTide", "Getting or creating daily tide"); - - // Get user's timezone - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - - loggingService.info("useDailyTide", "Getting or creating daily tide via MCPContext", { - timezone, - currentDate: new Date().toISOString(), - localDate: new Date().toLocaleDateString(), - localTime: new Date().toLocaleTimeString(), - }); - - const result = await getOrCreateDailyTide(timezone); - - if (result.success && result.tide) { - setDailyTide(result.tide); - setWasCreatedToday(result.created || false); - setIsReady(true); - - loggingService.info( - "useDailyTide", - result.created - ? "Created new daily tide" - : "Retrieved existing daily tide", - { tideId: result.tide.id, tideName: result.tide.name } - ); - } else { - throw new Error(result.error || "Failed to get daily tide"); - } - } catch (err) { - const errorMessage = - err instanceof Error ? err.message : "Failed to initialize daily tide"; - setError(errorMessage); - loggingService.error("useDailyTide", "Failed to initialize daily tide", { - error: errorMessage, - }); - } finally { - setLoading(false); - } - }, [isConnected, getOrCreateDailyTide]); - - // Refresh daily tide (useful for pull-to-refresh) - const refreshDailyTide = useCallback(async () => { - await initializeDailyTide(); - }, [initializeDailyTide]); - - // Rename daily tide (retroactive naming) - const renameDailyTide = useCallback( - async (newName: string) => { - if (!dailyTide) { - loggingService.warn("useDailyTide", "No daily tide to rename"); - return; - } - - try { - loggingService.info("useDailyTide", "Renaming daily tide", { - tideId: dailyTide.id, - oldName: dailyTide.name, - newName, - }); - - // For now, we'll update locally - // TODO: When server implements tide_update_name, call it here - setDailyTide((prev) => (prev ? { ...prev, name: newName } : null)); - - loggingService.info("useDailyTide", "Daily tide renamed successfully"); - } catch (err) { - const errorMessage = - err instanceof Error ? err.message : "Failed to rename tide"; - loggingService.error("useDailyTide", "Failed to rename daily tide", { - error: errorMessage, - }); - throw err; - } - }, - [dailyTide] - ); - - // Initialize on mount and when connection status changes - useEffect(() => { - if (isConnected && !dailyTide) { - initializeDailyTide(); - } - }, [isConnected, dailyTide, initializeDailyTide]); - - return { - // State - dailyTide, - isReady, - loading, - error, - wasCreatedToday, - - // Actions - refreshDailyTide, - renameDailyTide, - }; -}; diff --git a/apps/mobile/src/hooks/useEnergyData.ts b/apps/mobile/src/hooks/useEnergyData.ts deleted file mode 100644 index 3cc40a1..0000000 --- a/apps/mobile/src/hooks/useEnergyData.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { useMCP } from "../context/MCPContext"; -import { - EnergyChartData, - EnergyDataPoint, - energyLevelToNumber, -} from "../types/charts"; -import { loggingService } from "../services/loggingService"; - -export const useEnergyData = (tideId?: string) => { - const [chartData, setChartData] = useState({ - points: [], - loading: false, - error: null, - }); - - const { getTideReport, isConnected } = useMCP(); - - const fetchEnergyData = useCallback(async () => { - if (!isConnected) { - setChartData((prev) => ({ - ...prev, - error: "Not connected to MCP server", - })); - return; - } - - setChartData((prev) => ({ ...prev, loading: true, error: null })); - - try { - // Get tide report which includes energy progression - const reportResult = await getTideReport(tideId || "", "json"); - - if (reportResult.success && reportResult.report) { - const report = reportResult.report; - - // Convert energy progression to chart points - const points: EnergyDataPoint[] = []; - - if ( - report.energy_progression && - Array.isArray(report.energy_progression) - ) { - report.energy_progression.forEach( - (energyLevel: any, index: number) => { - // Create timestamp for each point (spread over recent time) - const now = Date.now(); - const timeOffset = - (report.energy_progression.length - 1 - index) * - 2 * - 60 * - 60 * - 1000; // 2 hours apart - const timestamp = now - timeOffset; - - points.push({ - date: new Date(timestamp), - value: energyLevelToNumber(energyLevel), - }); - } - ); - } - - // If no energy progression in report, use sample data for demonstration - if (points.length === 0) { - // Use sample data for demonstration - const samplePoints: EnergyDataPoint[] = [ - { date: new Date(Date.now() - 6 * 60 * 60 * 1000), value: 7 }, // 6 hours ago - { date: new Date(Date.now() - 4 * 60 * 60 * 1000), value: 8 }, // 4 hours ago - { date: new Date(Date.now() - 2 * 60 * 60 * 1000), value: 6 }, // 2 hours ago - { date: new Date(Date.now() - 1 * 60 * 60 * 1000), value: 9 }, // 1 hour ago - { date: new Date(), value: 8 }, // now - ]; - points.push(...samplePoints); - } - - setChartData({ - points: points.sort((a, b) => a.date.getTime() - b.date.getTime()), // Sort by timestamp - loading: false, - error: null, - }); - } else { - throw new Error("Failed to fetch energy data"); - } - } catch (error) { - loggingService.error("EnergyData", "Error fetching energy data", { - error, - }); - setChartData({ - points: [], - loading: false, - error: error instanceof Error ? error.message : "Unknown error", - }); - } - }, [getTideReport, isConnected, tideId]); - - // Fetch data when component mounts or dependencies change - useEffect(() => { - fetchEnergyData(); - }, [fetchEnergyData]); - - return { - ...chartData, - refetch: fetchEnergyData, - }; -}; diff --git a/apps/mobile/src/hooks/useHierarchicalContext.ts b/apps/mobile/src/hooks/useHierarchicalContext.ts deleted file mode 100644 index e58168f..0000000 --- a/apps/mobile/src/hooks/useHierarchicalContext.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { useMCP } from "../context/MCPContext"; -import { loggingService } from "../services/loggingService"; - -type ContextType = "daily" | "weekly" | "monthly" | "project"; - -interface ContextInfo { - context: string; - tide_id?: string; - tide_name?: string; - flow_count: number; - total_minutes: number; - available: boolean; -} - -interface UseHierarchicalContextReturn { - // State - currentContext: ContextType; - contexts: ContextInfo[]; - loading: boolean; - error: string | null; - summary: { - total_flow_sessions: number; - total_minutes: number; - }; - - // Actions - switchContext: (context: ContextType) => Promise; - refreshContexts: () => Promise; - startHierarchicalFlow: ( - intensity?: 'gentle' | 'moderate' | 'strong', - duration?: number, - workContext?: string - ) => Promise; -} - -/** - * Hook for managing hierarchical tide contexts - * Provides context switching, summary data, and hierarchical flow management - */ -export const useHierarchicalContext = ( - initialContext: ContextType = "daily" -): UseHierarchicalContextReturn => { - const { - switchTideContext, - // listTideContexts, // Currently unused - getTodaysSummary, - startHierarchicalFlow: mcpStartHierarchicalFlow, - isConnected, - } = useMCP(); - - const [currentContext, setCurrentContext] = useState(initialContext); - const [contexts, setContexts] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [summary, setSummary] = useState({ - total_flow_sessions: 0, - total_minutes: 0, - }); - - const refreshContexts = useCallback(async () => { - if (!isConnected) { - loggingService.debug("useHierarchicalContext", "Not connected, skipping refresh"); - return; - } - - setLoading(true); - setError(null); - - try { - loggingService.info("useHierarchicalContext", "Refreshing hierarchical contexts"); - - // Get today's summary with all context information - const result = await getTodaysSummary(); - - if (result.success && result.contexts) { - setContexts(result.contexts); - setSummary({ - total_flow_sessions: result.total_flow_sessions || 0, - total_minutes: result.total_minutes || 0, - }); - - loggingService.info("useHierarchicalContext", "Contexts refreshed", { - contextsCount: result.contexts.length, - totalSessions: result.total_flow_sessions, - totalMinutes: result.total_minutes, - }); - } else { - throw new Error(result.error || "Failed to load contexts"); - } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Failed to refresh contexts"; - setError(errorMessage); - loggingService.error("useHierarchicalContext", "Failed to refresh contexts", { - error: errorMessage, - }); - } finally { - setLoading(false); - } - }, [getTodaysSummary, isConnected]); - - const switchContext = useCallback( - async (contextType: ContextType) => { - if (contextType === currentContext || loading) return; - - setLoading(true); - setError(null); - - try { - loggingService.info("useHierarchicalContext", "Switching context", { - from: currentContext, - to: contextType, - }); - - const result = await switchTideContext(contextType); - - if (result.success) { - setCurrentContext(contextType); - - // Refresh contexts to get updated data - await refreshContexts(); - - loggingService.info("useHierarchicalContext", "Context switched successfully", { - contextType, - tideId: result.tide?.id, - created: result.created, - }); - } else { - throw new Error(result.error || "Failed to switch context"); - } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Failed to switch context"; - setError(errorMessage); - loggingService.error("useHierarchicalContext", "Failed to switch context", { - error: errorMessage, - contextType, - }); - } finally { - setLoading(false); - } - }, - [currentContext, loading, switchTideContext, refreshContexts] - ); - - const startHierarchicalFlow = useCallback( - async ( - intensity: 'gentle' | 'moderate' | 'strong' = 'moderate', - duration: number = 25, - workContext: string = 'General work' - ) => { - setLoading(true); - setError(null); - - try { - loggingService.info("useHierarchicalContext", "Starting hierarchical flow", { - intensity, - duration, - workContext, - }); - - const result = await mcpStartHierarchicalFlow( - intensity, - duration, - 'medium', // Default energy level - workContext - ); - - if (result.success) { - // Refresh contexts to show updated data - await refreshContexts(); - - loggingService.info("useHierarchicalContext", "Hierarchical flow started", { - sessionId: result.session_id, - contextsCount: result.contexts?.length || 0, - }); - } else { - throw new Error(result.error || "Failed to start hierarchical flow"); - } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Failed to start hierarchical flow"; - setError(errorMessage); - loggingService.error("useHierarchicalContext", "Failed to start hierarchical flow", { - error: errorMessage, - intensity, - duration, - workContext, - }); - throw err; - } finally { - setLoading(false); - } - }, - [mcpStartHierarchicalFlow, refreshContexts] - ); - - // Initialize and refresh on connection - useEffect(() => { - if (isConnected) { - refreshContexts(); - } - }, [isConnected, refreshContexts]); - - return { - // State - currentContext, - contexts, - loading, - error, - summary, - - // Actions - switchContext, - refreshContexts, - startHierarchicalFlow, - }; -}; \ No newline at end of file diff --git a/apps/mobile/src/hooks/useLocationData.ts b/apps/mobile/src/hooks/useLocationData.ts deleted file mode 100644 index 8d4447e..0000000 --- a/apps/mobile/src/hooks/useLocationData.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { useState, useEffect } from "react"; -import * as SunCalc from "suncalc"; -import Geolocation from "@react-native-community/geolocation"; -import { LocationInfo } from "../types/charts"; -import { loggingService } from "../services/loggingService"; - -export const useLocationData = () => { - const [locationInfo, setLocationInfo] = useState({ - sunrise: undefined, - sunset: undefined, - latitude: undefined, - longitude: undefined, - }); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchLocationAndSunTimes = async () => { - setLoading(true); - setError(null); - - try { - // Get current position - const position = await new Promise((resolve, reject) => { - Geolocation.getCurrentPosition( - resolve, - reject, - { - enableHighAccuracy: true, - timeout: 15000, - maximumAge: 300000 // 5 minutes - } - ); - }); - - const { latitude, longitude } = position.coords; - const now = new Date(); - - // Calculate sun times - const sunTimes = SunCalc.getTimes(now, latitude, longitude); - - setLocationInfo({ - sunrise: sunTimes.sunrise, - sunset: sunTimes.sunset, - latitude, - longitude, - }); - } catch (err) { - loggingService.error("LocationData", "Error fetching location", { error: err }); - setError(err instanceof Error ? err.message : "Location error"); - - // Fallback to default location (NYC) for demo purposes - const now = new Date(); - const sunTimes = SunCalc.getTimes(now, 40.7128, -74.0060); // NYC coordinates - - setLocationInfo({ - sunrise: sunTimes.sunrise, - sunset: sunTimes.sunset, - latitude: 40.7128, - longitude: -74.0060, - }); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchLocationAndSunTimes(); - }, []); - - return { - locationInfo, - loading, - error, - refetch: fetchLocationAndSunTimes, - }; -}; \ No newline at end of file diff --git a/apps/mobile/src/hooks/useToolMenu.ts b/apps/mobile/src/hooks/useToolMenu.ts deleted file mode 100644 index 45c2971..0000000 --- a/apps/mobile/src/hooks/useToolMenu.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { useState, useCallback, useRef } from "react"; -import { Animated } from "react-native"; -// import type { Tide } from "../types"; // Unused import -import { loggingService } from "../services/loggingService"; - -interface UseToolMenuReturn { - // State - showToolMenu: boolean; - toolButtonActive: boolean; - - // Animation refs - rotationAnim: Animated.Value; - menuHeightAnim: Animated.Value; - - // Actions - toggleToolMenu: () => void; - generateDefaultParams: (toolName: string) => Record | null; - getToolAvailability: (toolName: string) => { available: boolean; reason: string }; - handleToolSelect: (toolName: string, customParameters?: Record) => Promise; -} - -interface UseToolMenuProps { - executeMCPTool: (toolName: string, params: Record) => Promise; - sendMessage: (message: string) => Promise; - getCurrentContextTideId?: () => string | null; - setToolExecuting?: (executing: boolean) => void; - injectTemplate?: (template: string) => void; -} - -export const useToolMenu = ({ - executeMCPTool, - sendMessage: _sendMessage, - getCurrentContextTideId, - setToolExecuting, - injectTemplate, -}: UseToolMenuProps): UseToolMenuReturn => { - // State management - const [showToolMenu, setShowToolMenu] = useState(false); - const [toolButtonActive, setToolButtonActive] = useState(false); - - // Animation refs - const rotationAnim = useRef(new Animated.Value(0)).current; - const menuHeightAnim = useRef(new Animated.Value(0)).current; - - // Toggle tool menu with synchronized animations - const toggleToolMenu = useCallback(() => { - const isOpening = !showToolMenu; - - if (isOpening) { - setShowToolMenu(true); - setToolButtonActive(true); // Change color immediately - - // Synchronize button rotation and menu expansion - Animated.parallel([ - Animated.timing(rotationAnim, { - toValue: 1, - duration: 200, - useNativeDriver: true, - }), - Animated.timing(menuHeightAnim, { - toValue: 1, - duration: 200, - useNativeDriver: false, - }), - ]).start(); - } else { - setToolButtonActive(false); // Change color immediately - - // Synchronize button rotation and menu collapse - Animated.parallel([ - Animated.timing(rotationAnim, { - toValue: 0, - duration: 200, - useNativeDriver: true, - }), - Animated.timing(menuHeightAnim, { - toValue: 0, - duration: 200, - useNativeDriver: false, - }), - ]).start(() => { - setShowToolMenu(false); - }); - } - }, [showToolMenu, rotationAnim, menuHeightAnim]); - - // Context-aware parameter generation - const generateDefaultParams = useCallback( - (toolName: string) => { - const now = new Date(); - const dateString = now.toLocaleDateString(); - const timeString = now.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - }); - const currentHour = now.getHours(); - const timeBasedContext = - currentHour < 12 ? 'morning focus' : - currentHour < 17 ? 'afternoon productivity' : - 'evening deep work'; - - // Get current context tide ID for all tools - const contextTideId = getCurrentContextTideId?.(); - - switch (toolName) { - case "createTide": - return { - name: `Tide ${dateString} ${timeString}`, - description: `Created on ${dateString} at ${timeString}`, - flowType: "daily", - }; - case "startTideFlow": - case "tide_smart_flow": - return { - tideId: contextTideId, - intensity: "moderate", - duration: 25, - initialEnergy: "moderate", - workContext: timeBasedContext, - timestamp: now.toISOString(), - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }; - case "addEnergyToTide": - case "tide_add_energy": - return { - tideId: contextTideId, - energyLevel: "moderate", - context: `${timeBasedContext} - energy logged at ${timeString}`, - timestamp: now.toISOString(), - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }; - case "linkTaskToTide": - case "tide_link_task": - return { - tideId: contextTideId, - taskUrl: `https://example.com/task-${Date.now()}`, - taskTitle: `${timeBasedContext} - task created ${timeString}`, - taskType: "context_task", - timestamp: now.toISOString(), - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }; - case "getTaskLinks": - case "tide_list_task_links": - return { - tideId: contextTideId, - timestamp: now.toISOString(), - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }; - case "getTideReport": - case "tide_get_report": - return { - tideId: contextTideId, - format: "summary", - include_energy_analysis: true, - include_time_patterns: true, - timestamp: now.toISOString(), - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }; - case "getTideParticipants": - case "tides_get_participants": - return { - statusFilter: "active", - limit: 10, - timestamp: now.toISOString(), - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }; - default: - return { - contextTideId, - timestamp: now.toISOString(), - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }; - } - }, - [getCurrentContextTideId] - ); - - // Tool availability checking - All tools always available since hierarchical context tides always exist - const getToolAvailability = useCallback( - (_toolName: string) => { - // All tools available since hierarchical tides (daily/weekly/monthly) always exist - return { available: true, reason: "" }; - }, - [] // No dependencies - tools always available - ); - - // Generate tool parameter template for intellisense-style input - const generateToolTemplate = useCallback((toolName: string): string => { - switch (toolName) { - case 'tide_smart_flow': - return '/flow [what: ___] [energy: ___] [duration: ___] [type: ___]'; - case 'tide_add_energy': - return '/energy [level: ___] [context: ___]'; - case 'tide_link_task': - return '/link [task: ___] [url: ___] [type: ___]'; - case 'tide_get_report': - return '/report [period: ___] [format: ___]'; - default: - return `/${toolName} [params: ___]`; - } - }, []); - - // Handle tool selection with template injection - const handleToolSelect = useCallback( - async (toolName: string, customParameters?: Record) => { - // All tools always available - no availability checking needed - toggleToolMenu(); // Close menu first - - // For tide_smart_flow and other parameterized tools, inject template instead of executing - if (toolName === 'tide_smart_flow' || - toolName === 'tide_add_energy' || - toolName === 'tide_link_task' || - toolName === 'tide_get_report') { - - const template = generateToolTemplate(toolName); - - loggingService.info("ToolMenu", "Injecting tool parameter template", { - toolName, - template, - }); - - // Inject template into chat input via callback (to be passed from parent) - if (injectTemplate) { - injectTemplate(template); - return; - } - } - - // Set tool execution state (disables context switching) - setToolExecuting?.(true); - - try { - // Generate context-aware parameters for all tools - const contextTideId = getCurrentContextTideId?.(); - - // Use custom parameters or generate intelligent context-aware defaults - const params = customParameters || generateDefaultParams(toolName); - - await executeMCPTool(toolName, params); - - loggingService.info("ToolMenu", "Context-aware MCP tool executed", { - toolName, - contextTideId, - parameters: params, - usedDefaults: !customParameters, - }); - } catch (error) { - loggingService.error( - "ToolMenu", - "Failed to execute context-aware tool", - { error, toolName, parameters: customParameters } - ); - } finally { - // Re-enable context switching - setToolExecuting?.(false); - } - }, - [ - toggleToolMenu, - setToolExecuting, - getCurrentContextTideId, - executeMCPTool, - generateDefaultParams, - injectTemplate, - generateToolTemplate, - ] - ); - - return { - // State - showToolMenu, - toolButtonActive, - - // Animation refs - rotationAnim, - menuHeightAnim, - - // Actions - toggleToolMenu, - generateDefaultParams, - getToolAvailability, - handleToolSelect, - }; -}; \ No newline at end of file diff --git a/apps/mobile/src/navigation/MainNavigator.tsx b/apps/mobile/src/navigation/MainNavigator.tsx index 834349c..7e45dea 100644 --- a/apps/mobile/src/navigation/MainNavigator.tsx +++ b/apps/mobile/src/navigation/MainNavigator.tsx @@ -3,82 +3,89 @@ import React from "react"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { TouchableOpacity, View } from "react-native"; -import { AlignLeft, ChartLine } from "lucide-react-native"; +import { Menu, FileText, Plus, Settings as SettingsIcon } from "lucide-react-native"; import Home from "../screens/Main/Home"; import Settings from "../screens/Main/Settings"; import TideDetails from "../screens/Main/TideDetails"; +import LoggingMessages from "../screens/Main/LoggingMessages"; import { MainStackParamList, Routes, NavigationOptions } from "./types"; import { colors } from "../design-system/tokens"; -import { useTimeContext } from "../context/TimeContext"; -import { getContextDateRangeWithOffset } from "../utils/contextUtils"; -import { getHumanisticTimeContext } from "../utils/timeContextHelpers"; -import { Text } from "../design-system"; +import Chat from "../screens/Main/Chat"; +import { useTide } from "../context/TideContext"; const Stack = createNativeStackNavigator(); const SettingsHeaderButton = React.memo(({ navigation }: any) => ( navigation.navigate(Routes.main.settings)} - style={{ padding: 8 }} + style={{ + height: 44, + width: 44, + alignItems: "flex-start", + justifyContent: "center", + paddingTop: 18, + }} > - + )); -const TidesHeaderButton = React.memo(({ navigation }: any) => ( - navigation.navigate(Routes.main.settings)} - style={{ padding: 8 }} - > - - -)); -1; - -const HomeScreenTitle: React.FC<{ route: any }> = ({ route }) => { - const { currentContext, dateOffset } = useTimeContext(); - - const title = route.params?.tideId - ? `${route.params?.tideName || "Home"} (${route.params.tideId})` - : getContextDateRangeWithOffset(currentContext, dateOffset); - - const timeContext = getHumanisticTimeContext(currentContext, dateOffset); - const isCurrentTime = - timeContext === "Today" || - timeContext === "This week" || - timeContext === "This month"; - +const ChatHeaderButtons = React.memo(({ navigation }: any) => { + const { createNewTide } = useTide(); + + const handleCreateNewTide = async () => { + const tideName = `New Tide ${new Date().toLocaleTimeString()}`; + await createNewTide(tideName, "Created from chat screen"); + }; + return ( - - + navigation.navigate(Routes.main.settings)} + style={{ + height: 44, + width: 44, + alignItems: "center", + justifyContent: "center", + marginRight: 8, + }} + > + + + + navigation.navigate(Routes.main.loggingMessages)} + style={{ + height: 44, + width: 44, + alignItems: "center", + justifyContent: "center", + marginRight: 8, + }} > - {title} - - {!isCurrentTime && ( - - {timeContext} - - )} + + + + + + ); -}; +}); -const getHomeScreenOptions = ({ navigation, route }: any) => ({ - headerTitle: () => , - headerTintColor: colors.primary[900], +const getHomeScreenOptions = ({ navigation }: any) => ({ headerShown: true, headerShadowVisible: false, - headerRight: () => , + headerTitle: "", headerLeft: () => , headerTransparent: true, headerStyle: { @@ -89,7 +96,7 @@ const getHomeScreenOptions = ({ navigation, route }: any) => ({ export default function MainNavigator() { return ( + ({ + headerShown: true, + navigationBarHidden: true, + headerTitle: "Demo Chat", + sheetCornerRadius: 16, + sheetGrabberVisible: false, + gestureEnabled: false, + sheetLargestUndimmedDetentIndex: "last", + animationDuration: 200, + headerRight: () => , + })} + /> + + >(); -} - -export function useRootRoute() { - return useRoute>(); -} - -// Main stack navigation hooks -export function useMainNavigation() { - return useNavigation>(); -} - -export function useMainRoute() { - return useRoute>(); -} - -// Auth stack navigation hooks -export function useAuthNavigation() { - return useNavigation>(); -} - -export function useAuthRoute() { - return useRoute>(); -} - -// Typed navigation actions -export function useTypedNavigation() { - const rootNavigation = useRootNavigation(); - const mainNavigation = useMainNavigation(); - const authNavigation = useAuthNavigation(); - - return { - // Root level navigation - toAuth: () => (rootNavigation as any).navigate(Routes.root.auth), - toMain: () => (rootNavigation as any).navigate(Routes.root.main), - - // Auth navigation - toInitial: () => authNavigation.navigate(Routes.auth.initial), - toCreateAccount: () => authNavigation.navigate(Routes.auth.createAccount), - toAuthLoading: (params?: { email?: string }) => - authNavigation.navigate({ name: Routes.auth.authLoading, params: params || {} }), - - // Main navigation - toHome: () => (mainNavigation as any).navigate(Routes.main.home), - toServer: () => mainNavigation.navigate(Routes.main.server), - toMcp: () => mainNavigation.navigate(Routes.main.mcp), - toSettings: () => mainNavigation.navigate(Routes.main.settings), - toTidesList: () => mainNavigation.navigate(Routes.main.tidesList), - toTide: (params: { tideId: string; tideName?: string }) => - mainNavigation.navigate(Routes.main.tide, params), - toTideDetails: (params: { tideId: string; mode?: 'view' | 'edit' }) => - mainNavigation.navigate(Routes.main.tideDetails, params), - toFlowSession: (params: { tideId: string; sessionId?: string }) => - mainNavigation.navigate(Routes.main.flowSession, params), - toProfile: () => mainNavigation.navigate(Routes.main.profile), - toAbout: () => mainNavigation.navigate(Routes.main.about), - - // Common actions - goBack: () => { - if (rootNavigation.canGoBack()) { - rootNavigation.goBack(); - } - }, - - reset: (routeName: keyof RootStackParamList) => { - rootNavigation.reset({ - index: 0, - routes: [{ name: routeName }], - }); - }, - }; -} - -// Screen parameter hooks for easy access to route params -export function useTideParams() { - const route = useMainRoute<'Tide'>(); - return route.params; -} - -export function useTideDetailsParams() { - const route = useMainRoute<'TideDetails'>(); - return route.params; -} - -export function useFlowSessionParams() { - const route = useMainRoute<'FlowSession'>(); - return route.params; -} - -export function useAuthLoadingParams() { - const route = useAuthRoute<'AuthLoading'>(); - return route.params; -} - -// Navigation state helpers -export function useNavigationState() { - const rootNavigation = useRootNavigation(); - - return { - currentRoute: rootNavigation.getState()?.routes[rootNavigation.getState()?.index || 0]?.name, - canGoBack: rootNavigation.canGoBack(), - navigationState: rootNavigation.getState(), - }; -} - -// Focus and blur event hooks -export function useScreenFocus(callback: () => void) { - const navigation = useRootNavigation(); - - React.useEffect(() => { - const unsubscribe = navigation.addListener('focus', callback); - return unsubscribe; - }, [navigation, callback]); -} - -export function useScreenBlur(callback: () => void) { - const navigation = useRootNavigation(); - - React.useEffect(() => { - const unsubscribe = navigation.addListener('blur', callback); - return unsubscribe; - }, [navigation, callback]); -} - -// Screen lifecycle hooks -export function useScreenLifecycle( - onFocus?: () => void, - onBlur?: () => void -) { - const navigation = useRootNavigation(); - - React.useEffect(() => { - const unsubscribeFocus = onFocus - ? navigation.addListener('focus', onFocus) - : undefined; - - const unsubscribeBlur = onBlur - ? navigation.addListener('blur', onBlur) - : undefined; - - return () => { - unsubscribeFocus?.(); - unsubscribeBlur?.(); - }; - }, [navigation, onFocus, onBlur]); -} - -// Safe navigation hook that checks if routes exist -export function useSafeNavigation() { - const navigation = useTypedNavigation(); - - return { - ...navigation, - - safeNavigate: (routeName: string, params?: any) => { - try { - // Type assertion since we're doing runtime checking - (navigation as any).navigate(routeName, params); - } catch (error) { - console.warn(`Failed to navigate to ${routeName}:`, error); - } - }, - }; -} \ No newline at end of file diff --git a/apps/mobile/src/navigation/types.ts b/apps/mobile/src/navigation/types.ts index 6862736..0fde7c4 100644 --- a/apps/mobile/src/navigation/types.ts +++ b/apps/mobile/src/navigation/types.ts @@ -31,6 +31,7 @@ export type MainStackParamList = { tideId: string; mode?: 'view' | 'edit'; }; + LoggingMessages: undefined; Profile: undefined; About: undefined; }; @@ -95,6 +96,7 @@ export const ScreenNames = { TIDE: 'Tide' as const, TIDE_DETAILS: 'TideDetails' as const, FLOW_SESSION: 'FlowSession' as const, + LOGGING_MESSAGES: 'LoggingMessages' as const, PROFILE: 'Profile' as const, ABOUT: 'About' as const, } as const; @@ -117,6 +119,7 @@ export const Routes = { tide: ScreenNames.TIDE, tideDetails: ScreenNames.TIDE_DETAILS, flowSession: ScreenNames.FLOW_SESSION, + loggingMessages: ScreenNames.LOGGING_MESSAGES, profile: ScreenNames.PROFILE, about: ScreenNames.ABOUT, }, diff --git a/apps/mobile/src/screens/Main/Chat.tsx b/apps/mobile/src/screens/Main/Chat.tsx new file mode 100644 index 0000000..4f5be73 --- /dev/null +++ b/apps/mobile/src/screens/Main/Chat.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { View, ScrollView } from "react-native"; +import { colors, Text } from "../../design-system"; +import { ChatInput } from "../../components/chat/ChatInput"; +import { ChatToolbar } from "../../components/chat/ChatToolbar"; +import { useChat } from "../../context/ChatContext"; +import { useTide } from "../../context/TideContext"; +import type { DetectedToolSuggestion } from "../../utils/toolDetection"; +import type { Message } from "../../context/tideTypes"; + +export default function Chat() { + const { + setInputMessage, + setHighlightedTool, + setToolSuggestions, + setToolbar, + } = useChat(); + + const { messages } = useTide(); + + const handleToolSelect = (suggestion: DetectedToolSuggestion) => { + setInputMessage(suggestion.title); + setHighlightedTool(suggestion.title); + setToolSuggestions([]); + setToolbar("instructions"); + }; + + const renderMessage = (message: Message) => { + const isUser = message.type === "user"; + return ( + + + {message.content} + + + ); + }; + + return ( + + {/* + + */} + {messages.map(renderMessage)} + + + + ); +} diff --git a/apps/mobile/src/screens/Main/Home.tsx b/apps/mobile/src/screens/Main/Home.tsx index c85a5e5..f75b85a 100644 --- a/apps/mobile/src/screens/Main/Home.tsx +++ b/apps/mobile/src/screens/Main/Home.tsx @@ -1,537 +1,70 @@ -import React, { useState, useCallback, useEffect, useRef } from "react"; -import { - StyleSheet, - ScrollView, - View, - TouchableOpacity, - useWindowDimensions, - Alert, - Clipboard, - ImageBackground, -} from "react-native"; +import React, { useCallback, useEffect } from "react"; +import { StyleSheet, useWindowDimensions, ImageBackground } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useMCP } from "../../context/MCPContext"; -import { useChat } from "../../context/ChatContext"; -import { loggingService } from "../../services/loggingService"; -import { colors, spacing } from "../../design-system/tokens"; -import { useToolMenu } from "../../hooks/useToolMenu"; -import { useChatInput } from "../../hooks/useChatInput"; -import { useContextTide } from "../../hooks/useContextTide"; -import { useTimeContext } from "../../context/TimeContext"; -import { ChatMessages } from "../../components/chat/ChatMessages"; -import { ChatInput } from "../../components/chat/ChatInput"; -import { ToolMenu } from "../../components/tools/ToolMenu"; -// import { EnergyChart } from "../../components/tides/EnergyChart"; - -import { - createAgentContext, - executeAgentCommand, -} from "../../utils/agentCommandUtils"; -import EnergyChart from "../../components/EnergyChart"; -import { getChartData, numberToEnergyLevel } from "../../components/data/data"; -import { ContextToggle } from "../../components/ContextToggle"; -import { getSimpleTimeContext } from "../../utils/timeContextHelpers"; -import { Text } from "../../design-system"; +import { useFocusEffect } from "@react-navigation/native"; +import { colors } from "../../design-system/tokens"; +import { NewEnergyChart } from "../../components/NewEnergyChart"; +import { getTimeContextChartData } from "../../components/demo/data"; +import { TimeDisplayToggle } from "../../components/TimeDisplayToggle"; +import { HomeScreenProps, Routes } from "../../navigation/types"; +import ChartHeader from "../../components/ChartHeader"; import { - ChevronLeft, - ChevronRight, - Timer, - ChevronUp, - ChevronDown, -} from "lucide-react-native"; + useChartDisplayContext, + ChartDisplayContextProvider, +} from "../../context/ChartDisplayContext"; + +// HomeContent: Main chart display with intentional navigation redirect +// - timeRange: Current selected range from ChartDisplayContext ("1day", "3day", etc.) +// - Triple navigation calls: Hacky but stable feature for chat redirection +const HomeContent: React.FC<{ navigation: HomeScreenProps["navigation"] }> = ({ + navigation, +}) => { + // Triple navigation redirect (intentional hack) + useEffect(() => navigation.navigate(Routes.main.chat, {}), [navigation]); + useFocusEffect( + useCallback(() => navigation.navigate(Routes.main.chat, {}), [navigation]) + ); + useFocusEffect(() => navigation.navigate(Routes.main.chat, {})); -export default function Home() { const insets = useSafeAreaInsets(); - const { getCurrentServerUrl, isConnected } = useMCP(); - const { - messages, - isLoading, - // pendingToolCalls, - sendMessage, - executeMCPTool, - sendAgentMessage, - } = useChat(); - - // ✅ REQUIREMENT 1: Defined size of chart and canvas - const CHART_HEIGHT = 44; // Chart height in pixels - const CHART_MARGIN = 20; // Chart margin for axes space - const { width } = useWindowDimensions(); - const CHART_WIDTH = width; // Chart width from screen dimensions minus 52px - - const [_agentInitialized, setAgentInitialized] = useState(false); - const [_isChatInputFocused, setIsChatInputFocused] = useState(false); - const [templateToInject, setTemplateToInject] = useState(""); - - // Context tide management - handles daily/weekly/monthly switching - const { getCurrentContextTideId, setToolExecuting, currentContextTide } = - useContextTide(); - - // Time navigation for chart history - const { - navigateBackward, - navigateForward, - dateOffset, - currentContext, - isAtPresent, - } = useTimeContext(); - - // Get last energy level with formatted display - const getLastEnergyDisplay = useCallback(() => { - const chartData = getChartData(); - if (chartData.length === 0) return "No data"; - - // Sort by timestamp to get the most recent - const sortedData = chartData.sort((a, b) => b.x - a.x); - const lastPoint = sortedData[0]; - - // Convert to string descriptor and number - const energyNumber = Math.round(lastPoint.y); - const energyLabel = numberToEnergyLevel(energyNumber); - - // Capitalize first letter - const capitalizedLabel = - energyLabel.charAt(0).toUpperCase() + energyLabel.slice(1); - - return `${capitalizedLabel} (${energyNumber})`; - }, []); - - // Get time and relative time since last update - const getLastUpdatedDisplay = useCallback(() => { - const chartData = getChartData(); - if (chartData.length === 0) return "No updates"; - - // Sort by timestamp to get the most recent - const sortedData = chartData.sort((a, b) => b.x - a.x); - const lastPoint = sortedData[0]; - const lastUpdateTime = new Date(lastPoint.x); - const now = new Date(); - const diffMs = now.getTime() - lastUpdateTime.getTime(); - - const minutes = Math.floor(diffMs / (1000 * 60)); - const hours = Math.floor(diffMs / (1000 * 60 * 60)); - const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - - // Format the actual time - // const actualTime = lastUpdateTime.toLocaleTimeString([], { - // hour: 'numeric', - // minute: '2-digit', - // hour12: true - // }); - - // Format the relative time - let relativeTime; - if (minutes < 1) relativeTime = "Just now"; - else if (minutes < 60) - relativeTime = `${minutes} minute${minutes > 1 ? "s" : ""} ago`; - else if (hours < 24) - relativeTime = `${hours} hour${hours > 1 ? "s" : ""} ago`; - else if (days === 1) relativeTime = "Yesterday"; - else relativeTime = `${days} day${days > 1 ? "s" : ""} ago`; - - return `${relativeTime}`; - }, []); - - // Template injection callback - const injectTemplate = useCallback((template: string) => { - setTemplateToInject(template); - }, []); - - // Clear template after injection - const onTemplateInjected = useCallback(() => { - setTemplateToInject(""); - }, []); - - // Copy conversation function - const handleCopyConversation = useCallback(() => { - if (messages.length === 0) { - Alert.alert("No Messages", "There are no messages to copy."); - return; - } - - const conversationText = messages - .map((message) => { - const timestamp = new Date(message.timestamp).toLocaleString(); - const type = - message.type === "user" - ? "You" - : message.type === "assistant" - ? "Assistant" - : "System"; - return `[${timestamp}] ${type}: ${message.content}`; - }) - .join("\n\n"); - - Clipboard.setString(conversationText); - Alert.alert("Copied!", "Conversation copied to clipboard."); - }, [messages]); - - // Tool menu state management - context-aware - const { - showToolMenu, - toolButtonActive, - rotationAnim, - menuHeightAnim, - toggleToolMenu, - getToolAvailability, - handleToolSelect, - } = useToolMenu({ - executeMCPTool, - sendMessage, - getCurrentContextTideId, - setToolExecuting, - injectTemplate, - }); - - // Chat input state management - context-aware - const { inputMessage, setInputMessage, handleSendMessage } = useChatInput({ - getCurrentContextTideId, // ✅ Context-aware tide ID - isConnected, - getCurrentServerUrl, - sendMessage, - executeMCPTool, - }); - - const scrollViewRef = useRef(null); - - // Initialize agent service when component mounts - useEffect(() => { - const initializeAgent = async () => { - try { - const serverUrl = getCurrentServerUrl(); - setAgentInitialized(true); - - loggingService.info("Chat", "Agent service initialized", { serverUrl }); - } catch (initError) { - loggingService.error("Chat", "Failed to initialize agent service", { - error: initError, - }); - } - }; - - initializeAgent(); - }, [getCurrentServerUrl]); - - // Auto-scroll to bottom when new messages arrive - useEffect(() => { - if (messages.length > 0) { - setTimeout(() => { - scrollViewRef.current?.scrollToEnd({ animated: true }); - }, 100); - } - }, [messages]); - - // Handle agent commands - context-aware - const handleAgentCommand = useCallback( - async (command: string) => { - const contextTideId = getCurrentContextTideId(); - const context = createAgentContext({ - tideId: contextTideId || undefined, - currentContextTide, - isConnected, - getCurrentServerUrl, - }); - - await executeAgentCommand({ - command, - context, - sendAgentMessage, - toggleToolMenu, - }); - }, - [ - getCurrentContextTideId, - currentContextTide, - isConnected, - getCurrentServerUrl, - sendAgentMessage, - toggleToolMenu, - ] - ); + const { width, height } = useWindowDimensions(); + const { timeRange } = useChartDisplayContext(); return ( - - - {/* Tide Info Header */} - {/* - ""} /> - */} - - {/* ✅ REQUIREMENT 2: Sample data from getChartData() function */} - - - - - - - Updated {getLastUpdatedDisplay()} - - - - - - {getLastEnergyDisplay()} - - - - - - {/* - {getSimpleTimeContext(currentContext, dateOffset)} - - - {isAtPresent ? 'Current' : `${dateOffset} ${currentContext === 'daily' ? 'day' : currentContext === 'weekly' ? 'week' : 'month'}${dateOffset > 1 ? 's' : ''} ago`} - */} - - - - - - - - {/* Context Toggle */} - - - - - - - - - - - - - - {/* Tool Menu Overlay */} - {showToolMenu && ( - - )} - - {/* Tool Menu */} - + + - {/* Chat Input with Hierarchical Toggle */} + + + ); +}; - - +// Home: Wraps HomeContent with ChartDisplayContext +// - initialRange: Default time range ("1day") +// - autoRefresh: Enables reactive updates from TimeContext +export default function Home({ navigation }: HomeScreenProps) { + return ( + + + ); } const styles = StyleSheet.create({ - scrollContent: { - flexGrow: 1, - }, - tideInfoHeader: { - paddingTop: 10, - backgroundColor: colors.backgroundColor, - paddingVertical: 12, - paddingBottom: 10, - paddingHorizontal: 16, - }, - tideInfoInnerHeader: { + wrapper: { + gap: 10, + alignItems: "flex-start", + justifyContent: "flex-start", flex: 1, - borderWidth: 0.5, - borderColor: colors.containerBorder, - backgroundColor: colors.containerBackground, - borderRadius: 20, - }, - - container: { backgroundColor: colors.backgroundColor, - flex: 1, - }, - errorCard: { - margin: spacing[4], - backgroundColor: colors.error + "10", - borderColor: colors.error + "30", - }, - retryButton: { - marginTop: spacing[2], - }, - hierarchicalSection: { - maxHeight: 400, - backgroundColor: colors.background.secondary, - }, - hierarchicalContent: { - paddingVertical: spacing[2], - }, - hierarchicalToggle: { - paddingHorizontal: spacing[4], - paddingTop: spacing[2], - alignItems: "center", - }, - hierarchicalToggleButton: { - paddingHorizontal: spacing[4], - paddingVertical: spacing[2], - backgroundColor: colors.neutral[100], - borderRadius: spacing[3], - borderWidth: 1, - borderColor: colors.containerBorder, - }, - hierarchicalToggleButtonActive: { - backgroundColor: colors.backgroundColor, - borderColor: colors.primary[500], - }, - hierarchicalToggleText: { - color: colors.neutral[700], - fontSize: 14, - fontWeight: "500", - }, - hierarchicalToggleTextActive: { - color: colors.neutral[50], - }, - contextSwitcherSection: { - paddingHorizontal: spacing[4], - paddingBottom: spacing[3], - backgroundColor: colors.background.primary, - }, - toolMenuOverlay: { - position: "absolute", - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: "transparent", - }, - energyChartWrapper: { - - marginBottom: 0, - - paddingBottom: 8, - paddingHorizontal: 12, - shadowColor: "#000000", - shadowOffset: { - width: 0, - height: 4, - }, - shadowOpacity: 0.1, - shadowRadius: 12, - elevation: 8, - gap: 10, - display: "flex", - alignItems: "center", - justifyContent: "center", - paddingTop: 13, - - }, - energyChartBackgroundImage: { - - }, - contextToggleWrapper: { - paddingBottom: 0, - alignItems: "center", - display: "flex", - flexDirection: "row", - gap: 10, - width: "100%", - height: 44, - }, - descriptionContainerRow: { - width: "100%", - display: "flex", - flexDirection: "row", - justifyContent: "space-between", - marginBottom: 16, - }, - wholeDescriptionContainer: { - display: "flex", - flexDirection: "row", - gap: 8, - alignItems: "center", - justifyContent: "center", - }, - descriptionContainer: { - display: "flex", - flexDirection: "column", - gap: 0, - }, - title: {}, - description: { - color: "rgba(255,255,255,.6)", - fontSize: 13, - }, - navigationButton: { - height: 28, - width: 28, - backgroundColor: "rgba(255,255,255,.08)", - borderRadius: 100, - display: "flex", - flexDirection: "row", - alignItems: "center", - justifyContent: "center", }, }); diff --git a/apps/mobile/src/screens/Main/LoggingMessages.tsx b/apps/mobile/src/screens/Main/LoggingMessages.tsx new file mode 100644 index 0000000..f7aebf5 --- /dev/null +++ b/apps/mobile/src/screens/Main/LoggingMessages.tsx @@ -0,0 +1,116 @@ +import React, { useState, useEffect, useLayoutEffect } from "react"; +import { + View, + ScrollView, + TouchableOpacity, + Alert, + Clipboard, +} from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import { colors, Text } from "../../design-system"; +import { loggingService, LogMessage } from "../../services/loggingService"; + +export default function LoggingMessages() { + const navigation = useNavigation(); + const [messages, setMessages] = useState([]); + const [searchText, setSearchText] = useState(""); + + const refreshMessages = () => { + setMessages(loggingService.getMessages()); + }; + + const filteredMessages = messages.filter((message) => { + if (!searchText) return true; + const searchLower = searchText.toLowerCase(); + return ( + message.message.toLowerCase().includes(searchLower) || + message.service.toLowerCase().includes(searchLower) || + message.level.toLowerCase().includes(searchLower) || + (message.data && + (typeof message.data === 'string' + ? message.data.toLowerCase().includes(searchLower) + : JSON.stringify(message.data).toLowerCase().includes(searchLower) + ) + ) + ); + }); + + const copyToClipboard = async (message: LogMessage) => { + const formattedMessage = `[${message.level.toUpperCase()}] ${ + message.service + } - ${new Date(message.timestamp).toLocaleString()} +${message.message}${ + message.data + ? "\nData: " + + (typeof message.data === "string" + ? message.data + : JSON.stringify(message.data, null, 2)) + : "" + }`; + + try { + await Clipboard.setString(formattedMessage); + Alert.alert("Copied!", "Log message copied to clipboard"); + } catch (error) { + Alert.alert("Copy Failed", "Failed to copy log message to clipboard"); + } + }; + + useLayoutEffect(() => { + navigation.setOptions({ + headerSearchBarOptions: { + placeholder: "Search logs...", + hideWhenScrolling: false, + autoCapitalize: "none", + autoCorrect: false, + onChangeText: (event: any) => { + setSearchText(event.nativeEvent.text); + }, + }, + }); + }, [navigation]); + + useEffect(() => { + refreshMessages(); + const interval = setInterval(refreshMessages, 1000); // Refresh every second + return () => clearInterval(interval); + }, []); + + return ( + + + {filteredMessages.length === 0 ? ( + + + No log messages yet. Messages will appear here as they are + generated. + + + ) : ( + filteredMessages.map((message) => ( + copyToClipboard(message)} + style={{ + padding: 8, + backgroundColor: colors.background, + borderRadius: 0, + borderBottomWidth: 1, + borderBottomColor: colors.containerBorder, + }} + > + + {message.message} + + + )) + )} + + + ); +} diff --git a/apps/mobile/src/screens/Main/Settings.tsx b/apps/mobile/src/screens/Main/Settings.tsx index 9065e00..e2b6759 100644 --- a/apps/mobile/src/screens/Main/Settings.tsx +++ b/apps/mobile/src/screens/Main/Settings.tsx @@ -10,8 +10,7 @@ import { } from "react-native"; import { useAuth } from "../../context/AuthContext"; import { useMCP } from "../../context/MCPContext"; -import { useServerEnvironment } from "../../context/ServerEnvironmentContext"; -import { ServerEnvironmentSelector } from "../../components/ServerEnvironmentSelector"; +import { useTimeContext } from "../../context/TimeContext"; import { getRobotoMonoFont } from "../../utils/fonts"; import { @@ -27,7 +26,23 @@ export default function Settings() { const { user, signOut, apiKey } = useAuth(); const { isConnected, loading, error, checkConnection, getCurrentServerUrl } = useMCP(); - const { getCurrentEnvironment } = useServerEnvironment(); + const { + timeInfo, + locationInfo, + solarInfo, + loading: timeLoading, + error: timeError, + permissions, + refreshLocation, + getTimeOfDay, + } = useTimeContext(); + + // Hardcoded environment 006 + const hardcodedEnvironment = { + id: "env.006", + name: "Mason Dev", + url: "https://tides-006.mpazbot.workers.dev" + }; // Server environment configuration state const [showServerConfig, setShowServerConfig] = useState(false); @@ -51,18 +66,6 @@ export default function Settings() { } catch (err) {} }; - const handleEnvironmentSelected = async () => { - // Close the server config panel when environment is selected - setShowServerConfig(false); - - // Trigger a connection check to verify the new environment - try { - await checkConnection(); - } catch (err) { - // Error handling is done in checkConnection - } - }; - const handleCopyApiKey = async () => { if (!apiKey) { Alert.alert("No API Key", "No API key available to copy"); @@ -143,7 +146,7 @@ export default function Settings() { onPress={() => setShowServerConfig(!showServerConfig)} > - Current Environment: {getCurrentEnvironment().name} + Current Environment: {hardcodedEnvironment.name} {getCurrentServerUrl()} @@ -158,17 +161,6 @@ export default function Settings() { : "▼ Show Configuration"} - - {showServerConfig && ( - - - - )} {/* MCP Connection Section */} @@ -243,6 +235,153 @@ export default function Settings() { + {/* Time & Location Context Section */} + + + Time & Location Context + + + {/* Time Information */} + + + Time Information: + + {timeInfo ? ( + <> + + Local Time: {timeInfo.localTime.toISOString()} + + + Timezone: {timeInfo.timezone} + + + Formatted Time: {timeInfo.formattedTime} + + + Formatted Date: {timeInfo.formattedDate} + + + Timestamp: {timeInfo.timestamp} + + + ) : ( + + No time information available + + )} + + + {/* Location Information */} + + + Location Information: + + {locationInfo ? ( + <> + + Latitude: {locationInfo.latitude} + + + Longitude: {locationInfo.longitude} + + + Sunrise: {locationInfo.sunrise?.toLocaleString() || 'N/A'} + + + Sunset: {locationInfo.sunset?.toLocaleString() || 'N/A'} + + + Time of Day: {locationInfo.timeOfDay || 'N/A'} + + + City: {locationInfo.city || 'Not available'} + + + Region: {locationInfo.region || 'Not available'} + + + Country: {locationInfo.country || 'Not available'} + + + Formatted Address: {locationInfo.formattedAddress || 'Not available'} + + + ) : ( + + No location information available + + )} + + + {/* Solar Information */} + + + Solar Information: + + {solarInfo ? ( + <> + + Sunrise: {solarInfo.sunrise.toLocaleString()} + + + Sunset: {solarInfo.sunset.toLocaleString()} + + + Solar Noon: {solarInfo.solarNoon.toLocaleString()} + + + Golden Hour: {solarInfo.goldenHour.toLocaleString()} + + + Sun Azimuth: {solarInfo.azimuth.toFixed(6)} radians + + + Sun Altitude: {solarInfo.altitude.toFixed(6)} radians + + + ) : ( + + No solar information available + + )} + + + {/* Context Status */} + + + Context Status: + + + Loading: {timeLoading ? 'Yes' : 'No'} + + + Error: {timeError || 'None'} + + + Location Permissions: {permissions} + + + Current Time of Day: {getTimeOfDay()} + + + + {/* Context Actions */} + + + Available Actions: + + + + + {/* Debug Information Section */} {/* TODO: Move debug panel to dedicated development screen */} {user && ( @@ -260,7 +399,7 @@ export default function Settings() { color="secondary" style={styles.debugValue} > - {getCurrentEnvironment().name} ({getCurrentEnvironment().id}) + {hardcodedEnvironment.name} ({hardcodedEnvironment.id}) @@ -483,4 +622,7 @@ const styles = StyleSheet.create({ signOutButtonStyle: { marginTop: spacing[4], }, + refreshButton: { + marginTop: spacing[2], + }, }); diff --git a/apps/mobile/src/screens/Main/TideDetails.tsx b/apps/mobile/src/screens/Main/TideDetails.tsx index 8266c66..5022a3d 100644 --- a/apps/mobile/src/screens/Main/TideDetails.tsx +++ b/apps/mobile/src/screens/Main/TideDetails.tsx @@ -164,9 +164,6 @@ export default function TideDetails() { {tide.status.toUpperCase()} - - {tide.flow_type} tide - {tide.description && ( diff --git a/apps/mobile/src/services/LoggingService.ts b/apps/mobile/src/services/LoggingService.ts index b170ee4..6e83bb0 100644 --- a/apps/mobile/src/services/LoggingService.ts +++ b/apps/mobile/src/services/LoggingService.ts @@ -1,19 +1,131 @@ +export interface LogMessage { + id: string; + timestamp: string; + level: "info" | "error" | "warn" | "debug"; + service: string; + message: string; + data?: any; +} + class LoggingService { + private messages: LogMessage[] = []; + private maxMessages = 500; + private originalConsole = { + log: console.log, + error: console.error, + warn: console.warn, + debug: console.debug, + info: console.info, + }; + private isIntercepting = false; + + constructor() { + this.interceptConsole(); + } + + private addMessage( + level: "info" | "error" | "warn" | "debug", + service: string, + message: string, + data?: any + ) { + const logMessage: LogMessage = { + id: `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + timestamp: new Date().toISOString(), + level, + service, + message, + data, + }; + + this.messages.unshift(logMessage); + if (this.messages.length > this.maxMessages) { + this.messages = this.messages.slice(0, this.maxMessages); + } + } + + private interceptConsole() { + if (this.isIntercepting) return; + + this.isIntercepting = true; + + console.log = (...args: any[]) => { + this.originalConsole.log(...args); + this.addMessage("info", "console", this.formatArgs(args)); + }; + + console.error = (...args: any[]) => { + this.originalConsole.error(...args); + this.addMessage("error", "console", this.formatArgs(args)); + }; + + console.warn = (...args: any[]) => { + this.originalConsole.warn(...args); + this.addMessage("warn", "console", this.formatArgs(args)); + }; + + console.debug = (...args: any[]) => { + this.originalConsole.debug(...args); + this.addMessage("debug", "console", this.formatArgs(args)); + }; + + console.info = (...args: any[]) => { + this.originalConsole.info(...args); + this.addMessage("info", "console", this.formatArgs(args)); + }; + } + + private formatArgs(args: any[]): string { + return args + .map((arg) => { + if (typeof arg === "string") return arg; + if (typeof arg === "object") { + try { + return JSON.stringify(arg, null, 2); + } catch { + return String(arg); + } + } + return String(arg); + }) + .join(" "); + } + + restoreConsole() { + if (!this.isIntercepting) return; + + console.log = this.originalConsole.log; + console.error = this.originalConsole.error; + console.warn = this.originalConsole.warn; + console.debug = this.originalConsole.debug; + console.info = this.originalConsole.info; + + this.isIntercepting = false; + } + info(service: string, message: string, data?: any) { - console.log(`[${service}] ${message}`, data || ''); + this.originalConsole.log(`[${service}] ${message}`, data || ""); + this.addMessage("info", service, message, data); } error(service: string, message: string, data?: any) { - console.error(`[${service}] ${message}`, data || ''); + this.originalConsole.error(`[${service}] ${message}`, data || ""); + this.addMessage("error", service, message, data); } warn(service: string, message: string, data?: any) { - console.warn(`[${service}] ${message}`, data || ''); + this.originalConsole.warn(`[${service}] ${message}`, data || ""); + this.addMessage("warn", service, message, data); } debug(service: string, message: string, data?: any) { - console.debug(`[${service}] ${message}`, data || ''); + this.originalConsole.debug(`[${service}] ${message}`, data || ""); + this.addMessage("debug", service, message, data); + } + + getMessages(): LogMessage[] { + return [...this.messages]; } } -export const loggingService = new LoggingService(); \ No newline at end of file +export const loggingService = new LoggingService(); diff --git a/apps/mobile/src/services/agentService.ts b/apps/mobile/src/services/agentService.ts index c16545e..2c01328 100644 --- a/apps/mobile/src/services/agentService.ts +++ b/apps/mobile/src/services/agentService.ts @@ -23,278 +23,186 @@ export interface AgentResponse { }; } -export interface AgentStatus { - isHealthy: boolean; - status: string; - connected: boolean; - version?: string; - lastCheck: string; -} - -export interface AgentInsights { - insights: string[]; - recommendations?: string[]; - score?: number; -} - -export interface AgentOptimization { - optimizations: string[]; - estimated_improvement?: string; - tideId: string; -} - -export interface AgentPreferences { - preferences: Record; - updated: boolean; - timestamp: string; -} - class AgentService { private readonly SERVICE_NAME = "AgentService"; private sessionId: string | null = null; private conversationId: string | null = null; private conversationHistory: Array<{ role: string; content: string }> = []; - private getServerUrl: (() => string) | null = null; - private mcpToolExecutor: ((toolName: string, parameters: any) => Promise) | null = null; + private readonly BASE_URL = "https://tides-006.mpazbot.workers.dev"; + private cachedUserId: string | null = null; + private userIdCacheExpiry: number = 0; private readonly AI_ENDPOINTS = { conversation: "/ai/conversation", classification: "/ai/classify-intent", productivity: "/ai/productivity-analysis", flowSuggestions: "/ai/flow-suggestions", - health: "/ai/health" + health: "/ai/health", }; /** - * Configure the service with a URL provider from MCP context - */ - setUrlProvider(getServerUrl: () => string): void { - this.getServerUrl = getServerUrl; - - // Log current environment for debugging - const currentUrl = getServerUrl(); - loggingService.info(this.SERVICE_NAME, "Server URL configured", { - url: currentUrl, - hasAIEndpoints: this.checkAIEndpointsAvailable(currentUrl) - }); - } - - /** - * Check if AI endpoints are likely available on current server + * Simple retry wrapper with exponential backoff */ - private checkAIEndpointsAvailable(baseUrl: string): boolean { - // env006 and env001 are known to have AI endpoints - return baseUrl.includes('tides-006') || baseUrl.includes('tides-001'); - } - - /** - * Configure the service with MCP tool executor from MCP context - */ - setMCPToolExecutor(executor: (toolName: string, parameters: any) => Promise): void { - this.mcpToolExecutor = executor; - loggingService.info(this.SERVICE_NAME, "MCP tool executor configured", {}); - } + private async fetchWithRetry( + url: string, + options: RequestInit, + timeout = 10000 + ): Promise { + for (let attempt = 1; attempt <= 3; attempt++) { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); - /** - * Extract user ID from API key when available - * Format: tides_userId_randomId -> extract userId - */ - private async getUserIdFromApiKey(): Promise { - try { - const apiKey = await authService.getApiKey(); - if (!apiKey) return null; - - const userId = extractUserIdFromApiKey(apiKey); - if (userId) { - loggingService.info(this.SERVICE_NAME, "Extracted user ID from API key", { - userId, - apiKeyPrefix: apiKey.substring(0, 15) + '...' + const response = await fetch(url, { + ...options, + signal: controller.signal, }); - return userId; + + clearTimeout(timeoutId); + return response; + } catch (error) { + if (attempt === 3) throw error; + await new Promise((resolve) => + setTimeout(resolve, Math.pow(2, attempt - 1) * 1000) + ); } - - loggingService.warn(this.SERVICE_NAME, "API key is not in expected format for user ID extraction", { - apiKeyPrefix: apiKey.substring(0, 15) + '...', - expectedFormat: 'tides_userId_randomId' - }); - return null; - } catch (error) { - loggingService.error(this.SERVICE_NAME, "Failed to extract user ID from API key", error); - return null; } + throw new Error("Retry failed"); } /** - * Get user ID with fallback to API key extraction + * Get cached user ID or resolve it with Supabase first, fallback to API key extraction + * Cache expires after 5 minutes to handle auth state changes */ - private async getUserId(): Promise { + private async getUserId(): Promise { + const now = Date.now(); + + // Return cached userId if still valid (5 minutes) + if (this.cachedUserId && now < this.userIdCacheExpiry) { + return this.cachedUserId; + } try { // First try Supabase current user const user = await authService.getCurrentUser(); if (user?.id) { - loggingService.info(this.SERVICE_NAME, "Got user ID from Supabase", { userId: user.id }); + loggingService.info(this.SERVICE_NAME, "Got user ID from Supabase", { + userId: user.id, + }); + this.cachedUserId = user.id; + this.userIdCacheExpiry = now + 5 * 60 * 1000; // 5 minutes return user.id; } - - // Fallback to extracting from API key - loggingService.info(this.SERVICE_NAME, "Supabase user not available, extracting from API key"); - return await this.getUserIdFromApiKey(); - } catch (error) { - loggingService.error(this.SERVICE_NAME, "Failed to get user ID", error); - return null; - } - } - - /** - * Execute an MCP tool directly from agent service - */ - async executeMCPTool(toolName: string, parameters: any): Promise { - if (!this.mcpToolExecutor) { - throw new Error("MCP tool executor not configured"); - } - loggingService.info(this.SERVICE_NAME, "Executing MCP tool", { toolName, parameters }); - - try { - const result = await this.mcpToolExecutor(toolName, parameters); - loggingService.info(this.SERVICE_NAME, "MCP tool executed successfully", { toolName, result }); - return result; - } catch (error) { - loggingService.error(this.SERVICE_NAME, "MCP tool execution failed", { error, toolName, parameters }); - throw error; - } - } - - private async makeRequest( - endpoint: string, - method: "GET" | "POST" = "POST", - body?: any - ): Promise { - try { + // Fallback to extracting from API key + loggingService.info( + this.SERVICE_NAME, + "Supabase user not available, extracting from API key" + ); const apiKey = await authService.getApiKey(); if (!apiKey) { - throw new Error("No auth token available"); - } - - // Use configured server URL from MCP context with fallback to env001 - const baseUrl = this.getServerUrl?.() || "https://tides-001.mpazbot.workers.dev"; - if (!this.getServerUrl) { - loggingService.warn(this.SERVICE_NAME, "Using fallback URL (env001) - MCP context not configured"); + throw new Error( + "No authentication available - both Supabase user and API key are missing" + ); } - const url = `${baseUrl}/agents/tide-productivity/${endpoint}`; - - loggingService.info(this.SERVICE_NAME, `Agent request URL: ${url}`, {}); - loggingService.info(this.SERVICE_NAME, `Making ${method} request`, { - url, - requestBody: JSON.stringify(body, null, 2) - }); - - // Apply React Native network fixes with retry logic - const maxRetries = 3; - let lastError: unknown; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - loggingService.info(this.SERVICE_NAME, `Attempt ${attempt}/${maxRetries}`, { url }); - - // Add timeout and User-Agent for React Native compatibility - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout - - const response = await fetch(url, { - method, - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - 'User-Agent': 'TidesMobile/1.0 React-Native/0.80.2' - }, - ...(body && { body: JSON.stringify(body) }), - signal: controller.signal - }); - - clearTimeout(timeoutId); - - // If we get here, the request succeeded - loggingService.info(this.SERVICE_NAME, `Request succeeded on attempt ${attempt}`, { - status: response.status, - statusText: response.statusText - }); - - return await this.handleAgentResponse(response); - - } catch (networkError: unknown) { - lastError = networkError; - const errorMessage = networkError instanceof Error ? networkError.message : 'Unknown network error'; - - loggingService.error(this.SERVICE_NAME, `Attempt ${attempt} failed`, { - error: errorMessage, - url - }); - - if (attempt === maxRetries) { - loggingService.error(this.SERVICE_NAME, `All ${maxRetries} attempts failed`, { - finalError: errorMessage, - url - }); - break; + const userId = extractUserIdFromApiKey(apiKey); + if (userId) { + loggingService.info( + this.SERVICE_NAME, + "Extracted user ID from API key", + { + userId, + apiKeyPrefix: apiKey.substring(0, 15) + "...", } - - // Wait before retrying (exponential backoff) - const delay = Math.pow(2, attempt - 1) * 1000; // 1s, 2s, 4s - loggingService.info(this.SERVICE_NAME, `Waiting ${delay}ms before retry...`, {}); - await new Promise(resolve => setTimeout(resolve, delay)); - } + ); + this.cachedUserId = userId; + this.userIdCacheExpiry = now + 5 * 60 * 1000; // 5 minutes + return userId; } - // If we get here, all retries failed - const errorMessage = lastError instanceof Error ? lastError.message : 'Unknown network error'; - throw new Error(`Agent request failed after ${maxRetries} attempts: ${errorMessage}`); + loggingService.warn( + this.SERVICE_NAME, + "API key is not in expected format for user ID extraction", + { + apiKeyPrefix: apiKey.substring(0, 15) + "...", + expectedFormat: "tides_userId_randomId", + } + ); + throw new Error( + "API key is not in expected format for user ID extraction" + ); } catch (error) { + // Clear cache on error + this.cachedUserId = null; + this.userIdCacheExpiry = 0; + loggingService.error( this.SERVICE_NAME, - `Request to ${endpoint} failed`, + "Failed to resolve user ID", error ); throw new Error( - `Agent communication failed: ${ + `User ID resolution failed: ${ error instanceof Error ? error.message : "Unknown error" }` ); } } + private async makeRequest( + endpoint: string, + method: "GET" | "POST" = "POST", + body?: any + ): Promise { + const apiKey = await authService.getApiKey(); + if (!apiKey) { + throw new Error("No auth token available"); + } + + const url = `${this.BASE_URL}/agents/tide-productivity/${endpoint}`; + + const response = await this.fetchWithRetry(url, { + method, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + "User-Agent": "TidesMobile/1.0 React-Native/0.80.2", + }, + ...(body && { body: JSON.stringify(body) }), + }); + + return await this.handleAgentResponse(response); + } + private async handleAgentResponse(response: Response): Promise { - loggingService.info(this.SERVICE_NAME, `Response received`, { - status: response.status, + loggingService.info(this.SERVICE_NAME, `Response received`, { + status: response.status, statusText: response.statusText, - headers: Object.fromEntries(response.headers.entries()) + headers: Object.fromEntries(response.headers.entries()), }); if (!response.ok) { const errorText = await response.text(); - loggingService.error(this.SERVICE_NAME, `Agent request failed`, { - status: response.status, - errorText + loggingService.error(this.SERVICE_NAME, `Agent request failed`, { + status: response.status, + errorText, }); - throw new Error( - `Agent request failed: ${response.status} ${errorText}` - ); + throw new Error(`Agent request failed: ${response.status} ${errorText}`); } const data = await response.json(); - loggingService.info(this.SERVICE_NAME, `Agent response received`, { - data, + loggingService.info(this.SERVICE_NAME, `Agent response received`, { + data, hasContent: !!data?.content, - responseKeys: Object.keys(data || {}) + responseKeys: Object.keys(data || {}), }); // Transform the response to match expected AgentResponse format return { content: data.result?.message || "No response from agent", - message: data.result?.message || "No response from agent", + message: data.result?.message || "No response from agent", timestamp: new Date().toISOString(), type: data.result?.error ? "error" : "success", - data: data + data: data, }; } @@ -303,43 +211,25 @@ class AgentService { context?: TideContext ): Promise { try { - loggingService.info(this.SERVICE_NAME, "Processing message with enhanced AI", { - messageLength: message.length, - hasContext: !!context, - hasSessionId: !!this.sessionId, - hasConversationId: !!this.conversationId - }); - - // Get userId with fallback to API key extraction - loggingService.info(this.SERVICE_NAME, "Getting user ID", {}); - - const userId = await this.getUserId(); - - if (!userId) { - loggingService.error(this.SERVICE_NAME, "No user ID available", { userId }); - throw new Error("User ID is required for agent communication"); - } - - loggingService.info(this.SERVICE_NAME, "User ID confirmed, proceeding", { userId }); + loggingService.info( + this.SERVICE_NAME, + "Processing message with enhanced AI", + { + messageLength: message.length, + hasContext: !!context, + hasSessionId: !!this.sessionId, + hasConversationId: !!this.conversationId, + } + ); - // Initialize session and conversation IDs if not already set - loggingService.info(this.SERVICE_NAME, "Initializing session and conversation IDs", { - hasSessionId: !!this.sessionId, - hasConversationId: !!this.conversationId + const userId = await this.getUserId(); + loggingService.info(this.SERVICE_NAME, "User ID resolved, proceeding", { + userId, }); - - if (!this.sessionId) { - this.sessionId = this.generateSessionId(); - loggingService.info(this.SERVICE_NAME, "Generated new session ID", { sessionId: this.sessionId }); - } - if (!this.conversationId) { - this.conversationId = this.generateConversationId(); - loggingService.info(this.SERVICE_NAME, "Generated new conversation ID", { conversationId: this.conversationId }); - } // Add user message to history this.conversationHistory.push({ role: "user", content: message }); - + // Keep only last 10 messages to avoid context getting too large if (this.conversationHistory.length > 10) { this.conversationHistory = this.conversationHistory.slice(-10); @@ -349,37 +239,46 @@ class AgentService { sessionId: this.sessionId, conversationId: this.conversationId, historyLength: this.conversationHistory.length, - lastFewMessages: this.conversationHistory.slice(-3) + lastFewMessages: this.conversationHistory.slice(-3), }); // Try AI conversation endpoint first - loggingService.info(this.SERVICE_NAME, "About to call sendConversationMessage", { - endpoint: "conversation", - userId, - sessionId: this.sessionId, - conversationId: this.conversationId - }); - - try { - const conversationResponse = await this.sendConversationMessage(message, { + loggingService.info( + this.SERVICE_NAME, + "About to call sendConversationMessage", + { + endpoint: "conversation", userId, sessionId: this.sessionId, conversationId: this.conversationId, - tideId: context?.tideId, - workContext: context?.workContext, - recentMessages: this.conversationHistory.slice(-5) // Send last 5 messages for context - }); - + } + ); + + try { + const conversationResponse = await this.sendConversationMessage( + message, + { + userId, + tideId: context?.tideId, + workContext: context?.workContext, + recentMessages: this.conversationHistory.slice(-5), // Send last 5 messages for context + } + ); + // Add assistant response to history - this.conversationHistory.push({ - role: "assistant", - content: conversationResponse.content + this.conversationHistory.push({ + role: "assistant", + content: conversationResponse.content, }); - + return conversationResponse; } catch (aiError) { - loggingService.warn(this.SERVICE_NAME, "AI conversation failed, falling back to legacy", aiError); - + loggingService.warn( + this.SERVICE_NAME, + "AI conversation failed, falling back to legacy", + aiError + ); + // Fallback to legacy agent endpoint const requestBody = { userId, @@ -403,8 +302,6 @@ class AgentService { message: string, context: { userId: string; - sessionId: string; - conversationId: string; tideId?: string; workContext?: string; recentMessages?: Array<{ role: string; content: string }>; @@ -414,36 +311,50 @@ class AgentService { message: message.trim(), context: { userId: context.userId, - sessionId: context.sessionId, - conversationId: context.conversationId, tideId: context.tideId, - flowContext: "daily", // Default to daily context - recentMessages: context.recentMessages || [] + recentMessages: context.recentMessages || [], }, analysisType: "conversation", - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), }; loggingService.info(this.SERVICE_NAME, "Sending AI conversation request", { messageLength: message.length, - userId: context.userId.substring(0, 8) + '...' + userId: context.userId.substring(0, 8) + "...", }); try { - const response = await this.makeAIRequest(this.AI_ENDPOINTS.conversation, "POST", requestBody); - + const response = await this.makeAIRequest( + this.AI_ENDPOINTS.conversation, + "POST", + requestBody + ); + return { - content: response.response || response.result?.response || response.result?.analysis || "I understand your message. How can I help?", - message: response.response || response.result?.response || response.result?.analysis || "I understand your message. How can I help?", + content: + response.response || + response.result?.response || + response.result?.analysis || + "I understand your message. How can I help?", + message: + response.response || + response.result?.response || + response.result?.analysis || + "I understand your message. How can I help?", timestamp: new Date().toISOString(), type: response.type || response.result?.type || "text", agentId: "ai-conversation", - suggestedTools: response.suggestedTools || response.result?.suggestedTools || [], + suggestedTools: + response.suggestedTools || response.result?.suggestedTools || [], toolCall: response.toolCall || response.result?.toolCall, - data: response + data: response, }; } catch (error) { - loggingService.error(this.SERVICE_NAME, "AI conversation request failed", error); + loggingService.error( + this.SERVICE_NAME, + "AI conversation request failed", + error + ); throw error; } } @@ -462,135 +373,45 @@ class AgentService { confidence: number; suggestions?: string[]; }> { - const userId = await this.getUserId() || context?.userId; - - if (!userId) { - throw new Error("User ID is required for tool classification"); - } + const userId = context?.userId || (await this.getUserId()); const requestBody = { message: message.trim(), availableTools, context: { userId, - tideId: context?.tideId + tideId: context?.tideId, }, analysisType: "tool_classification", - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), }; try { - const response = await this.makeAIRequest(this.AI_ENDPOINTS.classification, "POST", requestBody); - + const response = await this.makeAIRequest( + this.AI_ENDPOINTS.classification, + "POST", + requestBody + ); + return { intent: response.result?.intent || "conversation", toolName: response.result?.toolName, parameters: response.result?.parameters || {}, confidence: response.result?.confidence || 0.3, - suggestions: response.result?.suggestions || [] + suggestions: response.result?.suggestions || [], }; } catch (error) { - loggingService.error(this.SERVICE_NAME, "Tool classification failed", error); - + loggingService.error( + this.SERVICE_NAME, + "Tool classification failed", + error + ); + // Fallback to simple classification return this.fallbackToolClassification(message, availableTools); } } - /** - * Get productivity insights using AI - */ - async getProductivityInsights( - analysisDepth: "quick" | "detailed" = "quick" - ): Promise { - const userId = await this.getUserId(); - - if (!userId) { - throw new Error("User ID is required for productivity analysis"); - } - - const requestBody = { - context: { - userId, - sessionId: this.generateSessionId(), - conversationId: this.generateConversationId() - }, - analysisDepth, - analysisType: "productivity", - timestamp: new Date().toISOString() - }; - - try { - const response = await this.makeAIRequest(this.AI_ENDPOINTS.productivity, "POST", requestBody); - - return { - content: response.result?.analysis || "Productivity analysis completed", - message: response.result?.analysis || "Productivity analysis completed", - timestamp: new Date().toISOString(), - type: "productivity_analysis", - agentId: "productivity-ai", - data: response - }; - } catch (error) { - loggingService.error(this.SERVICE_NAME, "Productivity insights failed", error); - throw error; - } - } - - /** - * Generate flow suggestions - */ - async generateFlowSuggestions( - energyLevel: number = 6 - ): Promise { - const userId = await this.getUserId(); - - if (!userId) { - throw new Error("User ID is required for flow suggestions"); - } - - const requestBody = { - context: { - userId, - sessionId: this.generateSessionId(), - conversationId: this.generateConversationId() - }, - energyLevel, - analysisType: "flow_suggestions", - timestamp: new Date().toISOString() - }; - - try { - const response = await this.makeAIRequest(this.AI_ENDPOINTS.flowSuggestions, "POST", requestBody); - - return { - content: response.result?.suggestions || "Consider starting a moderate flow session", - message: response.result?.suggestions || "Consider starting a moderate flow session", - timestamp: new Date().toISOString(), - type: "flow_suggestions", - agentId: "flow-ai", - suggestedTools: ["createTide", "startTideFlow"], - data: response - }; - } catch (error) { - loggingService.error(this.SERVICE_NAME, "Flow suggestions failed", error); - throw error; - } - } - - /** - * Check AI service health - */ - async checkAIHealth(): Promise { - try { - await this.makeAIRequest(this.AI_ENDPOINTS.health, "GET"); - return true; - } catch (error) { - loggingService.warn(this.SERVICE_NAME, "AI health check failed", error); - return false; - } - } - /** * Make request to AI endpoints */ @@ -604,132 +425,46 @@ class AgentService { throw new Error("No auth token available for AI service"); } - const baseUrl = this.getServerUrl?.() || "https://tides-001.mpazbot.workers.dev"; - if (!this.getServerUrl) { - loggingService.warn(this.SERVICE_NAME, "Using fallback URL (env001) - MCP context not configured"); - } - - // Warn if current environment might not have AI endpoints - if (!this.checkAIEndpointsAvailable(baseUrl)) { - loggingService.warn(this.SERVICE_NAME, "Current environment may not support AI endpoints", { - baseUrl, - suggestedEnvs: ["env001", "env006"] - }); - } - const url = `${baseUrl}${endpoint}`; - - loggingService.info(this.SERVICE_NAME, `AI request to: ${url}`, { method }); - - // Add timeout controller for AI requests - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 15000); // 15 second timeout - - try { - const response = await fetch(url, { + const url = `${this.BASE_URL}${endpoint}`; + + const response = await this.fetchWithRetry( + url, + { method, headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - 'Accept': 'application/json', - 'User-Agent': 'TidesMobile/1.0 React-Native/0.80.2' + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + Accept: "application/json", + "User-Agent": "TidesMobile/1.0 React-Native/0.80.2", }, ...(body && { body: JSON.stringify(body) }), - signal: controller.signal - }); - - clearTimeout(timeoutId); + }, + 15000 + ); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`AI request failed: ${response.status} ${errorText}`); - } + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`AI request failed: ${response.status} ${errorText}`); + } - // Handle Server-Sent Events (text/event-stream) format - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('text/event-stream')) { - const text = await response.text(); - loggingService.info(this.SERVICE_NAME, "Received SSE response", { contentType, textLength: text.length }); - - // Parse SSE format: "event: message\ndata: {...}\n\n" - const lines = text.split('\n'); - let jsonData = ''; - - for (const line of lines) { - if (line.startsWith('data: ')) { - jsonData = line.substring(6); // Remove 'data: ' prefix - break; - } - } - - if (jsonData) { - try { - const parsed = JSON.parse(jsonData); - loggingService.info(this.SERVICE_NAME, "Parsed SSE JSON", { hasResult: !!parsed.result }); - return parsed; - } catch (parseError) { - loggingService.warn(this.SERVICE_NAME, "Failed to parse SSE JSON data", { jsonData, parseError }); - throw new Error(`Invalid JSON in SSE response: ${parseError}`); + // Handle Server-Sent Events (text/event-stream) format + const contentType = response.headers.get("content-type"); + if (contentType && contentType.includes("text/event-stream")) { + const text = await response.text(); + const lines = text.split("\n"); + + for (const line of lines) { + if (line.startsWith("data: ")) { + const jsonData = line.substring(6); + if (jsonData) { + return JSON.parse(jsonData); } - } else { - throw new Error('No data found in SSE response'); - } - } - - // Regular JSON response - return await response.json(); - } catch (error) { - clearTimeout(timeoutId); - - // Enhanced error handling - if (error instanceof Error) { - if (error.name === 'AbortError') { - throw new Error('AI request timed out (15s)'); - } - if (error.message === 'Network request failed') { - throw new Error('Network connection failed - check server availability'); } } - throw error; + throw new Error("No data found in SSE response"); } - } - /** - * Reset conversation context (for new conversations) - */ - resetConversation(): void { - this.sessionId = null; - this.conversationId = null; - this.conversationHistory = []; - loggingService.info(this.SERVICE_NAME, "Conversation context reset"); - } - - /** - * Get current conversation context (for debugging) - */ - getConversationContext(): { - sessionId: string | null; - conversationId: string | null; - historyLength: number - } { - return { - sessionId: this.sessionId, - conversationId: this.conversationId, - historyLength: this.conversationHistory.length - }; - } - - /** - * Generate unique session ID - */ - private generateSessionId(): string { - return `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; - } - - /** - * Generate unique conversation ID - */ - private generateConversationId(): string { - return `conv_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; + return await response.json(); } /** @@ -745,85 +480,31 @@ class AgentService { suggestions: string[]; } { const lowerMessage = message.toLowerCase(); - + // Simple keyword matching if (lowerMessage.includes("create") || lowerMessage.includes("new")) { return { intent: "direct_tool", toolName: "createTide", confidence: 0.6, - suggestions: ["createTide"] + suggestions: ["createTide"], }; } - + if (lowerMessage.includes("list") || lowerMessage.includes("show")) { return { intent: "direct_tool", toolName: "getTideList", confidence: 0.6, - suggestions: ["getTideList"] + suggestions: ["getTideList"], }; } - + return { intent: "conversation", confidence: 0.3, - suggestions: availableTools.slice(0, 3) - }; - } - - async checkStatus(): Promise { - try { - // Check both legacy and AI endpoints - const [legacyStatus, aiHealthy] = await Promise.allSettled([ - this.makeRequest("status", "GET"), - this.checkAIHealth() - ]); - - const isLegacyHealthy = legacyStatus.status === 'fulfilled'; - const isAIHealthy = aiHealthy.status === 'fulfilled' && aiHealthy.value; - - return { - isHealthy: isLegacyHealthy || isAIHealthy, - status: isAIHealthy ? "enhanced" : (isLegacyHealthy ? "legacy" : "degraded"), - connected: isLegacyHealthy || isAIHealthy, - version: "v2.0-ai-enhanced", - lastCheck: new Date().toISOString() - }; - } catch (error) { - return { - isHealthy: false, - status: "error", - connected: false, - lastCheck: new Date().toISOString() - }; - } - } - - async getInsights(): Promise { - const requestBody = { - timestamp: new Date().toISOString(), - }; - - return this.makeRequest("insights", "POST", requestBody); - } - - async optimizeTide(tideId: string): Promise { - const requestBody = { - tideId, - timestamp: new Date().toISOString(), + suggestions: availableTools.slice(0, 3), }; - - return this.makeRequest("optimize", "POST", requestBody); - } - - async updatePreferences(preferences: Record): Promise { - const requestBody = { - preferences, - timestamp: new Date().toISOString(), - }; - - return this.makeRequest("preferences", "POST", requestBody); } } diff --git a/apps/mobile/src/services/authService.ts b/apps/mobile/src/services/authService.ts index 89d5f0e..1fb87b1 100644 --- a/apps/mobile/src/services/authService.ts +++ b/apps/mobile/src/services/authService.ts @@ -5,14 +5,15 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; // Keep fo import type { Session } from "@supabase/supabase-js"; class AuthService { - private currentUrl = "https://tides-001.mpazbot.workers.dev"; // Fallback to env001 + // ENV: Change for production - currently using tides-006 + private currentUrl = "https://tides-006.mpazbot.workers.dev"; // Fallback to env006 private urlReady = false; private urlProvider: (() => string) | null = null; constructor() { // Don't await in constructor - it's called synchronously - this.initUrl().catch(error => { - console.error('[AuthService] URL initialization failed:', error); + this.initUrl().catch((error) => { + console.error("[AuthService] URL initialization failed:", error); this.urlReady = true; // Mark as ready even if it fails }); } @@ -56,54 +57,62 @@ class AuthService { } private async registerApiKeyWithMCPServer( - apiKey: string, - userId: string, + apiKey: string, + userId: string, email: string ): Promise { try { - console.log('[AuthService] Registering API key with MCP server...', { - userId, + console.log("[AuthService] Registering API key with MCP server...", { + userId, email, - serverUrl: this.currentUrl + serverUrl: this.currentUrl, }); - + const serverUrl = this.urlProvider ? this.urlProvider() : this.currentUrl; const response = await fetch(`${serverUrl}/register-api-key`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", }, body: JSON.stringify({ api_key: apiKey, user_id: userId, user_email: email, - name: 'Mobile App Key' - }) + name: "Mobile App Key", + }), }); - + const result = await response.json(); - + if (result.success) { - console.log('[AuthService] ✅ API key registered with MCP server successfully', { - keyHash: result.key_hash?.substring(0, 8) + '...', - userId: result.user_id - }); + console.log( + "[AuthService] ✅ API key registered with MCP server successfully", + { + keyHash: result.key_hash?.substring(0, 8) + "...", + userId: result.user_id, + } + ); return true; } else { - console.error('[AuthService] ❌ Failed to register API key with MCP server:', { - error: result.error, - details: result.details - }); + console.error( + "[AuthService] ❌ Failed to register API key with MCP server:", + { + error: result.error, + details: result.details, + } + ); return false; } } catch (error) { - console.error('[AuthService] ❌ Network error during API key registration:', error); + console.error( + "[AuthService] ❌ Network error during API key registration:", + error + ); return false; } } - async signUpWithEmail(email: string, password: string) { try { const { data, error } = await supabase.auth.signUp({ email, password }); @@ -112,9 +121,13 @@ class AuthService { if (data.user && data.session) { const apiKey = this.generateApiKey(data.user.id); await secureStorage.setItem("api_key", apiKey); - + // Register with MCP server D1 database - await this.registerApiKeyWithMCPServer(apiKey, data.user.id, data.user.email || ''); + await this.registerApiKeyWithMCPServer( + apiKey, + data.user.id, + data.user.email || "" + ); } return { user: data.user, session: data.session }; @@ -125,37 +138,43 @@ class AuthService { async signInWithEmail(email: string, password: string) { try { - console.log('[AuthService] Attempting Supabase sign in...', { email }); - console.log('[AuthService] Supabase URL:', SUPABASE_CONFIG.url); - + console.log("[AuthService] Attempting Supabase sign in...", { email }); + console.log("[AuthService] Supabase URL:", SUPABASE_CONFIG.url); + const { data, error } = await supabase.auth.signInWithPassword({ email, password, }); - - console.log('[AuthService] Supabase response:', { - hasData: !!data, - hasError: !!error, - errorMessage: error?.message + + console.log("[AuthService] Supabase response:", { + hasData: !!data, + hasError: !!error, + errorMessage: error?.message, }); - + if (error) throw new Error(error.message); if (data.user && data.session) { const apiKey = this.generateApiKey(data.user.id); - console.log('[AuthService] Generated API key, storing...', { apiKey: apiKey.substring(0, 8) + '...' }); + console.log("[AuthService] Generated API key, storing...", { + apiKey: apiKey.substring(0, 8) + "...", + }); // TODO: Remove debug logging before production release // DEBUG: API key validation successful (key details redacted for security) await secureStorage.setItem("api_key", apiKey); - console.log('[AuthService] API key stored successfully'); - + console.log("[AuthService] API key stored successfully"); + // Register with MCP server (in case user signed up before this fix) - await this.registerApiKeyWithMCPServer(apiKey, data.user.id, data.user.email || ''); + await this.registerApiKeyWithMCPServer( + apiKey, + data.user.id, + data.user.email || "" + ); } return { user: data.user, session: data.session }; } catch (error) { - console.error('[AuthService] Sign in failed:', error); + console.error("[AuthService] Sign in failed:", error); return { user: null, session: null, error: error as Error }; } } @@ -163,13 +182,16 @@ class AuthService { async signOut() { // Clear local API key first (works offline) await secureStorage.removeItem("api_key"); - + // Then try Supabase signout (may fail if offline) try { const { error } = await supabase.auth.signOut(); if (error) throw new Error(error.message); } catch (error) { - console.log('[AuthService] Supabase signout failed (may be offline):', error); + console.log( + "[AuthService] Supabase signout failed (may be offline):", + error + ); // Don't throw - local cleanup is more important } } @@ -206,48 +228,58 @@ class AuthService { try { // First check if we have a valid session const session = await this.getCurrentSession(); - + if (session && session.user) { // We have an active session - verify with MCP server to ensure API key is still valid const apiKey = await secureStorage.getItem("api_key"); if (apiKey) { const mcpValid = await this.validateWithMCPServer(apiKey); if (mcpValid) { - console.log('[AuthService] User verification successful - active session + valid MCP'); + console.log( + "[AuthService] User verification successful - active session + valid MCP" + ); return { isValid: true, user: session.user }; } else { - console.log('[AuthService] User invalid - MCP server rejected API key (user likely deleted)'); + console.log( + "[AuthService] User invalid - MCP server rejected API key (user likely deleted)" + ); return { isValid: false }; } } } - + // No active session - check if API key is still valid with MCP server const apiKey = await secureStorage.getItem("api_key"); if (apiKey) { const mcpValid = await this.validateWithMCPServer(apiKey); if (mcpValid) { // API key is valid with MCP - allow offline mode (session just expired) - console.log('[AuthService] Session expired but API key valid, allowing offline mode'); + console.log( + "[AuthService] Session expired but API key valid, allowing offline mode" + ); return { isValid: true, isOffline: true }; } else { // API key rejected by MCP - user was deleted - console.log('[AuthService] User invalid - MCP server rejected API key (user deleted)'); + console.log( + "[AuthService] User invalid - MCP server rejected API key (user deleted)" + ); return { isValid: false }; } } - + // No API key stored - console.log('[AuthService] No API key stored'); + console.log("[AuthService] No API key stored"); return { isValid: false }; } catch (networkError) { // Network error - allow offline mode if we have an API key const apiKey = await secureStorage.getItem("api_key"); if (apiKey) { - console.log('[AuthService] Network error during verification, allowing offline mode'); + console.log( + "[AuthService] Network error during verification, allowing offline mode" + ); return { isValid: true, isOffline: true }; } - console.log('[AuthService] Network error and no API key stored'); + console.log("[AuthService] Network error and no API key stored"); return { isValid: false }; } } @@ -256,43 +288,55 @@ class AuthService { try { // Make a simple health check call to MCP server with the API key const response = await fetch(`${this.currentUrl}/ai/health`, { - method: 'GET', + method: "GET", headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", }, }); // 200 = valid user, 401 = invalid user, anything else = network issue if (response.status === 200) { - console.log('[AuthService] MCP validation successful'); + console.log("[AuthService] MCP validation successful"); return true; } else if (response.status === 401) { - console.log('[AuthService] MCP validation failed - 401 Unauthorized'); + console.log("[AuthService] MCP validation failed - 401 Unauthorized"); return false; } else { - console.log('[AuthService] MCP validation unclear - status:', response.status); + console.log( + "[AuthService] MCP validation unclear - status:", + response.status + ); // TODO: Implement proper error handling for ambiguous HTTP status codes return true; // Assume valid on unclear responses to avoid false logouts } } catch (error) { - console.log('[AuthService] MCP validation failed due to network error:', error); + console.log( + "[AuthService] MCP validation failed due to network error:", + error + ); return true; // Network error - assume valid for offline mode } } async getApiKey() { try { - console.log('[AuthService] getApiKey called'); - + console.log("[AuthService] getApiKey called"); + // Get API key from SecureStorage const apiKey = await secureStorage.getItem("api_key"); - console.log('[AuthService] Retrieved API key from SecureStorage:', { hasApiKey: !!apiKey, apiKeyLength: apiKey?.length }); - - console.log('[AuthService] Returning API key:', { hasApiKey: !!apiKey, apiKeyLength: apiKey?.length }); + console.log("[AuthService] Retrieved API key from SecureStorage:", { + hasApiKey: !!apiKey, + apiKeyLength: apiKey?.length, + }); + + console.log("[AuthService] Returning API key:", { + hasApiKey: !!apiKey, + apiKeyLength: apiKey?.length, + }); return apiKey; } catch (error) { - console.error('[AuthService] getApiKey failed:', error); + console.error("[AuthService] getApiKey failed:", error); return null; } } diff --git a/apps/mobile/src/services/mcpService.ts b/apps/mobile/src/services/mcpService.ts index 0d66699..3996c94 100644 --- a/apps/mobile/src/services/mcpService.ts +++ b/apps/mobile/src/services/mcpService.ts @@ -1,7 +1,7 @@ -import { authService } from './authService'; +import { authService } from "./authService"; interface MCPRequest { - jsonrpc: '2.0'; + jsonrpc: "2.0"; id: number; method: string; params?: any; @@ -9,7 +9,7 @@ interface MCPRequest { /** * MCP Service for Tides Mobile App - * + * * IMPORTANT: MCP Server Response Format * The server returns tool results wrapped in MCP protocol format: * { @@ -19,13 +19,13 @@ interface MCPRequest { * "jsonrpc": "2.0", * "id": 1 * } - * + * * The actual data (success, tides, etc.) is JSON-stringified inside result.content[0].text * and must be parsed to get the real response structure that the app expects. */ class MCPService { private requestId = 0; - private baseUrl = ''; + private baseUrl = ""; private urlProvider: (() => string) | null = null; /** @@ -38,52 +38,55 @@ class MCPService { async getConnectionStatus() { const apiKey = await authService.getApiKey(); - + if (!apiKey || !this.baseUrl) { return { isConnected: false, hasApiKey: !!apiKey }; } // Validate API key format (should be tides_userId_randomId format) const isValidFormat = apiKey.match(/^tides_[a-f0-9-]{36}_[a-z0-9]{6}$/i); - + if (!isValidFormat) { - console.error('[MCPService] Invalid auth token format:', { + console.error("[MCPService] Invalid auth token format:", { tokenLength: apiKey.length, - tokenPrefix: apiKey.substring(0, 12) + '...', - expectedFormat: 'tides_userId_randomId' + tokenPrefix: apiKey.substring(0, 12) + "...", + expectedFormat: "tides_userId_randomId", }); return { isConnected: false, hasApiKey: false }; } // Simple connectivity test with API key // TODO: Remove debug logging before production release - console.log('[DEBUG] MCP Health Check Details:', { + console.log("[DEBUG] MCP Health Check Details:", { url: `${this.baseUrl}/ai/health`, apiKey: apiKey, tokenLength: apiKey.length, - tokenFormat: apiKey.substring(0, 15) + '...' + apiKey.substring(apiKey.length - 10), - startsWithTides: apiKey.startsWith('tides_'), - isValidFormat: !!apiKey.match(/^tides_[a-f0-9-]{36}_[a-z0-9]{6}$/i) + tokenFormat: + apiKey.substring(0, 15) + "..." + apiKey.substring(apiKey.length - 10), + startsWithTides: apiKey.startsWith("tides_"), + isValidFormat: !!apiKey.match(/^tides_[a-f0-9-]{36}_[a-z0-9]{6}$/i), }); - + try { const response = await fetch(`${this.baseUrl}/ai/health`, { - method: 'GET', + method: "GET", headers: { - 'Accept': 'application/json', - 'Authorization': `Bearer ${apiKey}` - } + Accept: "application/json", + Authorization: `Bearer ${apiKey}`, + }, }); - console.log(`[MCPService] Health check status: ${response.status} with API key`); - + console.log( + `[MCPService] Health check status: ${response.status} with API key` + ); + if (response.status === 401) { const responseText = await response.text(); // TODO: Replace debug logging with proper error analytics - console.log('[DEBUG] 401 Response details:', { + console.log("[DEBUG] 401 Response details:", { status: response.status, statusText: response.statusText, responseBody: responseText, - headers: Object.fromEntries(response.headers.entries()) + headers: Object.fromEntries(response.headers.entries()), }); } return { isConnected: response.ok, hasApiKey: !!apiKey }; @@ -96,11 +99,6 @@ class MCPService { } } - async updateServerUrl(url: string) { - this.baseUrl = url; - await authService.setWorkerUrl(url); - } - /** * Get current server URL from provider or fallback */ @@ -108,37 +106,40 @@ class MCPService { if (this.urlProvider) { return this.urlProvider(); } - return this.baseUrl || 'https://tides-001.mpazbot.workers.dev'; + // ENV: Change for production - currently using tides-006 + return this.baseUrl || "https://tides-006.mpazbot.workers.dev"; } private async request(method: string, params?: any) { const apiKey = await authService.getApiKey(); - if (!apiKey) throw new Error('No API key'); - + if (!apiKey) throw new Error("No API key"); + // Validate API key format before making requests const isValidFormat = apiKey.match(/^tides_[a-f0-9-]{36}_[a-z0-9]{6}$/i); - + if (!isValidFormat) { - throw new Error('Invalid API key format - expected tides_userId_randomId'); + throw new Error( + "Invalid API key format - expected tides_userId_randomId" + ); } - + const currentUrl = this.getCurrentUrl(); if (!currentUrl) { - throw new Error('MCP server URL not configured'); + throw new Error("MCP server URL not configured"); } const body: MCPRequest = { - jsonrpc: '2.0', + jsonrpc: "2.0", id: ++this.requestId, method, - params + params, }; console.log(`[MCPService] Request to ${currentUrl}/mcp:`, { method, params, - apiKeyPrefix: apiKey.substring(0, 10) + '...', - baseUrl: currentUrl + apiKeyPrefix: apiKey.substring(0, 10) + "...", + baseUrl: currentUrl, }); // Retry logic for React Native network issues @@ -148,94 +149,114 @@ class MCPService { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { console.log(`[MCPService] Attempt ${attempt}/${maxRetries}`); - + // Add timeout and User-Agent for React Native compatibility const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout const response = await fetch(`${currentUrl}/mcp`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/event-stream', - 'Authorization': `Bearer ${apiKey}`, - 'User-Agent': 'TidesMobile/1.0 React-Native/0.80.2' + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + Authorization: `Bearer ${apiKey}`, + "User-Agent": "TidesMobile/1.0 React-Native/0.80.2", }, body: JSON.stringify(body), - signal: controller.signal + signal: controller.signal, }); clearTimeout(timeoutId); - + // If we get here, the request succeeded console.log(`[MCPService] Request succeeded on attempt ${attempt}`); return await this.handleResponse(response); - } catch (networkError: unknown) { lastError = networkError; - const errorMessage = networkError instanceof Error ? networkError.message : 'Unknown network error'; - + const errorMessage = + networkError instanceof Error + ? networkError.message + : "Unknown network error"; + console.error(`[MCPService] Attempt ${attempt} failed:`, errorMessage); - + if (attempt === maxRetries) { - console.error(`[MCPService] All ${maxRetries} attempts failed. Final error:`, { - name: networkError instanceof Error ? networkError.name : 'Unknown', - message: errorMessage, - stack: networkError instanceof Error ? networkError.stack : undefined, - url: `${currentUrl}/mcp` - }); + console.error( + `[MCPService] All ${maxRetries} attempts failed. Final error:`, + { + name: + networkError instanceof Error ? networkError.name : "Unknown", + message: errorMessage, + stack: + networkError instanceof Error ? networkError.stack : undefined, + url: `${currentUrl}/mcp`, + } + ); break; } - + // Wait before retrying (exponential backoff) const delay = Math.pow(2, attempt - 1) * 1000; // 1s, 2s, 4s console.log(`[MCPService] Waiting ${delay}ms before retry...`); - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise((resolve) => setTimeout(resolve, delay)); } } // If we get here, all retries failed - const errorMessage = lastError instanceof Error ? lastError.message : 'Unknown network error'; - throw new Error(`Network request failed after ${maxRetries} attempts: ${errorMessage}`); + const errorMessage = + lastError instanceof Error ? lastError.message : "Unknown network error"; + throw new Error( + `Network request failed after ${maxRetries} attempts: ${errorMessage}` + ); } private async handleResponse(response: Response) { - console.log(`[MCPService] Response status: ${response.status} ${response.statusText}`); + console.log( + `[MCPService] Response status: ${response.status} ${response.statusText}` + ); const headers = Object.fromEntries(response.headers.entries()); console.log(`[MCPService] Response headers:`, headers); - console.log(`[MCPService] Content-Type specifically:`, response.headers.get('content-type')); + console.log( + `[MCPService] Content-Type specifically:`, + response.headers.get("content-type") + ); if (!response.ok) { - const contentType = response.headers.get('content-type'); + const contentType = response.headers.get("content-type"); console.log(`[MCPService] Error response content-type:`, contentType); - - if (contentType?.includes('application/json')) { + + if (contentType?.includes("application/json")) { const errorData = await response.json(); console.log(`[MCPService] JSON error data:`, errorData); - throw new Error(errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`); + throw new Error( + errorData.error?.message || + `HTTP ${response.status}: ${response.statusText}` + ); } else { const errorText = await response.text(); console.log(`[MCPService] Plain text error:`, errorText); - throw new Error(`HTTP ${response.status}: ${errorText || response.statusText}`); + throw new Error( + `HTTP ${response.status}: ${errorText || response.statusText}` + ); } } const responseText = await response.text(); - const contentType = response.headers.get('content-type') || ''; + const contentType = response.headers.get("content-type") || ""; console.log(`[MCPService] Response content-type: ${contentType}`); console.log(`[MCPService] Raw response:`, responseText); try { let jsonData; - + // Parse based on actual content type returned by server - if (contentType.includes('text/event-stream')) { + if (contentType.includes("text/event-stream")) { console.log(`[MCPService] Parsing as Server-Sent Events`); const jsonMatch = responseText.match(/^data: (.+)$/m); if (jsonMatch) { jsonData = JSON.parse(jsonMatch[1]); } else { - throw new Error('No data field found in SSE response'); + throw new Error("No data field found in SSE response"); } } else { console.log(`[MCPService] Parsing as standard JSON`); @@ -244,131 +265,107 @@ class MCPService { console.log(`[MCPService] Parsed response:`, jsonData); if (jsonData.error) throw new Error(jsonData.error.message); - + // Handle nested JSON response format from MCP tools if (jsonData.result?.content?.[0]?.text) { const innerData = JSON.parse(jsonData.result.content[0].text); console.log(`[MCPService] Extracted inner data:`, innerData); return innerData; } - + return jsonData.result; } catch (parseError) { console.error(`[MCPService] JSON parse error:`, parseError); - console.error(`[MCPService] Response that failed to parse:`, responseText); - throw new Error(`Failed to parse response: ${responseText.substring(0, 100)}...`); + console.error( + `[MCPService] Response that failed to parse:`, + responseText + ); + throw new Error( + `Failed to parse response: ${responseText.substring(0, 100)}...` + ); } } tool(name: string, args?: any) { - return this.request('tools/call', { name, arguments: args || {} }); + return this.request("tools/call", { name, arguments: args || {} }); + } + + async createTide(name: string, description?: string) { + return this.tool("tide_create", { name, description }); } - async createTide(name: string, description?: string, flowType?: string) { - return this.tool('tide_create', { name, description, flow_type: flowType }); + async getOrCreateTide(timezone?: string) { + return this.tool("tide_get_or_create", { + timezone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, + }); } async listTides() { - return this.tool('tide_list', {}); + return this.tool("tide_list", {}); } async addEnergyToTide(tideId: string, energyLevel: string, context?: string) { - return this.tool('tide_add_energy', { tide_id: tideId, energy_level: energyLevel, context }); + return this.tool("tide_add_energy", { + tide_id: tideId, + energy_level: energyLevel, + context, + }); } - async startTideFlow(tideId: string, intensity?: string, duration?: number, initialEnergy?: string, workContext?: string) { - return this.tool('tide_flow', { tide_id: tideId, intensity, duration, initial_energy: initialEnergy, work_context: workContext }); + async startTideFlow( + tideId: string, + intensity?: string, + duration?: number, + initialEnergy?: string, + workContext?: string + ) { + return this.tool("tide_flow", { + tide_id: tideId, + intensity, + duration, + initial_energy: initialEnergy, + work_context: workContext, + }); } async getTideReport(tideId: string, format?: string) { - return this.tool('tide_get_report', { tide_id: tideId, format }); - } - - async linkTaskToTide(tideId: string, taskUrl: string, taskTitle: string, taskType?: string) { - return this.tool('tide_link_task', { tide_id: tideId, task_url: taskUrl, task_title: taskTitle, task_type: taskType }); + return this.tool("tide_get_report", { tide_id: tideId, format }); } - async listTaskLinks(tideId: string) { - return this.tool('tide_list_task_links', { tide_id: tideId }); - } - - async getTideParticipants(statusFilter?: string, dateFrom?: string, dateTo?: string, limit?: number) { - return this.tool('tides_get_participants', { status_filter: statusFilter, date_from: dateFrom, date_to: dateTo, limit }); - } - - /** - * Smart Flow - Always uses hierarchical flow since hierarchical tides always exist - * Combines the best of tide_flow and tide_start_hierarchical_flow - * Now supports context-aware execution with optional contextTideId - */ - async startSmartFlow(intensity?: string, duration?: number, workContext?: string, contextTideId?: string) { - // Get time of day for smart defaults - const hour = new Date().getHours(); - const timeBasedContext = - hour < 12 ? 'morning planning' : - hour < 17 ? 'afternoon focus' : - 'evening deep work'; - - const params: any = { - intensity: intensity || 'moderate', - duration_minutes: duration || 25, - work_context: workContext || timeBasedContext, - }; - - // Add context tide if provided - if (contextTideId) { - params.context_tide_id = contextTideId; - } - - return this.tool('tide_start_hierarchical_flow', params); - } - - /** - * Hierarchical Context Management Methods - * These methods align with the hierarchical tide system - */ - - async getOrCreateDailyTide(timezone?: string) { - return this.tool('tide_get_or_create_daily', { - timezone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone + async linkTaskToTide( + tideId: string, + taskUrl: string, + taskTitle: string, + taskType?: string + ) { + return this.tool("tide_link_task", { + tide_id: tideId, + task_url: taskUrl, + task_title: taskTitle, + task_type: taskType, }); } - async switchContext(contextType: 'daily' | 'weekly' | 'monthly' | 'project', date?: string) { - return this.tool('tide_switch_context', { - context_type: contextType, - date: date || new Date().toISOString().split('T')[0], - }); - } - - async listContexts(date?: string, includeEmpty = true) { - return this.tool('tide_list_contexts', { - date: date || new Date().toISOString().split('T')[0], - include_empty: includeEmpty, - }); + async listTaskLinks(tideId: string) { + return this.tool("tide_list_task_links", { tide_id: tideId }); } - async getTodaysSummary(date?: string) { - return this.tool('tide_get_todays_summary', { - date: date || new Date().toISOString().split('T')[0], + async getTideParticipants( + statusFilter?: string, + dateFrom?: string, + dateTo?: string, + limit?: number + ) { + return this.tool("tides_get_participants", { + status_filter: statusFilter, + date_from: dateFrom, + date_to: dateTo, + limit, }); } async getRawTideJson(tideId: string) { - return this.tool('tide_get_raw_json', { tide_id: tideId }); - } - - - /** - * Context-aware energy addition - */ - async addEnergyToContext(contextTideId: string, energyLevel: string, context?: string) { - return this.tool('tide_add_energy', { - tide_id: contextTideId, // Use context tide - energy_level: energyLevel, - context: context || `Energy added at ${new Date().toLocaleTimeString()}`, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }); + return this.tool("tide_get_raw_json", { tide_id: tideId }); } /** @@ -380,4 +377,4 @@ class MCPService { } } -export const mcpService = new MCPService(); \ No newline at end of file +export const mcpService = new MCPService(); diff --git a/apps/mobile/src/services/phraseDetectionService.ts b/apps/mobile/src/services/phraseDetectionService.ts deleted file mode 100644 index c0acb59..0000000 --- a/apps/mobile/src/services/phraseDetectionService.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { - TOOL_PHRASES, - TOOL_METADATA, - CONFIDENCE_THRESHOLD, - DetectedTool, - ToolPhrase, -} from "../config/toolPhrases"; -import { loggingService } from "./loggingService"; - -interface DetectionCache { - input: string; - result: DetectedTool | null; - timestamp: number; -} - -class PhraseDetectionService { - private static instance: PhraseDetectionService; - private cache: Map = new Map(); - private readonly CACHE_TTL = 5000; // 5 seconds - private readonly MAX_CACHE_SIZE = 50; - - private constructor() {} - - static getInstance(): PhraseDetectionService { - if (!PhraseDetectionService.instance) { - PhraseDetectionService.instance = new PhraseDetectionService(); - } - return PhraseDetectionService.instance; - } - - /** - * Detect tool intent from user input - */ - detectToolIntent(input: string): DetectedTool | null { - if (!input || input.trim().length < 3) { - return null; - } - - const normalizedInput = input.trim().toLowerCase(); - - // Check cache first - const cached = this.getCached(normalizedInput); - if (cached !== undefined) { - return cached; - } - - // Find matching patterns - const detectedTools: DetectedTool[] = []; - - for (const phrase of TOOL_PHRASES) { - for (const pattern of phrase.patterns) { - const match = input.match(pattern); - if (match) { - const metadata = TOOL_METADATA[phrase.toolId]; - if (!metadata) continue; - - // Calculate confidence based on match quality - const confidence = this.calculateConfidence(input, match[0], phrase); - - if (confidence >= CONFIDENCE_THRESHOLD) { - const extractedParams = phrase.extractParams ? phrase.extractParams(match) : {}; - - detectedTools.push({ - toolId: phrase.toolId, - metadata, - confidence, - extractedParams, - matchedPattern: pattern.source, - }); - - // Break after first pattern match for this tool - break; - } - } - } - } - - // Sort by confidence and priority - detectedTools.sort((a, b) => { - // First by confidence - if (b.confidence !== a.confidence) { - return b.confidence - a.confidence; - } - // Then by priority (from phrase config) - const aPriority = TOOL_PHRASES.find(p => p.toolId === a.toolId)?.priority || 0; - const bPriority = TOOL_PHRASES.find(p => p.toolId === b.toolId)?.priority || 0; - return bPriority - aPriority; - }); - - const result = detectedTools.length > 0 ? detectedTools[0] : null; - - // Cache the result - this.cacheResult(normalizedInput, result); - - if (result) { - loggingService.info("PhraseDetection", "Tool intent detected", { - input: input.substring(0, 50), - toolId: result.toolId, - confidence: result.confidence, - extractedParams: result.extractedParams, - }); - } - - return result; - } - - /** - * Calculate confidence score for a match - */ - private calculateConfidence(input: string, matchedText: string, phrase: ToolPhrase): number { - const normalizedInput = input.trim().toLowerCase(); - const normalizedMatch = matchedText.toLowerCase(); - - // Base confidence from match coverage - const coverage = normalizedMatch.length / normalizedInput.length; - let confidence = Math.min(coverage, 1.0); - - // Boost if match is at the beginning - if (normalizedInput.startsWith(normalizedMatch)) { - confidence += 0.1; - } - - // Boost for exact match - if (normalizedInput === normalizedMatch) { - confidence = 1.0; - } - - // Slight penalty for very short inputs (might be incomplete) - if (normalizedInput.length < 10) { - confidence *= 0.9; - } - - // Apply priority weight - const priorityBoost = (phrase.priority || 5) / 20; // 0 to 0.5 boost - confidence = Math.min(confidence + priorityBoost, 1.0); - - return confidence; - } - - /** - * Get similar tools for fuzzy matching - */ - getSimilarTools(input: string, threshold: number = 0.5): DetectedTool[] { - if (!input || input.trim().length < 3) { - return []; - } - - const normalizedInput = input.trim().toLowerCase(); - const detectedTools: DetectedTool[] = []; - - // Check each tool's name and keywords - for (const [toolId, metadata] of Object.entries(TOOL_METADATA)) { - const toolName = metadata.name.toLowerCase(); - const toolDesc = metadata.description.toLowerCase(); - - // Simple fuzzy matching based on containment - let confidence = 0; - - // Check if input contains tool name or vice versa - if (normalizedInput.includes(toolName) || toolName.includes(normalizedInput)) { - confidence = 0.6; - } - - // Check individual words - const inputWords = normalizedInput.split(/\s+/); - const toolWords = toolName.split(/\s+/); - - for (const inputWord of inputWords) { - for (const toolWord of toolWords) { - if (inputWord.length > 3 && toolWord.includes(inputWord)) { - confidence = Math.max(confidence, 0.5); - } - if (toolWord.length > 3 && inputWord.includes(toolWord)) { - confidence = Math.max(confidence, 0.5); - } - } - } - - // Check description - if (toolDesc.includes(normalizedInput)) { - confidence = Math.max(confidence, 0.4); - } - - if (confidence >= threshold) { - detectedTools.push({ - toolId, - metadata, - confidence, - }); - } - } - - return detectedTools.sort((a, b) => b.confidence - a.confidence); - } - - /** - * Check if input is likely a command (vs regular conversation) - */ - isLikelyCommand(input: string): boolean { - const commandIndicators = [ - /^(create|make|start|add|show|list|view|get|generate|analyze|link|connect)/i, - /^(my\s+)?(tide|flow|energy|task|report|insights|recommendations)/i, - /^(refresh|update|record|track)/i, - ]; - - return commandIndicators.some(pattern => pattern.test(input.trim())); - } - - /** - * Cache management - */ - private getCached(input: string): DetectedTool | null | undefined { - const cached = this.cache.get(input); - if (!cached) return undefined; - - const now = Date.now(); - if (now - cached.timestamp > this.CACHE_TTL) { - this.cache.delete(input); - return undefined; - } - - return cached.result; - } - - private cacheResult(input: string, result: DetectedTool | null): void { - // Manage cache size - if (this.cache.size >= this.MAX_CACHE_SIZE) { - const oldestKey = this.cache.keys().next().value; - if (oldestKey) { - this.cache.delete(oldestKey); - } - } - - this.cache.set(input, { - input, - result, - timestamp: Date.now(), - }); - } - - /** - * Clear the detection cache - */ - clearCache(): void { - this.cache.clear(); - } -} - -export const phraseDetectionService = PhraseDetectionService.getInstance(); \ No newline at end of file diff --git a/apps/mobile/src/services/secureStorage.ts b/apps/mobile/src/services/secureStorage.ts index 5e0f601..9f1713e 100644 --- a/apps/mobile/src/services/secureStorage.ts +++ b/apps/mobile/src/services/secureStorage.ts @@ -14,7 +14,9 @@ class SecureStorage { async getItem(key: string) { try { const credentials = await Keychain.getInternetCredentials(this.service); - return credentials && credentials.username === key ? credentials.password : null; + return credentials && credentials.username === key + ? credentials.password + : null; } catch { return null; } @@ -29,4 +31,4 @@ class SecureStorage { } } -export const secureStorage = new SecureStorage(); \ No newline at end of file +export const secureStorage = new SecureStorage(); diff --git a/apps/mobile/src/types/agents.ts b/apps/mobile/src/types/agents.ts deleted file mode 100644 index 813f59b..0000000 --- a/apps/mobile/src/types/agents.ts +++ /dev/null @@ -1,355 +0,0 @@ -/** - * Enhanced Agent Service Types - * - * Comprehensive type definitions for the reliable agent endpoint connection system. - * Extends existing chat types with advanced connection management capabilities. - */ - -// ======================== Core Agent Types ======================== - -export interface AgentMessage { - id: string; - type: "request" | "response" | "thinking" | "tool_call" | "system"; - content: string; - timestamp: Date; - agentId?: string; - userId?: string; - toolCalls?: ToolCall[]; - thinking?: boolean; - metadata?: AgentMessageMetadata; -} - -export interface AgentMessageMetadata { - conversationId?: string; - parentMessageId?: string; - connectionId?: string; - processingTime?: number; - retryCount?: number; - fallbackUsed?: boolean; - error?: { - code: string; - message: string; - recoverable: boolean; - }; -} - -export interface ToolCall { - id: string; - name: string; - parameters: Record; - status: "pending" | "executing" | "completed" | "failed"; - result?: any; - error?: string; - executionTime?: number; -} - -// ======================== Connection Management ======================== - -export type ConnectionState = - | "disconnected" - | "connecting" - | "connected" - | "reconnecting" - | "degraded" - | "failed"; - -export interface ConnectionStatus { - state: ConnectionState; - connectionId: string; - endpoint: string; - lastConnected?: Date; - lastError?: Date; - errorCount: number; - latency?: number; - isHealthy: boolean; - metadata: { - uptime?: number; - reconnectionAttempts: number; - lastHealthCheck: Date; - capabilities?: string[]; - }; -} - -export interface ConnectionPool { - primary: ConnectionStatus; - fallbacks: ConnectionStatus[]; - activeConnection: string; - healthyConnections: string[]; - totalConnections: number; -} - -// ======================== Health Monitoring ======================== - -export interface HealthMetrics { - availability: number; // 0-1 percentage - responseTime: number; // milliseconds - errorRate: number; // 0-1 percentage - throughput: number; // requests per second - lastUpdated: Date; - trends: { - availability7d: number; - responseTime7d: number; - errorRate24h: number; - }; -} - -export interface HealthCheck { - id: string; - endpoint: string; - status: "healthy" | "unhealthy" | "degraded"; - responseTime: number; - timestamp: Date; - details?: { - httpStatus?: number; - errorMessage?: string; - agentVersion?: string; - capabilities?: string[]; - }; -} - -// ======================== Circuit Breaker ======================== - -export type CircuitBreakerState = "closed" | "open" | "half-open"; - -export interface CircuitBreakerConfig { - failureThreshold: number; - recoveryTimeout: number; - monitoringPeriod: number; - minimumThroughput: number; -} - -export interface CircuitBreakerMetrics { - state: CircuitBreakerState; - failureCount: number; - successCount: number; - lastFailureTime?: Date; - lastSuccessTime?: Date; - nextAttemptTime?: Date; -} - -// ======================== Request Management ======================== - -export interface QueuedRequest { - id: string; - method: "POST" | "GET" | "PUT" | "DELETE"; - endpoint: string; - payload?: any; - headers?: Record; - priority: "low" | "normal" | "high" | "critical"; - timestamp: Date; - retryCount: number; - maxRetries: number; - callback?: (result: any, error?: Error) => void; -} - -export interface RequestQueueMetrics { - queueSize: number; - processedToday: number; - failedToday: number; - averageProcessingTime: number; - oldestRequest?: Date; -} - -// ======================== Natural Language Processing ======================== - -export interface ParsedCommand { - intent: CommandIntent; - confidence: number; - parameters: Record; - originalText: string; - alternatives?: ParsedCommand[]; -} - -export type CommandIntent = - | "create_tide" - | "list_tides" - | "start_flow" - | "add_energy" - | "get_report" - | "link_task" - | "get_insights" - | "optimize_tide" - | "question" - | "unknown"; - -export interface IntentPattern { - intent: CommandIntent; - patterns: RegExp[]; - requiredParams: string[]; - optionalParams: string[]; - examples: string[]; -} - -// ======================== Configuration ======================== - -export interface AgentServiceConfig { - // Connection settings - primaryEndpoint: string; - fallbackEndpoints?: string[]; - webSocketEndpoint?: string; - - // Timeout and retry settings - timeoutMs: number; - retryAttempts: number; - retryDelay: number; - - // Connection pool settings - maxConnections: number; - connectionTimeout: number; - keepAliveInterval: number; - - // Health monitoring - healthCheckInterval: number; - healthCheckTimeout: number; - degradationThreshold: number; - - // Circuit breaker settings - circuitBreaker: CircuitBreakerConfig; - - // Queue settings - maxQueueSize: number; - queuePersistence: boolean; - queueProcessingInterval: number; - - // Feature flags - enableWebSocket: boolean; - enableConnectionPooling: boolean; - enableRequestQueuing: boolean; - enableFallbacks: boolean; - enableNLParsing: boolean; -} - -// ======================== Service Responses ======================== - -export interface AgentResponse { - success: boolean; - data?: T; - error?: { - code: string; - message: string; - details?: any; - recoverable: boolean; - }; - metadata?: { - processingTime: number; - connectionId: string; - fallbackUsed: boolean; - queuePosition?: number; - }; -} - -export interface AgentStatus { - status: "healthy" | "degraded" | "unhealthy"; - agentId: string; - version: string; - uptime: number; - connectedClients: number; - capabilities: string[]; - performance: { - averageResponseTime: number; - requestsPerMinute: number; - errorRate: number; - }; - timestamp: Date; -} - -// ======================== Event Types ======================== - -export type AgentEvent = - | "connection_established" - | "connection_lost" - | "connection_degraded" - | "connection_recovered" - | "message_received" - | "message_sent" - | "health_check_passed" - | "health_check_failed" - | "circuit_breaker_opened" - | "circuit_breaker_closed" - | "fallback_activated" - | "queue_full" - | "request_queued" - | "request_processed"; - -export interface AgentEventData { - event: AgentEvent; - timestamp: Date; - connectionId?: string; - details?: any; - error?: Error; -} - -// ======================== Fallback Strategy ======================== - -export interface FallbackOption { - type: "mcp_direct" | "cached_response" | "default_message" | "offline_queue"; - priority: number; - enabled: boolean; - config?: any; -} - -export interface FallbackResult { - success: boolean; - source: "mcp" | "cache" | "default" | "queue"; - data?: any; - message?: string; - limitations?: string[]; -} - -// ======================== Cache Management ======================== - -export interface CachedResponse { - key: string; - data: any; - timestamp: Date; - expiresAt: Date; - hits: number; - source: string; -} - -export interface CacheMetrics { - hitRate: number; - totalSize: number; - evictionCount: number; - oldestEntry?: Date; -} - -// ======================== Export Types ======================== - -// Re-export for backward compatibility -export type { - AgentServiceConfig as LegacyAgentServiceConfig, - AgentMessage as LegacyAgentMessage -} from './chat'; - -// Main exports -export type EnhancedAgentService = { - // Connection management - getConnectionStatus(): ConnectionPool; - getHealthMetrics(): HealthMetrics; - testConnection(endpoint?: string): Promise; - - // Message handling with reliability - sendMessage(message: string, options?: { - timeout?: number; - priority?: "low" | "normal" | "high"; - fallbackAllowed?: boolean; - }): Promise>; - - // Natural language processing - parseCommand(text: string): ParsedCommand; - executeCommand(command: ParsedCommand): Promise; - - // Queue management - getQueueMetrics(): RequestQueueMetrics; - clearQueue(): Promise; - retryFailedRequests(): Promise; - - // Configuration - updateConfig(config: Partial): void; - getConfig(): AgentServiceConfig; - - // Event handling - on(event: AgentEvent, callback: (data: AgentEventData) => void): () => void; - off(event: AgentEvent, callback: (data: AgentEventData) => void): void; -}; \ No newline at end of file diff --git a/apps/mobile/src/types/api.ts b/apps/mobile/src/types/api.ts index 6a00e9c..0751e80 100644 --- a/apps/mobile/src/types/api.ts +++ b/apps/mobile/src/types/api.ts @@ -7,7 +7,6 @@ import type { TideReport, FlowIntensity, EnergyLevel, - FlowType, TideStatus } from './models'; @@ -22,7 +21,6 @@ export interface BaseResponse { export interface TideCreateResponse extends BaseResponse { tide_id?: string; name?: string; - flow_type?: FlowType; created_at?: string; status?: TideStatus; description?: string; diff --git a/apps/mobile/src/types/chat.ts b/apps/mobile/src/types/chat.ts deleted file mode 100644 index d970a7f..0000000 --- a/apps/mobile/src/types/chat.ts +++ /dev/null @@ -1,121 +0,0 @@ -export interface ChatMessage { - id: string; - type: "user" | "assistant" | "system" | "tool_result"; - content: string; - timestamp: Date; - metadata?: ChatMessageMetadata; -} - -export interface ChatMessageMetadata { - toolName?: string; - toolResult?: any; - agentThinking?: boolean; - error?: boolean; - conversationId?: string; - userId?: string; - agentResponse?: boolean; - agentId?: string; - responseType?: string; - isAgentMessage?: boolean; - suggestedTools?: string[]; - toolSuggestion?: { - name: string; - parameters: Record; - confidence: number; - }; - fallbackResponse?: boolean; - helpCommand?: boolean; -} - -export interface MCPToolCall { - id: string; - name: string; - parameters: Record; - timestamp: Date; - status: "pending" | "executing" | "completed" | "failed"; - result?: any; - error?: string; -} - -export interface AgentMessage { - id: string; - type: "request" | "response" | "status"; - content: string; - timestamp: Date; - agentId?: string; - toolCalls?: MCPToolCall[]; - thinking?: boolean; -} - -export interface ConversationContext { - userId: string; - sessionId: string; - activeConversationId: string; - currentTideId?: string; - mcpConnectionStatus: boolean; - agentConnectionStatus: boolean; -} - -export interface ChatState { - messages: ChatMessage[]; - isLoading: boolean; - error: string | null; - conversationContext: ConversationContext; - pendingToolCalls: MCPToolCall[]; - agentStatus: "idle" | "thinking" | "executing" | "responding"; - connectionStatus: { - mcp: boolean; - agent: boolean; - }; -} - -export type ChatAction = - | { type: "ADD_MESSAGE"; payload: ChatMessage } - | { type: "SET_LOADING"; payload: boolean } - | { type: "SET_ERROR"; payload: string | null } - | { type: "SET_AGENT_STATUS"; payload: ChatState["agentStatus"] } - | { type: "ADD_TOOL_CALL"; payload: MCPToolCall } - | { - type: "UPDATE_TOOL_CALL"; - payload: { id: string; updates: Partial }; - } - | { type: "SET_CONNECTION_STATUS"; payload: { mcp: boolean; agent: boolean } } - | { type: "CLEAR_MESSAGES" } - | { type: "SET_CONVERSATION_CONTEXT"; payload: Partial } - | { type: "RESET_CHAT" }; - -export interface AvailableMCPTool { - name: string; - description: string; - parameters: { - name: string; - type: string; - required: boolean; - description: string; - }[]; -} - -export interface AgentServiceConfig { - agentEndpoint: string; - webSocketEndpoint?: string; - retryAttempts: number; - timeoutMs: number; -} - -export interface MessageInputProps { - onSendMessage: (message: string) => void; - onExecuteTool: (toolName: string, parameters: any) => void; - isLoading?: boolean; - availableTools?: AvailableMCPTool[]; -} - -export interface MessageBubbleProps { - message: ChatMessage; - isOwnMessage: boolean; -} - -export interface ToolExecutionProps { - toolCall: MCPToolCall; - onRetry?: () => void; - onCancel?: () => void; -} diff --git a/apps/mobile/src/types/mcp.ts b/apps/mobile/src/types/mcp.ts index 361fae8..09abd6c 100644 --- a/apps/mobile/src/types/mcp.ts +++ b/apps/mobile/src/types/mcp.ts @@ -54,14 +54,12 @@ export enum MCPErrorCodes { // MCP method parameter types export interface TideCreateParams { name: string; - flow_type: 'daily' | 'weekly' | 'project' | 'seasonal'; description?: string; initial_energy?: 'low' | 'medium' | 'high'; } export interface TideListParams { status?: 'active' | 'completed' | 'paused'; - flow_type?: 'daily' | 'weekly' | 'project' | 'seasonal'; limit?: number; } diff --git a/apps/mobile/src/types/models.ts b/apps/mobile/src/types/models.ts index 158ae0e..c3d764a 100644 --- a/apps/mobile/src/types/models.ts +++ b/apps/mobile/src/types/models.ts @@ -15,27 +15,20 @@ export interface Tide { id: string; name: string; status: TideStatus; - flow_type: FlowType; description?: string; energy_level?: number; flow_count?: number; last_flow?: string | null; created_at: string; updated_at: string; - // Hierarchical tide fields - parent_tide_id?: string | null; date_start?: string | null; // ISO date (YYYY-MM-DD) - date_end?: string | null; // ISO date (YYYY-MM-DD) + date_end?: string | null; // ISO date (YYYY-MM-DD) auto_created?: boolean; - // Computed hierarchical properties - children?: Tide[]; - parent?: Tide; } -export type TideStatus = 'active' | 'completed' | 'paused'; -export type FlowType = 'daily' | 'weekly' | 'monthly' | 'project' | 'seasonal'; -export type FlowIntensity = 'gentle' | 'moderate' | 'strong'; -export type EnergyLevel = 'low' | 'medium' | 'high' | 'completed'; +export type TideStatus = "active" | "completed" | "paused"; +export type FlowIntensity = "gentle" | "moderate" | "strong"; +export type EnergyLevel = "low" | "medium" | "high" | "completed"; // Flow session models export interface FlowSession { @@ -79,13 +72,12 @@ export interface Participant { created_at: string; } -export type ParticipantStatus = 'active' | 'inactive' | 'pending'; +export type ParticipantStatus = "active" | "inactive" | "pending"; // Report models export interface TideReport { tide_id: string; name: string; - flow_type: FlowType; created_at: string; total_flows: number; total_duration: number; @@ -101,4 +93,4 @@ export interface ApiResponse { data?: T; error?: string; message?: string; -} \ No newline at end of file +} diff --git a/apps/mobile/src/types/react-native-localize.d.ts b/apps/mobile/src/types/react-native-localize.d.ts new file mode 100644 index 0000000..37236f4 --- /dev/null +++ b/apps/mobile/src/types/react-native-localize.d.ts @@ -0,0 +1,39 @@ +declare module 'react-native-localize' { + export interface Locale { + languageCode: string; + scriptCode?: string; + countryCode: string; + languageTag: string; + isRTL: boolean; + } + + export interface Currency { + code: string; + symbol: string; + } + + export interface TemperatureUnit { + unit: 'celsius' | 'fahrenheit'; + } + + export interface Timezone { + timezone: string; + } + + export function getLocales(): Locale[]; + export function getCurrencies(): Currency[]; + export function getCountry(): string; + export function getCalendar(): string; + export function getTemperatureUnit(): TemperatureUnit; + export function getTimeZone(): string; + export function uses24HourClock(): boolean; + export function usesMetricSystem(): boolean; + export function usesAutoDateAndTime(): boolean; + export function usesAutoTimeZone(): boolean; + + export function findBestLanguageTag(languageTags: string[]): { languageTag: string; isRTL: boolean } | void; + export function findBestAvailableLanguage(languageTagsWithCountries: { [key: string]: T }): { languageTag: string; language: T } | void; + + export function addEventListener(type: 'change', handler: () => void): void; + export function removeEventListener(type: 'change', handler: () => void): void; +} \ No newline at end of file diff --git a/apps/mobile/src/utils/agentCommandUtils.ts b/apps/mobile/src/utils/agentCommandUtils.ts deleted file mode 100644 index 7eafba4..0000000 --- a/apps/mobile/src/utils/agentCommandUtils.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { loggingService } from "../services/loggingService"; - -type TideContext = 'daily' | 'weekly' | 'monthly'; - -interface ContextTide { - id: string; - name: string; - context: TideContext; - created_at: string; - status: 'active'; -} - -interface AgentCommandContext { - tideId?: string; - currentContextTide: ContextTide | null; - currentScreen: string; - isConnected: boolean; - currentServerUrl: string; - requestedAt: string; -} - -interface CreateAgentContextParams { - tideId?: string; - currentContextTide: ContextTide | null; - isConnected: boolean; - getCurrentServerUrl: () => string; -} - -export const createAgentContext = ({ - tideId, - currentContextTide, - isConnected, - getCurrentServerUrl, -}: CreateAgentContextParams): AgentCommandContext => { - return { - // Current tide context (if navigated from a specific tide) - ...(tideId && { tideId }), - - // Current context tide (daily/weekly/monthly - always available) - currentContextTide, - - // Current app state - currentScreen: "Home", - - // Connection state - isConnected, - currentServerUrl: getCurrentServerUrl(), - - // Timestamp for context - requestedAt: new Date().toISOString(), - }; -}; - -interface ExecuteAgentCommandParams { - command: string; - context: AgentCommandContext; - sendAgentMessage: (message: string, context: any) => Promise; - toggleToolMenu: () => void; -} - -export const executeAgentCommand = async ({ - command, - context, - sendAgentMessage, - toggleToolMenu, -}: ExecuteAgentCommandParams): Promise => { - toggleToolMenu(); // Close menu first - - try { - loggingService.info("ToolMenu", "Sending agent command with context", { - command, - contextKeys: Object.keys(context), - currentContext: context.currentContextTide?.context || 'none', - }); - - await sendAgentMessage(command, context); - - loggingService.info("ToolMenu", "Agent command executed from menu", { - command, - tideId: context.tideId, - contextProvided: true, - }); - } catch (agentError) { - loggingService.error( - "ToolMenu", - "Failed to execute agent command from menu", - { error: agentError, command, tideId: context.tideId } - ); - throw agentError; - } -}; \ No newline at end of file diff --git a/apps/mobile/src/utils/contextUtils.ts b/apps/mobile/src/utils/contextUtils.ts deleted file mode 100644 index 4cb0fcd..0000000 --- a/apps/mobile/src/utils/contextUtils.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { TimeContextType } from "../context/TimeContext"; - -// Calculate target date based on context and offset -export const getDateWithOffset = (context: TimeContextType, offset: number): Date => { - const now = new Date(); - const targetDate = new Date(now); - - switch (context) { - case "daily": - targetDate.setDate(now.getDate() - offset); - break; - - case "weekly": - targetDate.setDate(now.getDate() - (offset * 7)); - break; - - case "monthly": - targetDate.setMonth(now.getMonth() - offset); - break; - - case "project": - // Project context doesn't have time navigation - return now; - - default: - return now; - } - - return targetDate; -}; - -// Get formatted date range with offset support -export const getContextDateRangeWithOffset = (context: TimeContextType, offset: number = 0): string => { - const targetDate = getDateWithOffset(context, offset); - - switch (context) { - case "daily": - return targetDate.toLocaleDateString("en-US", { - weekday: "long", - month: "long", - day: "numeric" - }); - - case "weekly": - const startOfWeek = new Date(targetDate); - startOfWeek.setDate(targetDate.getDate() - targetDate.getDay()); - const endOfWeek = new Date(startOfWeek); - endOfWeek.setDate(startOfWeek.getDate() + 6); - - return `${startOfWeek.toLocaleDateString("en-US", { - month: "long", day: "numeric" - })} - ${endOfWeek.toLocaleDateString("en-US", { - month: "long", day: "numeric" - })}`; - - case "monthly": - return targetDate.toLocaleDateString("en-US", { - month: "long", - year: "numeric" - }); - - case "project": - return "Long-term Goals"; - - default: - return "Current Focus"; - } -}; - -// Backward compatibility function -export const getContextDateRange = (context: TimeContextType): string => { - return getContextDateRangeWithOffset(context, 0); -}; \ No newline at end of file diff --git a/apps/mobile/src/utils/dateFormatters.ts b/apps/mobile/src/utils/dateFormatters.ts new file mode 100644 index 0000000..5d86ba2 --- /dev/null +++ b/apps/mobile/src/utils/dateFormatters.ts @@ -0,0 +1,30 @@ +// Shared date formatting utilities for consistent display across components + +// formatMonthDay: Returns "Aug 31", "Sept 15" (custom Sept abbreviation) +// Used by: ChartDisplayContext labels, chart axis labels +export const formatMonthDay = (date: Date): string => { + const month = date.toLocaleDateString("en-US", { month: "short" }); + const day = date.toLocaleDateString("en-US", { day: "numeric" }); + return `${month === "Sep" ? "Sept" : month} ${day}`; +}; + +// formatMonthDayWithOrdinal: Returns "Aug 31st", "Sept 2nd" +// Used by: Future ordinal date displays +export const formatMonthDayWithOrdinal = (date: Date): string => { + const month = date.toLocaleDateString("en-US", { month: "short" }); + const day = date.getDate(); + const ordinal = (n: number) => + n + + (["th", "st", "nd", "rd"][((n % 100) - 20) % 10] || + ["th", "st", "nd", "rd"][n % 100] || + "th"); + return `${month === "Sep" ? "Sept" : month} ${ordinal(day)}`; +}; + +// formatTimeRange: Returns "Aug 31" or "Aug 29 - Aug 31" for date ranges +// Used by: Range display components +export const formatTimeRange = (start: Date, end: Date): string => { + const startStr = formatMonthDay(start); + const endStr = formatMonthDay(end); + return startStr === endStr ? startStr : `${startStr} - ${endStr}`; +}; diff --git a/apps/mobile/src/utils/localizationTest.ts b/apps/mobile/src/utils/localizationTest.ts new file mode 100644 index 0000000..9385356 --- /dev/null +++ b/apps/mobile/src/utils/localizationTest.ts @@ -0,0 +1,35 @@ +/** + * Test utility for react-native-localize integration + * Use this to verify that the native linking is working properly + */ +import { getLocales, getTimeZone, getCountry, getCurrencies } from 'react-native-localize'; + +export const testLocalizationFunctions = () => { + try { + console.log('=== React Native Localize Test ==='); + + // Test getTimeZone + const timezone = getTimeZone(); + console.log('✅ Timezone:', timezone); + + // Test getLocales + const locales = getLocales(); + console.log('✅ Locales:', locales); + + // Test getCountry + const country = getCountry(); + console.log('✅ Country:', country); + + // Test getCurrencies + const currencies = getCurrencies(); + console.log('✅ Currencies:', currencies); + + console.log('✅ All react-native-localize functions working correctly!'); + return true; + } catch (error) { + console.error('❌ Error testing react-native-localize:', error); + return false; + } +}; + +export default testLocalizationFunctions; \ No newline at end of file diff --git a/apps/mobile/src/utils/timeContextHelpers.ts b/apps/mobile/src/utils/timeContextHelpers.ts deleted file mode 100644 index 566e446..0000000 --- a/apps/mobile/src/utils/timeContextHelpers.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { TimeContextType } from "../context/TimeContext"; - -/** - * Converts a number to its ordinal form (1st, 2nd, 3rd, etc.) - */ -const getOrdinal = (num: number): string => { - const suffix = ["th", "st", "nd", "rd"]; - const mod = num % 100; - return num + (suffix[(mod - 20) % 10] || suffix[mod] || suffix[0]); -}; - -/** - * Simple time context formatting - * - * Examples: - * - Daily: "Today", "Yesterday", "2 days ago", "3 days ago", etc. - * - Weekly: "This week", "1 week ago", "2 weeks ago", etc. - * - Monthly: "This month", "1 month ago", "2 months ago", etc. - */ -export const getHumanisticTimeContext = ( - context: TimeContextType, - dateOffset: number -): string => { - if (dateOffset === 0) { - switch (context) { - case "daily": return "Today"; - case "weekly": return "This week"; - case "monthly": return "This month"; - case "project": return "Current"; - default: return "Current"; - } - } - - if (context === "daily") { - if (dateOffset === 1) return "Yesterday"; - return `${dateOffset} days ago`; - } - - if (context === "weekly") { - return `${dateOffset} week${dateOffset > 1 ? 's' : ''} ago`; - } - - if (context === "monthly") { - return `${dateOffset} month${dateOffset > 1 ? 's' : ''} ago`; - } - - return "Historical"; -}; - -/** - * Formats simple time context (for basic navigation) - */ -export const getSimpleTimeContext = ( - context: TimeContextType, - dateOffset: number -): string => { - if (dateOffset === 0) { - switch (context) { - case "daily": return "Today"; - case "weekly": return "This Week"; - case "monthly": return "This Month"; - case "project": return "Current"; - default: return "Current"; - } - } - - if (context === "daily") { - if (dateOffset === 1) return "Yesterday"; - const targetDate = new Date(); - targetDate.setDate(targetDate.getDate() - dateOffset); - return targetDate.toLocaleDateString([], { month: 'short', day: 'numeric' }); - } else if (context === "weekly") { - if (dateOffset === 1) return "Last Week"; - const now = new Date(); - const weekStart = new Date(now); - weekStart.setDate(now.getDate() - (now.getDay() + dateOffset * 7)); - return `Week of ${weekStart.toLocaleDateString([], { month: 'short', day: 'numeric' })}`; - } else if (context === "monthly") { - if (dateOffset === 1) return "Last Month"; - const now = new Date(); - const targetMonth = new Date(now.getFullYear(), now.getMonth() - dateOffset, 1); - return targetMonth.toLocaleDateString([], { month: 'long', year: 'numeric' }); - } - - return "Historical"; -}; \ No newline at end of file diff --git a/apps/server/docs/architecture.md b/apps/server/docs/architecture.md index 4ea4cac..eb86540 100644 --- a/apps/server/docs/architecture.md +++ b/apps/server/docs/architecture.md @@ -104,7 +104,7 @@ Autonomous agents that maintain persistent state and handle real-time features: Primary relational storage for structured data: ```sql - users (id, email, api_key, created_at) -- tides (id, user_id, name, flow_type, metadata) +- tides (id, user_id, name, metadata) - flow_sessions (id, tide_id, start_time, duration) - tide_tasks (id, tide_id, external_id, platform) - energy_readings (id, user_id, level, timestamp) diff --git a/apps/server/scripts/benchmark/benchmark.ts b/apps/server/scripts/benchmark/benchmark.ts index e4a698a..b120662 100644 --- a/apps/server/scripts/benchmark/benchmark.ts +++ b/apps/server/scripts/benchmark/benchmark.ts @@ -92,7 +92,6 @@ export class StorageBenchmark { for (let i = 0; i < iterations; i++) { const input: CreateTideInput = { name: `Benchmark Tide ${i}`, - flow_type: 'daily', description: 'Created for benchmarking purposes' }; @@ -122,7 +121,6 @@ export class StorageBenchmark { if (typeof storage.batchCreateTides === 'function') { const inputs: CreateTideInput[] = Array.from({ length: batchSize }, (_, i) => ({ name: `Batch Tide ${i}`, - flow_type: 'project', description: `Batch creation test ${i}` })); @@ -141,7 +139,6 @@ export class StorageBenchmark { const promises = Array.from({ length: batchSize }, async (_, i) => { const input: CreateTideInput = { name: `Batch Tide ${i}`, - flow_type: 'project', description: `Batch creation test ${i}` }; @@ -198,8 +195,6 @@ export class StorageBenchmark { const filterTests = [ undefined, // No filter { active_only: true }, - { flow_type: 'daily' }, - { flow_type: 'project', active_only: true } ]; for (const filter of filterTests) { @@ -259,7 +254,6 @@ export class StorageBenchmark { try { const tide = await this.storage.createTide({ name: `Concurrent Tide ${i}`, - flow_type: 'daily', description: 'Concurrent creation test' }); @@ -292,7 +286,6 @@ export class StorageBenchmark { try { const tide = await this.storage.createTide({ name: `Test Tide ${i}`, - flow_type: i % 2 === 0 ? 'daily' : 'project', description: `Test tide for benchmarking ${i}` }); tides.push(tide); diff --git a/apps/server/scripts/debug/debug-template-processing.js b/apps/server/scripts/debug/debug-template-processing.js index ea26349..3c6cbed 100644 --- a/apps/server/scripts/debug/debug-template-processing.js +++ b/apps/server/scripts/debug/debug-template-processing.js @@ -9,7 +9,6 @@ const testData = { tide: { id: 'tide_test_123', name: 'Test Deep Work Tide', - flow_type: 'daily', description: 'Test tide for debugging', created_at: '2025-08-07T17:00:00.000Z', status: 'active' @@ -59,7 +58,6 @@ const testData = { // Simple template to test basic substitution const simpleTemplate = ` Tide: {{tide.name}} -Type: {{tide.flow_type}} Sessions: {{flowSessions.length}} Duration: {{totalDuration}} minutes `; @@ -68,7 +66,6 @@ Duration: {{totalDuration}} minutes const complexTemplate = ` TIDE INFORMATION: - Name: {{tide.name}} -- Type: {{tide.flow_type}} - Description: {{tide.description || 'No description provided'}} - Total Sessions: {{flowSessions.length}} diff --git a/apps/server/scripts/tide-creation/README.md b/apps/server/scripts/tide-creation/README.md index 6aa53dc..8817266 100644 --- a/apps/server/scripts/tide-creation/README.md +++ b/apps/server/scripts/tide-creation/README.md @@ -26,8 +26,6 @@ TIDES_URL=https://tides-001.mpazbot.workers.dev ./create-synthetic-tide.sh # Custom tide name TIDE_NAME="My Test Tide" ./create-synthetic-tide.sh -# Different flow type -FLOW_TYPE=weekly ./create-synthetic-tide.sh ``` ### Environment Variables @@ -35,7 +33,6 @@ FLOW_TYPE=weekly ./create-synthetic-tide.sh - `TIDES_URL` - Server URL (default: tides-003 development) - `TIDES_API_KEY` - Authentication key (default: tides_testuser_001) - `TIDE_NAME` - Name for the tide (default: "Synthetic Test Tide") -- `FLOW_TYPE` - Type of tide: daily|weekly|custom (default: daily) ### Output diff --git a/apps/server/scripts/tide-creation/create-synthetic-tide.sh b/apps/server/scripts/tide-creation/create-synthetic-tide.sh index ea58342..5ea9404 100755 --- a/apps/server/scripts/tide-creation/create-synthetic-tide.sh +++ b/apps/server/scripts/tide-creation/create-synthetic-tide.sh @@ -9,7 +9,6 @@ set -e BASE_URL="${TIDES_URL:-https://tides-003.mpazbot.workers.dev}" API_KEY="${TIDES_API_KEY:-tides_testuser_001}" TIDE_NAME="${TIDE_NAME:-Synthetic Test Tide}" -FLOW_TYPE="${FLOW_TYPE:-daily}" # Colors for output RED='\033[0;31m' @@ -84,7 +83,6 @@ create_tide() { local args=$(cat < /dev/null; then @@ -285,7 +282,6 @@ main() { echo -e "${GREEN}Summary:${NC}" echo -e " ${CYAN}Tide ID:${NC} $tide_id" echo -e " ${CYAN}Name:${NC} $TIDE_NAME" - echo -e " ${CYAN}Type:${NC} $FLOW_TYPE" echo -e " ${CYAN}Data Created:${NC}" echo -e " • 6 flow sessions (185 total minutes)" echo -e " • 8 energy level updates" @@ -322,7 +318,6 @@ case "${1:-}" in echo " TIDES_URL Server URL (default: https://tides-003.mpazbot.workers.dev)" echo " TIDES_API_KEY API key (default: tides_testuser_001)" echo " TIDE_NAME Tide name (default: Synthetic Test Tide)" - echo " FLOW_TYPE Flow type: daily|weekly|custom (default: daily)" exit 0 ;; --quick) diff --git a/apps/server/scripts/tide-creation/deprecated/create-complete-tide.sh b/apps/server/scripts/tide-creation/deprecated/create-complete-tide.sh index 0eda10c..fe94ca9 100755 --- a/apps/server/scripts/tide-creation/deprecated/create-complete-tide.sh +++ b/apps/server/scripts/tide-creation/deprecated/create-complete-tide.sh @@ -34,7 +34,6 @@ RESPONSE=$(curl -s -X POST "$BASE_URL" \ "name": "tide_create", "arguments": { "name": "Deep Work Day - Complete Test", - "flow_type": "daily", "description": "Full day of focused work with realistic energy patterns and task management" } } diff --git a/apps/server/scripts/tide-creation/deprecated/create-manual-tide.sh b/apps/server/scripts/tide-creation/deprecated/create-manual-tide.sh index 6093edc..0791e1b 100755 --- a/apps/server/scripts/tide-creation/deprecated/create-manual-tide.sh +++ b/apps/server/scripts/tide-creation/deprecated/create-manual-tide.sh @@ -25,7 +25,6 @@ curl -X POST "$BASE_URL" \ "name": "tide_create", "arguments": { "name": "Daily Deep Work - Manual Test", - "flow_type": "daily", "description": "Synthetic tide for agent testing with real flow patterns" } } diff --git a/apps/server/scripts/tide-creation/deprecated/create-sample-tide.sh b/apps/server/scripts/tide-creation/deprecated/create-sample-tide.sh index d9a20f0..6cf6bb4 100755 --- a/apps/server/scripts/tide-creation/deprecated/create-sample-tide.sh +++ b/apps/server/scripts/tide-creation/deprecated/create-sample-tide.sh @@ -72,7 +72,6 @@ create_sample_tide() { \"name\": \"create_tide\", \"arguments\": { \"name\": \"Daily Deep Work - $today\", - \"flow_type\": \"daily\", \"description\": \"Focused productivity session with mixed task types and energy patterns\" } }" diff --git a/apps/server/scripts/tide-creation/deprecated/tide_creation.json b/apps/server/scripts/tide-creation/deprecated/tide_creation.json index 02b72db..3e95b3b 100644 --- a/apps/server/scripts/tide-creation/deprecated/tide_creation.json +++ b/apps/server/scripts/tide-creation/deprecated/tide_creation.json @@ -1,3 +1,3 @@ event: message -data: {"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"tide_id\": \"tide_1754586909833_jrybwfivqb\",\n \"name\": \"Daily Deep Work - Manual Test\",\n \"flow_type\": \"daily\",\n \"created_at\": \"2025-08-07T17:15:09.833Z\",\n \"status\": \"active\",\n \"description\": \"Synthetic tide for agent testing with real flow patterns\",\n \"next_flow\": \"2025-08-08 09:00\"\n}"}]},"jsonrpc":"2.0","id":1} +data: {"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"tide_id\": \"tide_1754586909833_jrybwfivqb\",\n \"name\": \"Daily Deep Work - Manual Test\",\n \"created_at\": \"2025-08-07T17:15:09.833Z\",\n \"status\": \"active\",\n \"description\": \"Synthetic tide for agent testing with real flow patterns\",\n \"next_flow\": \"2025-08-08 09:00\"\n}"}]},"jsonrpc":"2.0","id":1} diff --git a/apps/server/src/db/schema.sql b/apps/server/src/db/schema.sql index 1dc5c04..31f63fe 100644 --- a/apps/server/src/db/schema.sql +++ b/apps/server/src/db/schema.sql @@ -29,10 +29,6 @@ CREATE TABLE IF NOT EXISTS tide_index ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, - -- CHANGE: Added 'monthly' to flow types for hierarchical time contexts - -- WHY: Mobile users need daily → weekly → monthly progression without manual management - -- BUSINESS IMPACT: Enables natural workflow scaling from daily habits to monthly goals - flow_type TEXT NOT NULL CHECK (flow_type IN ('daily', 'weekly', 'monthly', 'project', 'seasonal')), status TEXT DEFAULT 'active' CHECK (status IN ('active', 'completed', 'paused')), description TEXT, -- For search and filtering created_at DATETIME DEFAULT CURRENT_TIMESTAMP, @@ -42,13 +38,6 @@ CREATE TABLE IF NOT EXISTS tide_index ( total_duration INTEGER DEFAULT 0, -- Cached total flow duration in minutes energy_balance INTEGER DEFAULT 0, -- Cached energy score r2_path TEXT NOT NULL, -- Path to full JSON in R2 - -- NEW SCHEMA: Hierarchical tide support (ADR-003 implementation) - -- ARCHITECTURE: Enables parent-child relationships without complex queries - -- PERFORMANCE: Date range queries avoid expensive JSON parsing in R2 - parent_tide_id TEXT REFERENCES tide_index(id), -- Monthly tide → Weekly tide → Daily tide - date_start TEXT, -- ISO date (YYYY-MM-DD) for time-bound tides - date_end TEXT, -- ISO date (YYYY-MM-DD) for time-bound tides - auto_created BOOLEAN DEFAULT FALSE, -- Distinguishes system vs user-created tides FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); @@ -110,23 +99,13 @@ CREATE TABLE IF NOT EXISTS flow_session_summary ( CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id); CREATE INDEX IF NOT EXISTS idx_tide_user ON tide_index(user_id); CREATE INDEX IF NOT EXISTS idx_tide_user_status ON tide_index(user_id, status); -CREATE INDEX IF NOT EXISTS idx_tide_user_flow_type ON tide_index(user_id, flow_type); -- Composite indexes for complex queries -CREATE INDEX IF NOT EXISTS idx_tide_user_status_flowtype ON tide_index(user_id, status, flow_type); CREATE INDEX IF NOT EXISTS idx_tide_user_created ON tide_index(user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_tide_user_lastflow ON tide_index(user_id, last_flow DESC); CREATE INDEX IF NOT EXISTS idx_tide_user_updated ON tide_index(user_id, updated_at DESC); --- NEW INDEXES: Hierarchical tide performance optimization --- WHY: Auto-creation queries need sub-100ms response times for mobile UX --- SCALE IMPACT: These indexes support millions of auto-created tides efficiently --- QUERY PATTERNS: Optimized for "get/create daily tide for user X on date Y" -CREATE INDEX IF NOT EXISTS idx_tides_parent ON tide_index(parent_tide_id); -- Parent-child traversal -CREATE INDEX IF NOT EXISTS idx_tides_date_range ON tide_index(date_start, date_end); -- Time range queries -CREATE INDEX IF NOT EXISTS idx_tides_auto_created ON tide_index(auto_created, flow_type); -- System vs user tides -CREATE INDEX IF NOT EXISTS idx_tides_user_date_type ON tide_index(user_id, date_start, flow_type); -- Core lookup -CREATE INDEX IF NOT EXISTS idx_tides_user_date_auto ON tide_index(user_id, date_start, auto_created); -- Auto-creation check +-- Date-based tide indexes for auto-creation performance -- Analytics table indexes CREATE INDEX IF NOT EXISTS idx_analytics_user ON tide_analytics(user_id); diff --git a/apps/server/src/handlers/tools.ts b/apps/server/src/handlers/tools.ts index 6e00923..8c84843 100644 --- a/apps/server/src/handlers/tools.ts +++ b/apps/server/src/handlers/tools.ts @@ -46,17 +46,14 @@ export function registerTideTools(server: McpServer, storage: TideStorage) { "tide_create", { title: "Create Tide", - description: "Create a new tidal workflow for rhythmic productivity. Use when users want to start a new workflow, project, or productivity cycle. Accepts name, flow type (daily/weekly/project/seasonal), and optional description. Returns tide ID and scheduling info for follow-up actions.", + description: "Create a new tidal workflow for rhythmic productivity. Use when users want to start a new workflow, project, or productivity cycle. Accepts name, and optional description. Returns tide ID and scheduling info for follow-up actions.", inputSchema: { name: z.string().describe("Human-readable name for the tide"), - // CHANGE: Added "monthly" to flow types for hierarchical contexts - // MOBILE IMPACT: Enables daily → weekly → monthly progression in mobile UX - flow_type: z.enum(["daily", "weekly", "monthly", "project", "seasonal"]).describe("Type of tide rhythm"), description: z.string().optional().describe("Detailed description of the tide's purpose"), }, }, - async ({ name, flow_type, description }) => { - const result = await tideTools.createTide({ name, flow_type, description }, storage); + async ({ name, description }) => { + const result = await tideTools.createTide({ name, description }, storage); return { content: [ { @@ -82,12 +79,11 @@ export function registerTideTools(server: McpServer, storage: TideStorage) { title: "List Tides", description: "List all tidal workflows with optional filtering by flow type and active status. Perfect for dashboard views and workflow management. Returns array of tide summaries with flow counts and timestamps for mobile display.", inputSchema: { - flow_type: z.string().optional().describe("Filter by flow type"), active_only: z.boolean().optional().describe("Show only active tides"), }, }, - async ({ flow_type, active_only }) => { - const result = await tideTools.listTides({ flow_type, active_only }, storage); + async ({ active_only }) => { + const result = await tideTools.listTides({ active_only }, storage); return { content: [ { @@ -319,34 +315,24 @@ export function registerTideTools(server: McpServer, storage: TideStorage) { }, ); - // ============================================================================= - // NEW SECTION: Hierarchical Tide Tools (ADR-003 Implementation) - // ============================================================================= - // BUSINESS IMPACT: These tools transform Tides from "manual tide management" to "just work" - // MOBILE CRITICAL: Without these tools, mobile apps require users to manually create daily tides - // ARCHITECTURE: Implements context-based tides that always exist vs user-created project tides - /** - * NEW TOOL: tide_get_or_create_daily - * - * MOBILE CRITICAL: This tool eliminates the #1 UX friction point in mobile apps - * WHY CRITICAL: Mobile users expect to "just start working" without setup tasks - * PRODUCTION FIX: Solves mobile app crashes when no tides exist + * MCP Tool: tide_get_or_create * - * Following service_noun_verb pattern: tide_get_or_create_daily + * Ensures a tide exists for the current date, creating one if needed. + * This simplifies mobile app UX by removing the need for manual tide creation. */ server.registerTool( - "tide_get_or_create_daily", + "tide_get_or_create", { - title: "Get or Create Daily Tide", - description: "Get or create a daily tide for today (or specified date). This is the key tool for mobile apps to automatically manage daily workflows without user intervention. Ensures a daily tide exists and returns it with hierarchical context when available.", + title: "Get or Create Tide", + description: "Get or create a tide for today (or specified date). This is the key tool for mobile apps to automatically manage workflows without user intervention. Ensures a tide exists and returns it.", inputSchema: { timezone: z.string().optional().describe("User's timezone for date calculation"), date: z.string().optional().describe("Specific date in YYYY-MM-DD format (defaults to today)"), }, }, async ({ timezone, date }) => { - const result = await tideTools.tideGetOrCreateDaily({ timezone, date }, storage); + const result = await tideTools.tideGetOrCreate({ timezone, date }, storage); return { content: [ { @@ -358,137 +344,4 @@ export function registerTideTools(server: McpServer, storage: TideStorage) { }, ); - /** - * MCP Tool: tide_switch_context - * - * Switches between daily, weekly, and monthly tide contexts for the same - * underlying workflow data. Core functionality for hierarchical tide navigation. - * - * Following service_noun_verb pattern: tide_switch_context - */ - server.registerTool( - "tide_switch_context", - { - title: "Switch Tide Context", - description: "Switch between daily, weekly, and monthly views of the same workflow data. Enables seamless navigation between different time-scale perspectives with automatic context creation and hierarchical relationships.", - inputSchema: { - context: z.enum(["daily", "weekly", "monthly"]).describe("Target time context to switch to"), - date: z.string().optional().describe("ISO date for context (defaults to today)"), - create_if_missing: z.boolean().optional().default(true).describe("Create context if it doesn't exist"), - }, - }, - async ({ context, date, create_if_missing }) => { - const result = await tideTools.tideSwitchContext({ context, date, create_if_missing }, storage); - return { - content: [ - { - type: "text", - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - ); - - /** - * MCP Tool: tide_list_contexts - * - * Lists available tide contexts for a given date with metadata about - * each context's content and activity levels. - * - * Following service_noun_verb pattern: tide_list_contexts - */ - server.registerTool( - "tide_list_contexts", - { - title: "List Tide Contexts", - description: "List available tide contexts (daily, weekly, monthly) for a given date with activity metadata. Shows which contexts exist, their flow session counts, and creation availability for context navigation UI.", - inputSchema: { - date: z.string().optional().describe("ISO date to check contexts for (defaults to today)"), - include_empty: z.boolean().optional().default(true).describe("Include contexts with no flow sessions"), - }, - }, - async ({ date, include_empty }) => { - const result = await tideTools.tideListContexts({ date, include_empty }, storage); - return { - content: [ - { - type: "text", - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - ); - - /** - * MCP Tool: tide_start_hierarchical_flow - * - * Starts a flow session that automatically distributes across all relevant - * hierarchical contexts (daily, weekly, monthly). This is the enhanced flow - * function that implements the core ADR-003 hierarchical tide pattern. - * - * Following service_noun_verb pattern: tide_start_hierarchical_flow - */ - server.registerTool( - "tide_start_hierarchical_flow", - { - title: "Start Hierarchical Flow", - description: "Start a flow session that automatically distributes to daily, weekly, and monthly contexts simultaneously. Implements the hierarchical tide pattern where one flow session contributes to all relevant time scales with automatic context creation.", - inputSchema: { - intensity: z.enum(["gentle", "moderate", "strong"]).optional().default("moderate").describe("Work intensity level"), - duration: z.number().optional().default(25).describe("Session duration in minutes"), - initial_energy: z.string().optional().default("medium").describe("Starting energy level"), - work_context: z.string().optional().default("General work").describe("Description of work being done"), - date: z.string().optional().describe("Date for the session (defaults to today)"), - }, - }, - async ({ intensity, duration, initial_energy, work_context, date }) => { - const result = await tideTools.startHierarchicalFlow({ - intensity, - duration, - initial_energy, - work_context, - date - }, storage); - return { - content: [ - { - type: "text", - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - ); - - /** - * MCP Tool: tide_get_todays_summary - * - * Gets a summary of today's hierarchical tide contexts showing activity - * across daily, weekly, and monthly views for dashboard displays. - * - * Following service_noun_verb pattern: tide_get_todays_summary - */ - server.registerTool( - "tide_get_todays_summary", - { - title: "Get Today's Context Summary", - description: "Get a summary of today's hierarchical tide contexts showing flow sessions and activity across daily, weekly, and monthly views. Perfect for dashboard displays and activity overviews.", - inputSchema: { - date: z.string().optional().describe("Date to get context summary for (defaults to today)"), - }, - }, - async ({ date }) => { - const result = await tideTools.getTodaysContextSummary({ date }, storage); - return { - content: [ - { - type: "text", - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - ); } \ No newline at end of file diff --git a/apps/server/src/prompts/analyze-tide.md b/apps/server/src/prompts/analyze-tide.md index 402793a..0723ed7 100644 --- a/apps/server/src/prompts/analyze-tide.md +++ b/apps/server/src/prompts/analyze-tide.md @@ -7,7 +7,7 @@ COMPREHENSIVE TIDE ANALYSIS REQUEST TIDE INFORMATION: - Name: {{tide.name}} -- Type: {{tide.flow_type}} +- Status: {{tide.status}} - Description: {{tide.description || 'No description provided'}} - Created: {{tide.created_at}} - Status: {{tide.status || 'active'}} diff --git a/apps/server/src/prompts/custom-tide-analysis.md b/apps/server/src/prompts/custom-tide-analysis.md index 6746f64..8231bea 100644 --- a/apps/server/src/prompts/custom-tide-analysis.md +++ b/apps/server/src/prompts/custom-tide-analysis.md @@ -7,7 +7,7 @@ CUSTOM TIDE ANALYSIS REQUEST TIDE INFORMATION: - Name: {{tide.name}} -- Type: {{tide.flow_type}} +- Status: {{tide.status}} - Description: {{tide.description || 'No description provided'}} - Created: {{tide.created_at}} - Status: {{tide.status || 'active'}} diff --git a/apps/server/src/prompts/optimize-energy.md b/apps/server/src/prompts/optimize-energy.md index 7451742..083c368 100644 --- a/apps/server/src/prompts/optimize-energy.md +++ b/apps/server/src/prompts/optimize-energy.md @@ -7,7 +7,7 @@ ENERGY OPTIMIZATION ANALYSIS REQUEST TIDE INFORMATION: - Name: {{tide.name}} -- Type: {{tide.flow_type}} +- Status: {{tide.status}} - Target Schedule: {{target_schedule || 'Not specified'}} - Energy Goals: {{energy_goals || 'General optimization'}} diff --git a/apps/server/src/prompts/productivity-insights.md b/apps/server/src/prompts/productivity-insights.md index 77f52ac..574b47d 100644 --- a/apps/server/src/prompts/productivity-insights.md +++ b/apps/server/src/prompts/productivity-insights.md @@ -7,7 +7,7 @@ PRODUCTIVITY INSIGHTS ANALYSIS REQUEST TIDE OVERVIEW: - Name: {{tide.name}} -- Type: {{tide.flow_type}} +- Status: {{tide.status}} - Analysis Period: {{time_period || 'All available data'}} - Comparison Baseline: {{comparison_baseline || 'None specified'}} diff --git a/apps/server/src/prompts/registry.ts b/apps/server/src/prompts/registry.ts index 2a4628b..9bdd0a3 100644 --- a/apps/server/src/prompts/registry.ts +++ b/apps/server/src/prompts/registry.ts @@ -51,7 +51,6 @@ export const PROMPT_TEMPLATES: Record = { TIDE INFORMATION: - Name: {{tide.name}} -- Type: {{tide.flow_type}} - Description: {{tide.description || 'No description provided'}} - Created: {{tide.created_at}} - Status: {{tide.status || 'active'}} @@ -128,7 +127,6 @@ Please structure your response with clear sections and specific, actionable insi TIDE OVERVIEW: - Name: {{tide.name}} -- Type: {{tide.flow_type}} - Analysis Period: {{time_period || 'All available data'}} - Comparison Baseline: {{comparison_baseline || 'None specified'}} @@ -195,7 +193,6 @@ Focus on actionable insights that can immediately improve productivity patterns TIDE INFORMATION: - Name: {{tide.name}} -- Type: {{tide.flow_type}} - Target Schedule: {{target_schedule || 'Not specified'}} - Energy Goals: {{energy_goals || 'General optimization'}} @@ -336,7 +333,6 @@ Focus on actionable recommendations that enhance both individual performance and TIDE INFORMATION: - Name: {{tide.name}} -- Type: {{tide.flow_type}} - Description: {{tide.description || 'No description provided'}} - Created: {{tide.created_at}} - Status: {{tide.status || 'active'}} diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 146959c..a463da8 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -43,13 +43,11 @@ * // Example: Creating a new tide * const result = await mcpClient.callTool('tide_create', { * name: "Daily Standup Prep", - * flow_type: "daily", * description: "Prepare talking points for standup" * }); * * // Example: Getting tides for FlatList * const tidesResult = await mcpClient.callTool('tide_list', { - * flow_type: "daily", * active_only: true * }); * ``` diff --git a/apps/server/src/services/aiService.ts b/apps/server/src/services/aiService.ts index 541729a..b229c28 100644 --- a/apps/server/src/services/aiService.ts +++ b/apps/server/src/services/aiService.ts @@ -458,7 +458,22 @@ Analyze patterns, identify trends, and provide specific recommendations for opti request.context ); - // Use Llama for fast conversational responses with structured prompt + // Fast fallback for simple greetings to avoid cold start delays + const isSimpleGreeting = /^(hi|hello|hey|good\s+(morning|afternoon|evening))\s*$/i.test(request.message.trim()); + + if (isSimpleGreeting) { + // Return immediate response for simple greetings + const result: ConversationResponse = { + response: "Welcome to Tides AI. I'm here to help you navigate your energy and focus patterns. How's your energy flowing today? Are you feeling energized or a bit drained?", + type: "text", + suggestedTools: ["getTideList", "createTide"], + source: "workers-ai", + }; + this.setCache(cacheKey, result, 10 * 60 * 1000); + return result; + } + + // Use Llama for more complex conversational responses const response = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", { messages: [ { diff --git a/apps/server/src/storage/d1-r2.ts b/apps/server/src/storage/d1-r2.ts index a817d20..ae5932e 100644 --- a/apps/server/src/storage/d1-r2.ts +++ b/apps/server/src/storage/d1-r2.ts @@ -64,7 +64,6 @@ export class D1R2HybridStorage implements TideStorage { const tide: Tide = { id: tideId, name: input.name, - flow_type: input.flow_type, description: input.description, created_at: now, status: 'active', @@ -80,13 +79,12 @@ export class D1R2HybridStorage implements TideStorage { // Enhanced transaction-like pattern: prepare all operations first const d1Statement = this.db.prepare(` INSERT INTO tide_index ( - id, user_id, name, flow_type, description, status, created_at, updated_at, r2_path + id, user_id, name, description, status, created_at, updated_at, r2_path ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `).bind( tideId, userId, input.name, - input.flow_type, input.description || null, 'active', now, @@ -98,7 +96,6 @@ export class D1R2HybridStorage implements TideStorage { tideId, userId, name: input.name, - flow_type: input.flow_type, description: input.description || null, status: 'active', created_at: now, @@ -177,11 +174,6 @@ export class D1R2HybridStorage implements TideStorage { let query = 'SELECT * FROM tide_index WHERE user_id = ?'; const params: any[] = [userId]; - if (filter?.flow_type) { - query += ' AND flow_type = ?'; - params.push(filter.flow_type); - } - if (filter?.active_only) { query += ' AND status = ?'; params.push('active'); @@ -199,7 +191,6 @@ export class D1R2HybridStorage implements TideStorage { return results.results.map((row: any) => ({ id: row.id, name: row.name, - flow_type: row.flow_type, status: row.status, created_at: row.created_at, description: row.description || '', // Use actual description from D1 @@ -225,12 +216,11 @@ export class D1R2HybridStorage implements TideStorage { // Always update updated_at timestamp await this.db.prepare(` UPDATE tide_index - SET name = ?, status = ?, flow_type = ?, description = ?, updated_at = ? + SET name = ?, status = ?, description = ?, updated_at = ? WHERE id = ? AND user_id = ? `).bind( updated.name, updated.status, - updated.flow_type, updated.description || null, now, id, @@ -610,263 +600,6 @@ export class D1R2HybridStorage implements TideStorage { } } - // ============================================================================= - // NEW FEATURE: Hierarchical Tide Methods (ADR-003 Implementation) - // ============================================================================= - // WHY: Mobile apps need seamless daily workflow management without manual tide creation - // PATTERN: Auto-creating context-based tides eliminates user friction while providing time-scale views - // IMPACT: This transforms the UX from "manage tides" to "just work" - crucial for mobile adoption - - /** - * NEW: Gets or creates a daily tide for the specified date - * MOBILE UX: Core function that eliminates manual tide management for users - * AUTO-SCALING: Creates hierarchical relationships (daily → weekly → monthly) automatically - */ - async getOrCreateDailyTide(date: string): Promise { - const userId = this.getUserId(); - - // CHANGE: Query uses new hierarchical fields (date_start, auto_created) - // WHY: Distinguishes auto-created context tides from user-created project tides - // SCALE: Query is optimized with composite index on (user_id, date_start, auto_created) - const existing = await this.db.prepare(` - SELECT r2_path FROM tide_index - WHERE user_id = ? AND flow_type = 'daily' AND date_start = ? AND auto_created = true - `).bind(userId, date).first(); - - if (existing) { - const tide = await this.r2.getObject(existing.r2_path as string); - if (tide) return tide; - } - // Create new daily tide - const tideId = this.generateId('tide'); - const now = new Date().toISOString(); - const { formatDate } = await import('../utils/date-utils'); - - const tide: Tide = { - id: tideId, - name: `Daily Focus - ${formatDate(date)}`, - flow_type: 'daily', - description: `Automatically created daily tide for ${date}`, - created_at: now, - status: 'active', - flow_sessions: [], - energy_updates: [], - task_links: [], - }; - - const r2Path = this.getUserR2Path(userId, tideId); - - try { - // Insert into D1 with hierarchical data - await this.db.prepare(` - INSERT INTO tide_index ( - id, user_id, name, flow_type, description, status, created_at, updated_at, r2_path, - date_start, date_end, auto_created - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).bind( - tideId, userId, tide.name, tide.flow_type, tide.description, - tide.status, now, now, r2Path, date, date, true - ).run(); - - // Store in R2 - await this.r2.putObject(r2Path, tide); - - // Initialize analytics - await this.db.prepare(` - INSERT INTO tide_analytics (tide_id, user_id, created_at, updated_at) - VALUES (?, ?, ?, ?) - `).bind(tideId, userId, now, now).run(); - - console.log(`✅ Created daily tide for ${date}: ${tideId}`); - return tide; - } catch (error) { - console.error('Error creating daily tide:', error); - throw error; - } - } - - /** - * Gets or creates a weekly tide for the week containing the specified date - */ - async getOrCreateWeeklyTide(date: string): Promise { - const userId = this.getUserId(); - const { getWeekStart, getWeekEnd, formatDateRange } = await import('../utils/date-utils'); - - const weekStart = getWeekStart(date); - const weekEnd = getWeekEnd(date); - - // Check if weekly tide already exists for this week - const existing = await this.db.prepare(` - SELECT r2_path FROM tide_index - WHERE user_id = ? AND flow_type = 'weekly' AND date_start = ? AND date_end = ? AND auto_created = true - `).bind(userId, weekStart, weekEnd).first(); - - if (existing) { - const tide = await this.r2.getObject(existing.r2_path as string); - if (tide) return tide; - } - - // Create new weekly tide - const tideId = this.generateId('tide'); - const now = new Date().toISOString(); - - const tide: Tide = { - id: tideId, - name: `Week of ${formatDateRange(weekStart, weekEnd)}`, - flow_type: 'weekly', - description: `Automatically created weekly tide for ${weekStart} to ${weekEnd}`, - created_at: now, - status: 'active', - flow_sessions: [], - energy_updates: [], - task_links: [], - }; - - const r2Path = this.getUserR2Path(userId, tideId); - - try { - // Insert into D1 with hierarchical data - await this.db.prepare(` - INSERT INTO tide_index ( - id, user_id, name, flow_type, description, status, created_at, updated_at, r2_path, - date_start, date_end, auto_created - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).bind( - tideId, userId, tide.name, tide.flow_type, tide.description, - tide.status, now, now, r2Path, weekStart, weekEnd, true - ).run(); - // Store in R2 - await this.r2.putObject(r2Path, tide); - - // Initialize analytics - await this.db.prepare(` - INSERT INTO tide_analytics (tide_id, user_id, created_at, updated_at) - VALUES (?, ?, ?, ?) - `).bind(tideId, userId, now, now).run(); - - // Link existing daily tides as children - await this.linkDailyTidesToWeek(userId, tideId, weekStart, weekEnd); - - console.log(`✅ Created weekly tide for ${weekStart} to ${weekEnd}: ${tideId}`); - return tide; - } catch (error) { - console.error('Error creating weekly tide:', error); - throw error; - } - } - - /** - * Gets or creates a monthly tide for the month containing the specified date - */ - async getOrCreateMonthlyTide(date: string): Promise { - const userId = this.getUserId(); - const { getMonthStart, getMonthEnd, formatDateRange } = await import('../utils/date-utils'); - - const monthStart = getMonthStart(date); - const monthEnd = getMonthEnd(date); - - // Check if monthly tide already exists for this month - const existing = await this.db.prepare(` - SELECT r2_path FROM tide_index - WHERE user_id = ? AND flow_type = 'monthly' AND date_start = ? AND date_end = ? AND auto_created = true - `).bind(userId, monthStart, monthEnd).first(); - - if (existing) { - const tide = await this.r2.getObject(existing.r2_path as string); - if (tide) return tide; - } - - // Create new monthly tide - const tideId = this.generateId('tide'); - const now = new Date().toISOString(); - - const tide: Tide = { - id: tideId, - name: `${formatDateRange(monthStart, monthEnd)}`, - flow_type: 'monthly', - description: `Automatically created monthly tide for ${monthStart} to ${monthEnd}`, - created_at: now, - status: 'active', - flow_sessions: [], - energy_updates: [], - task_links: [], - }; - - const r2Path = this.getUserR2Path(userId, tideId); - - try { - // Insert into D1 with hierarchical data - await this.db.prepare(` - INSERT INTO tide_index ( - id, user_id, name, flow_type, description, status, created_at, updated_at, r2_path, - date_start, date_end, auto_created - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).bind( - tideId, userId, tide.name, tide.flow_type, tide.description, - tide.status, now, now, r2Path, monthStart, monthEnd, true - ).run(); - - // Store in R2 - await this.r2.putObject(r2Path, tide); - - // Initialize analytics - await this.db.prepare(` - INSERT INTO tide_analytics (tide_id, user_id, created_at, updated_at) - VALUES (?, ?, ?, ?) - `).bind(tideId, userId, now, now).run(); - - // Link existing weekly tides as children - await this.linkWeeklyTidesToMonth(userId, tideId, monthStart, monthEnd); - - console.log(`✅ Created monthly tide for ${monthStart} to ${monthEnd}: ${tideId}`); - return tide; - } catch (error) { - console.error('Error creating monthly tide:', error); - throw error; - } - } - - /** - * Links existing daily tides to a weekly parent - */ - private async linkDailyTidesToWeek(userId: string, weeklyTideId: string, weekStart: string, weekEnd: string): Promise { - await this.db.prepare(` - UPDATE tide_index - SET parent_tide_id = ? - WHERE user_id = ? AND flow_type = 'daily' - AND date_start >= ? AND date_start <= ? - AND parent_tide_id IS NULL - `).bind(weeklyTideId, userId, weekStart, weekEnd).run(); - } - - /** - * Links existing weekly tides to a monthly parent - */ - private async linkWeeklyTidesToMonth(userId: string, monthlyTideId: string, monthStart: string, monthEnd: string): Promise { - await this.db.prepare(` - UPDATE tide_index - SET parent_tide_id = ? - WHERE user_id = ? AND flow_type = 'weekly' - AND date_start >= ? AND date_start <= ? - AND parent_tide_id IS NULL - `).bind(monthlyTideId, userId, monthStart, monthEnd).run(); - } - - /** - * Gets tide by context and date (for context switching) - */ - async getTideByContext(context: 'daily' | 'weekly' | 'monthly', date: string): Promise { - switch (context) { - case 'daily': - return await this.getOrCreateDailyTide(date); - case 'weekly': - return await this.getOrCreateWeeklyTide(date); - case 'monthly': - return await this.getOrCreateMonthlyTide(date); - default: - throw new Error(`Invalid context: ${context}`); - } - } } \ No newline at end of file diff --git a/apps/server/src/storage/index.ts b/apps/server/src/storage/index.ts index d9bed53..44e4d09 100644 --- a/apps/server/src/storage/index.ts +++ b/apps/server/src/storage/index.ts @@ -4,7 +4,6 @@ import type { Env as AgentEnv } from "@agents/types"; export interface Tide { id: string; name: string; - flow_type: "daily" | "weekly" | "monthly" | "project" | "seasonal"; description?: string; created_at: string; status: "active" | "completed" | "paused"; @@ -42,12 +41,10 @@ export interface TaskLink { export interface CreateTideInput { name: string; - flow_type: "daily" | "weekly" | "monthly" | "project" | "seasonal"; description?: string; } export interface TideFilter { - flow_type?: string; active_only?: boolean; } diff --git a/apps/server/src/storage/mock.ts b/apps/server/src/storage/mock.ts index c2aae95..c6eef5e 100644 --- a/apps/server/src/storage/mock.ts +++ b/apps/server/src/storage/mock.ts @@ -14,7 +14,6 @@ export class MockTideStorage implements TideStorage { const tide: Tide = { id: this.generateId('tide'), name: input.name, - flow_type: input.flow_type, description: input.description, created_at: new Date().toISOString(), status: 'active', @@ -34,10 +33,6 @@ export class MockTideStorage implements TideStorage { async listTides(filter?: TideFilter): Promise { let tides = Array.from(this.tides.values()); - if (filter?.flow_type) { - tides = tides.filter(tide => tide.flow_type === filter.flow_type); - } - if (filter?.active_only) { tides = tides.filter(tide => tide.status === 'active'); } diff --git a/apps/server/src/storage/r2-rest.ts b/apps/server/src/storage/r2-rest.ts index 2457ac2..33780e6 100644 --- a/apps/server/src/storage/r2-rest.ts +++ b/apps/server/src/storage/r2-rest.ts @@ -5,7 +5,6 @@ interface TideIndex { tides: Array<{ id: string; name: string; - flow_type: 'daily' | 'weekly' | 'monthly' | 'project' | 'seasonal'; status: 'active' | 'completed' | 'paused'; created_at: string; flow_count: number; @@ -82,7 +81,6 @@ export class R2RestApiStorage implements TideStorage { const tide: Tide = { id: this.generateId('tide'), name: input.name, - flow_type: input.flow_type, description: input.description, created_at: new Date().toISOString(), status: 'active', @@ -117,11 +115,6 @@ export class R2RestApiStorage implements TideStorage { let filteredTides = index.tides; - // Apply filters - if (filter?.flow_type) { - filteredTides = filteredTides.filter(tide => tide.flow_type === filter.flow_type); - } - if (filter?.active_only) { filteredTides = filteredTides.filter(tide => tide.status === 'active'); } @@ -131,7 +124,6 @@ export class R2RestApiStorage implements TideStorage { const tides: Tide[] = filteredTides.map(indexEntry => ({ id: indexEntry.id, name: indexEntry.name, - flow_type: indexEntry.flow_type, status: indexEntry.status, created_at: indexEntry.created_at, description: '', // Not stored in index @@ -284,7 +276,6 @@ export class R2RestApiStorage implements TideStorage { const indexEntry = { id: tide.id, name: tide.name, - flow_type: tide.flow_type, status: tide.status, created_at: tide.created_at, flow_count: tide.flow_sessions.length, diff --git a/apps/server/src/storage/r2.ts b/apps/server/src/storage/r2.ts index ec22c3c..4699502 100644 --- a/apps/server/src/storage/r2.ts +++ b/apps/server/src/storage/r2.ts @@ -5,7 +5,6 @@ interface TideIndex { tides: Array<{ id: string; name: string; - flow_type: 'daily' | 'weekly' | 'monthly' | 'project' | 'seasonal'; status: 'active' | 'completed' | 'paused'; created_at: string; flow_count: number; @@ -33,7 +32,6 @@ export class R2TideStorage implements TideStorage { const tide: Tide = { id: this.generateId('tide'), name: input.name, - flow_type: input.flow_type, description: input.description, created_at: new Date().toISOString(), status: 'active', @@ -82,11 +80,6 @@ export class R2TideStorage implements TideStorage { let filteredTides = index.tides; - // Apply filters - if (filter?.flow_type) { - filteredTides = filteredTides.filter(tide => tide.flow_type === filter.flow_type); - } - if (filter?.active_only) { filteredTides = filteredTides.filter(tide => tide.status === 'active'); } @@ -96,7 +89,6 @@ export class R2TideStorage implements TideStorage { const tides: Tide[] = filteredTides.map(indexEntry => ({ id: indexEntry.id, name: indexEntry.name, - flow_type: indexEntry.flow_type, status: indexEntry.status, created_at: indexEntry.created_at, description: '', // Not stored in index @@ -255,7 +247,6 @@ export class R2TideStorage implements TideStorage { const indexEntry = { id: tide.id, name: tide.name, - flow_type: tide.flow_type, status: tide.status, created_at: tide.created_at, flow_count: tide.flow_sessions.length, diff --git a/apps/server/src/tools/index.ts b/apps/server/src/tools/index.ts index 5e42367..4d7f0e7 100644 --- a/apps/server/src/tools/index.ts +++ b/apps/server/src/tools/index.ts @@ -55,7 +55,7 @@ */ // Core tide management operations -export { createTide, listTides, tideGetOrCreateDaily } from './tide-core'; // NEW: Auto daily creation +export { createTide, listTides, tideGetOrCreate } from './tide-core'; // NEW: Auto daily creation // Flow sessions and energy tracking export { startTideFlow, addTideEnergy } from './tide-sessions'; @@ -65,13 +65,3 @@ export { linkTideTask, listTideTaskLinks } from './tide-tasks'; // Analytics and reporting export { getTideReport, getTideRawJson, getParticipants } from './tide-analytics'; - -// NEW FEATURES: Hierarchical tide management (ADR-003) -// WHY: Mobile apps need seamless daily/weekly/monthly context switching -// IMPACT: Eliminates manual tide management while providing time-scale perspectives -export { tideSwitchContext, tideListContexts } from './tide-context'; - -// NEW FEATURES: Enhanced hierarchical flow sessions -// WHY: Single flow session should contribute to daily, weekly, AND monthly views -// UX BENEFIT: "Just start working" - system handles all the complexity -export { startHierarchicalFlow, getTodaysContextSummary } from './tide-hierarchical-flow'; \ No newline at end of file diff --git a/apps/server/src/tools/tide-analytics.ts b/apps/server/src/tools/tide-analytics.ts index 17c0a0c..62b288d 100644 --- a/apps/server/src/tools/tide-analytics.ts +++ b/apps/server/src/tools/tide-analytics.ts @@ -79,7 +79,6 @@ * interface TideReport { * tide_id: string; // Tide identifier * name: string; // Tide display name - * flow_type: string; // Tide rhythm type * created_at: string; // Tide creation timestamp * total_flows: number; // Number of flow sessions * total_duration: number; // Total minutes of focused work @@ -218,7 +217,6 @@ export async function getTideReport( const baseReport = { tide_id: params.tide_id, name: tide.name, - flow_type: tide.flow_type, created_at: tide.created_at, total_flows: flowSessions.length, total_duration: totalDuration, @@ -232,7 +230,6 @@ export async function getTideReport( const energyList = energyProgression.map((energy, i) => `- Session ${i + 1}: ${energy}`).join('\n'); const markdown = `# Tide Report: ${tide.name} -**Type:** ${tide.flow_type} **Created:** ${new Date(tide.created_at).toLocaleDateString()} **Total Sessions:** ${flowSessions.length} **Average Duration:** ${averageDuration} minutes diff --git a/apps/server/src/tools/tide-context.ts b/apps/server/src/tools/tide-context.ts deleted file mode 100644 index 156b406..0000000 --- a/apps/server/src/tools/tide-context.ts +++ /dev/null @@ -1,400 +0,0 @@ -/** - * @fileoverview Hierarchical Tide Context Tools - * - * This module provides tools for managing hierarchical tide contexts and context switching. - * These tools enable seamless navigation between daily, weekly, and monthly views of the - * same underlying workflow data. - * - * ## Context Switching - * - * Users can switch between different time-scale views of their workflow: - * - **Daily Context**: Focus on today's activities and flows - * - **Weekly Context**: View the current week's patterns and progress - * - **Monthly Context**: See long-term trends and monthly achievements - * - * ## Automatic Context Creation - * - * When switching to a context that doesn't exist, the system automatically creates: - * - Daily tides for the specified date - * - Weekly tides for the week containing the date - * - Monthly tides for the month containing the date - * - * ## Hierarchical Relationships - * - * Context switching maintains proper hierarchical relationships: - * ``` - * Monthly (Aug 2025) - * ├── Weekly (Aug 18-24) - * │ ├── Daily (Aug 18) ← You are here - * │ ├── Daily (Aug 19) - * │ └── Daily (Aug 20) - * └── Weekly (Aug 25-31) - * ``` - * - * @author Tides Development Team - * @version 2.0.0 - * @since 2025-01-01 - */ - -import type { TideStorage } from '../storage'; - -/** - * Switches tide context to a different time scale view - * - * @description Allows users to switch between daily, weekly, and monthly contexts - * for the same underlying workflow data. Automatically creates the target context - * if it doesn't exist, maintaining proper hierarchical relationships. - * - * @param {Object} params - The context switching parameters - * @param {'daily'|'weekly'|'monthly'} params.context - Target time context - * @param {string} [params.date] - ISO date for context (defaults to today) - * @param {boolean} [params.create_if_missing=true] - Create context if it doesn't exist - * @param {TideStorage} storage - Storage instance with hierarchical support - * - * @returns {Promise} Promise resolving to context switch result - * - * @example - * // Switch to weekly view for current week - * const result = await tideSwitchContext({ - * context: "weekly" - * }, storage); - * - * // Switch to daily view for specific date - * const result = await tideSwitchContext({ - * context: "daily", - * date: "2025-08-15" - * }, storage); - * - * if (result.success) { - * // Use result.tide for display - * // result.hierarchy shows parent-child relationships - * } - * - * @since 2.0.0 - */ -export async function tideSwitchContext( - params: { - context: 'daily' | 'weekly' | 'monthly'; - date?: string; - create_if_missing?: boolean; - }, - storage: TideStorage & { - getTideByContext?: (context: 'daily' | 'weekly' | 'monthly', date: string) => Promise; - getOrCreateDailyTide?: (date: string) => Promise; - getOrCreateWeeklyTide?: (date: string) => Promise; - getOrCreateMonthlyTide?: (date: string) => Promise; - } -) { - try { - const targetDate = params.date || new Date().toISOString().split('T')[0]; - const createIfMissing = params.create_if_missing !== false; - - // If storage supports hierarchical context switching, use it - if (storage.getTideByContext) { - const tide = await storage.getTideByContext(params.context, targetDate); - - if (!tide && !createIfMissing) { - return { - success: false, - error: `${params.context} tide not found for ${targetDate}`, - }; - } - - // Get hierarchical context (parent and children) - const hierarchy = await buildHierarchyContext(storage, tide, params.context, targetDate); - - return { - success: true, - context: params.context, - date: targetDate, - tide: { - id: tide.id, - name: tide.name, - flow_type: tide.flow_type, - status: tide.status, - created_at: tide.created_at, - description: tide.description || "", - flow_count: tide.flow_sessions.length, - last_flow: tide.flow_sessions.length > 0 - ? tide.flow_sessions[tide.flow_sessions.length - 1].started_at - : null, - }, - hierarchy, - created: tide.created_at.split('T')[0] === targetDate, - }; - } - - // Fallback for non-hierarchical storage - const tides = await storage.listTides({ - flow_type: params.context, - active_only: true, - }); - - // Simple date-based matching for fallback - const contextTide = tides.find(t => - t.created_at.split('T')[0] === targetDate - ); - - if (!contextTide && !createIfMissing) { - return { - success: false, - error: `${params.context} tide not found for ${targetDate}`, - }; - } - - if (!contextTide) { - // Create new context using existing createTide - const { createTide } = await import('./tide-core'); - const { formatDate } = await import('../utils/date-utils'); - - const result = await createTide({ - name: `${params.context.charAt(0).toUpperCase() + params.context.slice(1)} - ${formatDate(targetDate)}`, - flow_type: params.context, - description: `${params.context} context for ${targetDate}`, - }, storage); - - if (!result.success) { - throw new Error(result.error); - } - - return { - success: true, - context: params.context, - date: targetDate, - tide: { - id: result.tide_id, - name: result.name, - flow_type: result.flow_type, - status: result.status, - created_at: result.created_at, - description: result.description, - flow_count: 0, - last_flow: null, - }, - hierarchy: null, // No hierarchy in fallback mode - created: true, - }; - } - - return { - success: true, - context: params.context, - date: targetDate, - tide: { - id: contextTide.id, - name: contextTide.name, - flow_type: contextTide.flow_type, - status: contextTide.status, - created_at: contextTide.created_at, - description: contextTide.description || "", - flow_count: contextTide.flow_sessions.length, - last_flow: contextTide.flow_sessions.length > 0 - ? contextTide.flow_sessions[contextTide.flow_sessions.length - 1].started_at - : null, - }, - hierarchy: null, // No hierarchy in fallback mode - created: false, - }; - - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Context switch failed', - }; - } -} - -/** - * Builds hierarchical context showing parent and child relationships - */ -async function buildHierarchyContext( - storage: any, - tide: any, - currentContext: 'daily' | 'weekly' | 'monthly', - date: string -): Promise { - const { getWeekStart, getWeekEnd, getMonthStart, getMonthEnd } = await import('../utils/date-utils'); - - const hierarchy: any = { - current: { - context: currentContext, - tide_id: tide.id, - date_range: { - start: tide.date_start || date, - end: tide.date_end || date, - } - }, - parent: null, - children: [], - }; - - try { - // Build parent context - if (currentContext === 'daily') { - // Parent is weekly - if (storage.getOrCreateWeeklyTide) { - const weeklyTide = await storage.getOrCreateWeeklyTide(date); - hierarchy.parent = { - context: 'weekly', - tide_id: weeklyTide.id, - name: weeklyTide.name, - date_range: { - start: getWeekStart(date), - end: getWeekEnd(date), - } - }; - } - } else if (currentContext === 'weekly') { - // Parent is monthly - if (storage.getOrCreateMonthlyTide) { - const monthlyTide = await storage.getOrCreateMonthlyTide(date); - hierarchy.parent = { - context: 'monthly', - tide_id: monthlyTide.id, - name: monthlyTide.name, - date_range: { - start: getMonthStart(date), - end: getMonthEnd(date), - } - }; - } - } - - // Build children contexts (simplified for now) - if (currentContext === 'monthly') { - hierarchy.children.push({ - context: 'weekly', - available: true, - description: 'Switch to weekly view for detailed patterns' - }); - } else if (currentContext === 'weekly') { - hierarchy.children.push({ - context: 'daily', - available: true, - description: 'Switch to daily view for detailed activities' - }); - } - - } catch (error) { - console.warn('Failed to build complete hierarchy:', error); - } - - return hierarchy; -} - -/** - * Lists available contexts for a given date range - * - * @description Provides information about available tide contexts that can be - * switched to, along with metadata about each context's availability and content. - * - * @param {Object} params - The context listing parameters - * @param {string} [params.date] - ISO date to check contexts for (defaults to today) - * @param {boolean} [params.include_empty=true] - Include contexts with no flow sessions - * @param {TideStorage} storage - Storage instance - * - * @returns {Promise} Promise resolving to available contexts - * - * @example - * const contexts = await tideListContexts({ - * date: "2025-08-23" - * }, storage); - * - * // Show available contexts in UI - * contexts.available.forEach(ctx => { - * console.log(`${ctx.context}: ${ctx.tide_name} (${ctx.flow_count} flows)`); - * }); - * - * @since 2.0.0 - */ -export async function tideListContexts( - params: { - date?: string; - include_empty?: boolean; - }, - storage: TideStorage -) { - try { - const targetDate = params.date || new Date().toISOString().split('T')[0]; - const includeEmpty = params.include_empty !== false; - - // Get all tides for the user to analyze available contexts - const allTides = await storage.listTides({}); - const { getWeekStart, getWeekEnd, getMonthStart, getMonthEnd } = await import('../utils/date-utils'); - - const contexts = { - daily: null as any, - weekly: null as any, - monthly: null as any, - }; - - const weekStart = getWeekStart(targetDate); - const weekEnd = getWeekEnd(targetDate); - const monthStart = getMonthStart(targetDate); - const monthEnd = getMonthEnd(targetDate); - - // Find matching contexts - for (const tide of allTides) { - if (tide.flow_type === 'daily' && tide.created_at.split('T')[0] === targetDate) { - contexts.daily = { - context: 'daily', - tide_id: tide.id, - tide_name: tide.name, - flow_count: tide.flow_sessions.length, - last_activity: tide.flow_sessions.length > 0 - ? tide.flow_sessions[tide.flow_sessions.length - 1].started_at - : null, - date_range: { start: targetDate, end: targetDate } - }; - } else if (tide.flow_type === 'weekly' && - tide.created_at >= weekStart && tide.created_at <= weekEnd) { - contexts.weekly = { - context: 'weekly', - tide_id: tide.id, - tide_name: tide.name, - flow_count: tide.flow_sessions.length, - last_activity: tide.flow_sessions.length > 0 - ? tide.flow_sessions[tide.flow_sessions.length - 1].started_at - : null, - date_range: { start: weekStart, end: weekEnd } - }; - } else if (tide.flow_type === 'monthly' && - tide.created_at >= monthStart && tide.created_at <= monthEnd) { - contexts.monthly = { - context: 'monthly', - tide_id: tide.id, - tide_name: tide.name, - flow_count: tide.flow_sessions.length, - last_activity: tide.flow_sessions.length > 0 - ? tide.flow_sessions[tide.flow_sessions.length - 1].started_at - : null, - date_range: { start: monthStart, end: monthEnd } - }; - } - } - - // Filter out empty contexts if requested - const available = Object.values(contexts) - .filter(ctx => ctx !== null && (includeEmpty || ctx.flow_count > 0)); - - return { - success: true, - date: targetDate, - available, - total_contexts: available.length, - can_create: { - daily: !contexts.daily, - weekly: !contexts.weekly, - monthly: !contexts.monthly, - } - }; - - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to list contexts', - available: [], - total_contexts: 0, - }; - } -} \ No newline at end of file diff --git a/apps/server/src/tools/tide-core.ts b/apps/server/src/tools/tide-core.ts index ef1a5ac..b49758c 100644 --- a/apps/server/src/tools/tide-core.ts +++ b/apps/server/src/tools/tide-core.ts @@ -9,10 +9,6 @@ * * ### Tide Workflows * A **tide** represents a recurring workflow or project pattern with its own rhythm: - * - **Daily tides**: Recurring daily activities (standup prep, morning routine) - * - **Weekly tides**: Weekly patterns (planning, reviews, retrospectives) - * - **Project tides**: One-time or irregular projects with defined scope - * - **Seasonal tides**: Long-term cyclical workflows (quarterly reviews, annual planning) * * ### Tide Lifecycle * ``` @@ -28,7 +24,6 @@ * interface Tide { * id: string; // Format: "tide_TIMESTAMP_HASH" * name: string; // User-friendly display name - * flow_type: FlowType; // Rhythm pattern (daily/weekly/monthly/project/seasonal) * description?: string; // Optional detailed description * status: TideStatus; // Lifecycle state (active/completed/paused) * created_at: string; // ISO timestamp of creation @@ -49,7 +44,6 @@ * // Create a new daily workflow * const morningTide = await createTide({ * name: "Morning Deep Work", - * flow_type: "daily", * description: "90-minute focused work session before meetings" * }, storage); * @@ -120,7 +114,6 @@ import type { TideStorage, CreateTideInput, TideFilter } from '../storage'; * * @param {Object} params - The tide creation parameters * @param {string} params.name - The display name for the tide (max 100 chars recommended) - * @param {'daily'|'weekly'|'monthly'|'project'|'seasonal'} params.flow_type - How often this tide flows * @param {string} [params.description] - Optional description (max 500 chars recommended) * @param {TideStorage} storage - Storage instance for persistence * @@ -130,7 +123,6 @@ import type { TideStorage, CreateTideInput, TideFilter } from '../storage'; * // React Native usage example * const result = await createTide({ * name: "Daily Standup Prep", - * flow_type: "daily", * description: "Prepare talking points for daily standup meeting" * }, storage); * @@ -145,7 +137,6 @@ import type { TideStorage, CreateTideInput, TideFilter } from '../storage'; export async function createTide( params: { name: string; - flow_type: 'daily' | 'weekly' | 'monthly' | 'project' | 'seasonal'; description?: string; }, storage: TideStorage @@ -153,7 +144,6 @@ export async function createTide( try { const input: CreateTideInput = { name: params.name, - flow_type: params.flow_type, description: params.description, }; @@ -161,23 +151,12 @@ export async function createTide( // Determine next flow time based on flow type let next_flow = null; - const now = new Date(); - - if (params.flow_type === "daily") { - next_flow = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString().split('T')[0] + " 09:00"; - } else if (params.flow_type === "weekly") { - next_flow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; - } else if (params.flow_type === "project") { - next_flow = "When project phase begins"; - } else if (params.flow_type === "seasonal") { - next_flow = "Next seasonal transition"; - } + return { success: true, tide_id: tide.id, name: tide.name, - flow_type: tide.flow_type, created_at: tide.created_at, status: tide.status, description: tide.description || "", @@ -199,16 +178,14 @@ export async function createTide( * information like flow count and last flow time. * * @param {Object} params - The filtering parameters - * @param {string} [params.flow_type] - Filter by flow type ('daily', 'weekly', 'monthly', 'project', 'seasonal') * @param {boolean} [params.active_only=false] - If true, only return active tides * @param {TideStorage} storage - Storage instance for data retrieval * * @returns {Promise} Promise resolving to tide list * * @example - * // Get all active daily tides + * // Get all active tides * const result = await listTides({ - * flow_type: "daily", * active_only: true * }, storage); * @@ -220,21 +197,18 @@ export async function createTide( */ export async function listTides( params: { - flow_type?: string; active_only?: boolean; }, storage: TideStorage ) { try { const tides = await storage.listTides({ - flow_type: params.flow_type, active_only: params.active_only, }); const formattedTides = tides.map(tide => ({ id: tide.id, name: tide.name, - flow_type: tide.flow_type, status: tide.status, created_at: tide.created_at, description: tide.description || "", @@ -260,22 +234,21 @@ export async function listTides( } /** - * Gets or creates a daily tide for today (or specified date) + * Gets or creates a tide for today (or specified date) * - * @description This is the key tool needed by mobile apps for automatic daily tide management. - * It ensures a daily tide exists for the current day and returns it, handling all the - * complexity of hierarchical tide creation and linking automatically. + * @description This is the key tool needed by mobile apps for automatic tide management. + * It ensures a tide exists for the current date and returns it. * - * @param {Object} params - The daily tide parameters + * @param {Object} params - The tide parameters * @param {string} [params.timezone] - User's timezone for date calculation (optional) * @param {string} [params.date] - Specific date in YYYY-MM-DD format (defaults to today) * @param {TideStorage} storage - Storage instance for persistence * - * @returns {Promise} Promise resolving to daily tide result + * @returns {Promise} Promise resolving to tide result * * @example - * // Mobile usage - get today's daily tide - * const result = await tideGetOrCreateDaily({ + * // Mobile usage - get today's tide + * const result = await tideGetOrCreate({ * timezone: "America/New_York" * }, storage); * @@ -286,48 +259,23 @@ export async function listTides( * * @since 2.0.0 */ -export async function tideGetOrCreateDaily( +export async function tideGetOrCreate( params: { timezone?: string; date?: string; }, - storage: TideStorage & { getOrCreateDailyTide?: (date: string) => Promise } + storage: TideStorage ) { try { // Calculate target date (use provided date or today) const targetDate = params.date || new Date().toISOString().split('T')[0]; - // If storage supports hierarchical operations, use them - if (storage.getOrCreateDailyTide) { - const tide = await storage.getOrCreateDailyTide(targetDate); - - return { - success: true, - tide: { - id: tide.id, - name: tide.name, - flow_type: tide.flow_type, - status: tide.status, - created_at: tide.created_at, - description: tide.description || "", - flow_count: tide.flow_sessions.length, - last_flow: tide.flow_sessions.length > 0 - ? tide.flow_sessions[tide.flow_sessions.length - 1].started_at - : null, - }, - created: tide.created_at.split('T')[0] === targetDate, // Approximate check for newly created - date: targetDate, - timezone: params.timezone || 'UTC', - }; - } - - // Fallback: use existing createTide if hierarchical not available + // Check for existing tide for the target date const existingTides = await storage.listTides({ - flow_type: 'daily', active_only: true, }); - // Check if we already have a daily tide for today (simple heuristic) + // Check if we already have a tide for today const todayTide = existingTides.find(t => t.created_at.split('T')[0] === targetDate ); @@ -338,7 +286,6 @@ export async function tideGetOrCreateDaily( tide: { id: todayTide.id, name: todayTide.name, - flow_type: todayTide.flow_type, status: todayTide.status, created_at: todayTide.created_at, description: todayTide.description || "", @@ -353,11 +300,10 @@ export async function tideGetOrCreateDaily( }; } - // Create new daily tide + // Create new tide for today const result = await createTide({ - name: `Daily Focus - ${new Date(targetDate).toLocaleDateString()}`, - flow_type: 'daily', - description: `Daily tide for ${targetDate}`, + name: `Focus - ${new Date(targetDate).toLocaleDateString()}`, + description: `Tide for ${targetDate}`, }, storage); if (result.success) { @@ -366,7 +312,6 @@ export async function tideGetOrCreateDaily( tide: { id: result.tide_id, name: result.name, - flow_type: result.flow_type, status: result.status, created_at: result.created_at, description: result.description, diff --git a/apps/server/src/tools/tide-hierarchical-flow.ts b/apps/server/src/tools/tide-hierarchical-flow.ts deleted file mode 100644 index 182e281..0000000 --- a/apps/server/src/tools/tide-hierarchical-flow.ts +++ /dev/null @@ -1,437 +0,0 @@ -/** - * @fileoverview Enhanced Hierarchical Flow Sessions - * - * This module provides enhanced flow session management that automatically distributes - * flow sessions across hierarchical tide contexts (daily, weekly, monthly). When a user - * starts a flow, it contributes to all relevant time contexts simultaneously. - * - * ## Key Features - * - * ### Automatic Context Distribution - * A single flow session automatically contributes to: - * - Daily tide for the session date - * - Weekly tide containing that date - * - Monthly tide containing that date - * - * ### Smart Auto-Creation - * Missing hierarchical contexts are created automatically with proper linking: - * ``` - * User starts flow → Daily tide created (if needed) - * → Weekly tide created (if needed) - * → Monthly tide created (if needed) - * → Flow session added to all three - * ``` - * - * ### Seamless User Experience - * Users don't need to think about tide management: - * - Just start working → system handles context creation - * - All time scales automatically updated - * - Natural journaling workflow maintained - * - * @author Tides Development Team - * @version 2.0.0 - * @since 2025-01-01 - */ - -import type { TideStorage } from '../storage'; - -/** - * Starts a hierarchical flow session that distributes across all relevant contexts - * - * @description This is the enhanced flow session function that implements the core - * hierarchical tide pattern from ADR-003. When called, it: - * 1. Auto-creates daily, weekly, monthly tides as needed - * 2. Adds the flow session to all relevant hierarchical contexts - * 3. Maintains proper parent-child relationships - * 4. Returns information about all affected contexts - * - * @param {Object} params - The hierarchical flow parameters - * @param {'gentle'|'moderate'|'strong'} [params.intensity='moderate'] - Work intensity level - * @param {number} [params.duration=25] - Session duration in minutes - * @param {string} [params.initial_energy='medium'] - Starting energy level - * @param {string} [params.work_context='General work'] - Description of work - * @param {string} [params.date] - Date for the session (defaults to today) - * @param {TideStorage} storage - Storage instance with hierarchical support - * - * @returns {Promise} Promise resolving to hierarchical flow result - * - * @example - * // Simple usage - just start working - * const result = await startHierarchicalFlow({ - * intensity: "moderate", - * duration: 25, - * work_context: "Code review for authentication PR" - * }, storage); - * - * if (result.success) { - * // Session automatically added to daily, weekly, monthly tides - * console.log(`Session created: ${result.session_id}`); - * console.log(`Contexts updated: ${result.contexts.length}`); - * } - * - * @since 2.0.0 - */ -export async function startHierarchicalFlow( - params: { - intensity?: 'gentle' | 'moderate' | 'strong'; - duration?: number; - initial_energy?: string; - work_context?: string; - date?: string; - }, - storage: TideStorage & { - getOrCreateDailyTide?: (date: string) => Promise; - getOrCreateWeeklyTide?: (date: string) => Promise; - getOrCreateMonthlyTide?: (date: string) => Promise; - } -) { - try { - const intensity = params.intensity || 'moderate'; - const duration = params.duration || 25; - const energy_level = params.initial_energy || 'medium'; - const work_context = params.work_context || 'General work'; - const sessionDate = params.date || new Date().toISOString().split('T')[0]; - const started_at = new Date().toISOString(); - - // Check if storage supports hierarchical operations - if (!storage.getOrCreateDailyTide || !storage.getOrCreateWeeklyTide || !storage.getOrCreateMonthlyTide) { - // Fallback to single tide flow session - return await fallbackFlowSession(params, storage); - } - - console.log(`🌊 Starting hierarchical flow for ${sessionDate}`); - - // Auto-create hierarchical tides - const [dailyTide, weeklyTide, monthlyTide] = await Promise.all([ - storage.getOrCreateDailyTide(sessionDate), - storage.getOrCreateWeeklyTide(sessionDate), - storage.getOrCreateMonthlyTide(sessionDate), - ]); - - console.log(`📊 Created/retrieved tides: daily=${dailyTide.id}, weekly=${weeklyTide.id}, monthly=${monthlyTide.id}`); - - // Create the flow session object - const sessionData = { - intensity, - duration, - started_at, - energy_level, - work_context, - }; - - // Add flow session to all relevant tides - const [dailySession, weeklySession, monthlySession] = await Promise.all([ - storage.addFlowSession(dailyTide.id, sessionData), - storage.addFlowSession(weeklyTide.id, sessionData), - storage.addFlowSession(monthlyTide.id, sessionData), - ]); - - console.log(`✅ Flow sessions created: ${dailySession.id}, ${weeklySession.id}, ${monthlySession.id}`); - - return { - success: true, - session_id: dailySession.id, // Use daily session as primary - date: sessionDate, - intensity, - duration, - started_at, - energy_level, - work_context, - contexts: [ - { - context: 'daily', - tide_id: dailyTide.id, - tide_name: dailyTide.name, - session_id: dailySession.id, - created: dailyTide.created_at.split('T')[0] === sessionDate, - }, - { - context: 'weekly', - tide_id: weeklyTide.id, - tide_name: weeklyTide.name, - session_id: weeklySession.id, - created: weeklyTide.created_at.split('T')[0] === sessionDate, - }, - { - context: 'monthly', - tide_id: monthlyTide.id, - tide_name: monthlyTide.name, - session_id: monthlySession.id, - created: monthlyTide.created_at.split('T')[0] === sessionDate, - } - ], - message: `Hierarchical flow session started across ${3} contexts`, - }; - - } catch (error) { - console.error('❌ Hierarchical flow session failed:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Hierarchical flow creation failed', - }; - } -} - -/** - * Fallback flow session for non-hierarchical storage - */ -async function fallbackFlowSession( - params: any, - storage: TideStorage -): Promise { - console.log('⚠️ Hierarchical storage not available, using fallback flow session'); - - try { - // Import and use the existing startTideFlow function - const { startTideFlow } = await import('./tide-sessions'); - - // Try to find or create a daily tide for today - const tides = await storage.listTides({ - flow_type: 'daily', - active_only: true, - }); - - const today = new Date().toISOString().split('T')[0]; - let dailyTide = tides.find(t => t.created_at.split('T')[0] === today); - - if (!dailyTide) { - // Create a daily tide for today - const { createTide } = await import('./tide-core'); - const result = await createTide({ - name: `Daily Focus - ${new Date().toLocaleDateString()}`, - flow_type: 'daily', - description: `Daily tide for ${today}`, - }, storage); - - if (!result.success) { - throw new Error(`Failed to create daily tide: ${result.error}`); - } - - // Need to get the actual tide object - const newTides = await storage.listTides({ - flow_type: 'daily', - active_only: true, - }); - dailyTide = newTides.find(t => t.id === result.tide_id); - } - - if (!dailyTide) { - throw new Error('Failed to find or create daily tide'); - } - - // Start flow session on the daily tide - const flowResult = await startTideFlow({ - tide_id: dailyTide.id, - intensity: params.intensity, - duration: params.duration, - initial_energy: params.initial_energy, - work_context: params.work_context, - }, storage); - - if (!flowResult.success) { - throw new Error(flowResult.error); - } - - // Return in hierarchical format for consistency - return { - success: true, - session_id: flowResult.session_id, - date: today, - intensity: flowResult.intensity, - duration: flowResult.duration, - started_at: flowResult.started_at, - energy_level: flowResult.energy_level, - work_context: flowResult.work_context, - contexts: [ - { - context: 'daily', - tide_id: dailyTide.id, - tide_name: dailyTide.name, - session_id: flowResult.session_id, - created: dailyTide.created_at.split('T')[0] === today, - } - ], - message: `Flow session started (fallback mode)`, - fallback_mode: true, - }; - - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Fallback flow session failed', - }; - } -} - -/** - * Gets today's hierarchical context summary - * - * @description Provides a summary of today's hierarchical tide contexts, - * showing flow sessions across daily, weekly, and monthly views. - * - * @param {Object} params - The context summary parameters - * @param {string} [params.date] - Date to get context for (defaults to today) - * @param {TideStorage} storage - Storage instance - * - * @returns {Promise} Promise resolving to context summary - * - * @example - * const summary = await getTodaysContextSummary({}, storage); - * - * // Show context summary in UI - * summary.contexts.forEach(ctx => { - * console.log(`${ctx.context}: ${ctx.flow_count} sessions, ${ctx.total_minutes} minutes`); - * }); - * - * @since 2.0.0 - */ -export async function getTodaysContextSummary( - params: { - date?: string; - }, - storage: TideStorage & { - getOrCreateDailyTide?: (date: string) => Promise; - getOrCreateWeeklyTide?: (date: string) => Promise; - getOrCreateMonthlyTide?: (date: string) => Promise; - } -) { - try { - const targetDate = params.date || new Date().toISOString().split('T')[0]; - - // If hierarchical storage not available, provide basic summary - if (!storage.getOrCreateDailyTide) { - const tides = await storage.listTides({ active_only: true }); - const dailyTides = tides.filter(t => - t.flow_type === 'daily' && - t.created_at.split('T')[0] === targetDate - ); - - const totalSessions = dailyTides.reduce((sum, t) => sum + t.flow_sessions.length, 0); - const totalMinutes = dailyTides.reduce((sum, t) => - sum + t.flow_sessions.reduce((s, session) => s + session.duration, 0), 0 - ); - - return { - success: true, - date: targetDate, - contexts: [ - { - context: 'daily', - flow_count: totalSessions, - total_minutes: totalMinutes, - tide_count: dailyTides.length, - available: dailyTides.length > 0, - } - ], - total_flow_sessions: totalSessions, - total_minutes: totalMinutes, - fallback_mode: true, - }; - } - - // Get hierarchical contexts (don't auto-create, just check what exists) - const tides = await storage.listTides({}); - const { getWeekStart, getWeekEnd, getMonthStart, getMonthEnd } = await import('../utils/date-utils'); - - const weekStart = getWeekStart(targetDate); - const weekEnd = getWeekEnd(targetDate); - const monthStart = getMonthStart(targetDate); - const monthEnd = getMonthEnd(targetDate); - - const contexts = []; - let totalSessions = 0; - let totalMinutes = 0; - - // Daily context - const dailyTide = tides.find(t => - t.flow_type === 'daily' && - t.created_at.split('T')[0] === targetDate - ); - - if (dailyTide) { - const dailyMinutes = dailyTide.flow_sessions.reduce((sum, s) => sum + s.duration, 0); - contexts.push({ - context: 'daily', - tide_id: dailyTide.id, - tide_name: dailyTide.name, - flow_count: dailyTide.flow_sessions.length, - total_minutes: dailyMinutes, - available: true, - }); - totalSessions += dailyTide.flow_sessions.length; - totalMinutes += dailyMinutes; - } else { - contexts.push({ - context: 'daily', - flow_count: 0, - total_minutes: 0, - available: false, - }); - } - - // Weekly context - const weeklyTide = tides.find(t => - t.flow_type === 'weekly' && - t.created_at >= weekStart && t.created_at <= weekEnd - ); - - if (weeklyTide) { - const weeklyMinutes = weeklyTide.flow_sessions.reduce((sum, s) => sum + s.duration, 0); - contexts.push({ - context: 'weekly', - tide_id: weeklyTide.id, - tide_name: weeklyTide.name, - flow_count: weeklyTide.flow_sessions.length, - total_minutes: weeklyMinutes, - available: true, - }); - } else { - contexts.push({ - context: 'weekly', - flow_count: 0, - total_minutes: 0, - available: false, - }); - } - - // Monthly context - const monthlyTide = tides.find(t => - t.flow_type === 'monthly' && - t.created_at >= monthStart && t.created_at <= monthEnd - ); - - if (monthlyTide) { - const monthlyMinutes = monthlyTide.flow_sessions.reduce((sum, s) => sum + s.duration, 0); - contexts.push({ - context: 'monthly', - tide_id: monthlyTide.id, - tide_name: monthlyTide.name, - flow_count: monthlyTide.flow_sessions.length, - total_minutes: monthlyMinutes, - available: true, - }); - } else { - contexts.push({ - context: 'monthly', - flow_count: 0, - total_minutes: 0, - available: false, - }); - } - - return { - success: true, - date: targetDate, - contexts, - total_flow_sessions: totalSessions, - total_minutes: totalMinutes, - }; - - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to get context summary', - contexts: [], - }; - } -} \ No newline at end of file diff --git a/apps/server/src/tools/tide-sessions.ts b/apps/server/src/tools/tide-sessions.ts index 3e91e97..0c85559 100644 --- a/apps/server/src/tools/tide-sessions.ts +++ b/apps/server/src/tools/tide-sessions.ts @@ -44,14 +44,14 @@ * ```typescript * // Morning energy check-in * await addTideEnergy({ - * tide_id: dailyTideId, + * tide_id: tideId, * energy_level: "high", * context: "Fresh start after coffee" * }, storage); * * // Post-lunch dip * await addTideEnergy({ - * tide_id: dailyTideId, + * tide_id: tideId, * energy_level: "low", * context: "Post-lunch energy dip" * }, storage); @@ -72,7 +72,7 @@ * ```typescript * interface FlowSession { * id: string; // Format: "session_TIMESTAMP_HASH" - * tide_id: string; // Parent tide ID + * tide_id: string; // Tide ID * intensity: 'gentle' | 'moderate' | 'strong'; * duration: number; // Minutes * started_at: string; // ISO timestamp @@ -87,7 +87,7 @@ * ```typescript * interface EnergyUpdate { * id: string; // Format: "energy_TIMESTAMP_HASH" - * tide_id: string; // Parent tide ID + * tide_id: string; // Tide ID * energy_level: string; // Energy level (1-10 or descriptive) * context: string; // What's affecting energy * timestamp: string; // ISO timestamp diff --git a/apps/server/src/tools/tide-tasks.ts b/apps/server/src/tools/tide-tasks.ts index eefb756..7d54e65 100644 --- a/apps/server/src/tools/tide-tasks.ts +++ b/apps/server/src/tools/tide-tasks.ts @@ -11,9 +11,9 @@ * ### Task Linking * Task links connect external work items to tides for context and tracking: * - **GitHub Issues/PRs**: Link development work to project tides - * - **Linear/Jira Tasks**: Connect product work to weekly/project tides + * - **Linear/Jira Tasks**: Connect product work to project tides * - **Obsidian Notes**: Link knowledge work to seasonal/research tides - * - **Calendar Events**: Connect meetings to daily tides + * - **Calendar Events**: Connect meetings to tides * - **General URLs**: Link any web resource to relevant tides * * ### Integration Patterns @@ -45,7 +45,7 @@ * * ### Project Management Integration * ```typescript - * // Link multiple tasks to weekly sprint tide + * // Link multiple tasks to sprint tide * const sprintTasks = [ * { url: "https://linear.app/team/issue/123", title: "User onboarding flow" }, * { url: "https://linear.app/team/issue/124", title: "Dashboard performance" }, @@ -54,7 +54,7 @@ * * for (const task of sprintTasks) { * await linkTideTask({ - * tide_id: weeklySprintTideId, + * tide_id: sprintTideId, * task_url: task.url, * task_title: task.title, * task_type: "linear_task" @@ -79,7 +79,7 @@ * ```typescript * interface TaskLink { * id: string; // Format: "link_TIMESTAMP_HASH" - * tide_id: string; // Parent tide ID + * tide_id: string; // Tide ID * task_url: string; // URL to external task * task_title: string; // Display title for the task * task_type: string; // System type (github_issue, linear_task, etc.) diff --git a/apps/server/src/utils/date-utils.ts b/apps/server/src/utils/date-utils.ts index 9c15b04..b09c680 100644 --- a/apps/server/src/utils/date-utils.ts +++ b/apps/server/src/utils/date-utils.ts @@ -1,6 +1,6 @@ /** - * Date utility functions for hierarchical tide context - * Handles date boundary calculations for daily/weekly/monthly tides + * Date utility functions for tide management + * Handles date boundary calculations and formatting */ /** diff --git a/apps/server/tests/agents/tide-productivity-agent-refactored.test.ts b/apps/server/tests/agents/tide-productivity-agent-refactored.test.ts index 55e9bd3..7b3f63a 100644 --- a/apps/server/tests/agents/tide-productivity-agent-refactored.test.ts +++ b/apps/server/tests/agents/tide-productivity-agent-refactored.test.ts @@ -167,7 +167,7 @@ describe('TideProductivityAgent (Refactored)', () => { result: { content: [{ text: JSON.stringify({ - tides: [{ id: 'tide_001', name: 'Test Tide', flow_type: 'daily' }] + tides: [{ id: 'tide_001', name: 'Test Tide' }] }) }] } diff --git a/apps/server/tests/debug-specific-template.test.ts b/apps/server/tests/debug-specific-template.test.ts index 0d870db..762a113 100644 --- a/apps/server/tests/debug-specific-template.test.ts +++ b/apps/server/tests/debug-specific-template.test.ts @@ -8,7 +8,6 @@ const realTemplate = `COMPREHENSIVE TIDE ANALYSIS REQUEST TIDE INFORMATION: - Name: {{tide.name}} -- Type: {{tide.flow_type}} - Description: {{tide.description || 'No description provided'}} - Created: {{tide.created_at}} - Status: {{tide.status || 'active'}} @@ -22,7 +21,6 @@ const realData = { tide: { id: 'tide_1754586971583_ph37cp2nze', name: 'Deep Work Day - Complete Test', - flow_type: 'daily', description: 'Full day of focused work with realistic energy patterns and task management', created_at: '2025-08-07T17:16:11.583Z', status: 'active' diff --git a/apps/server/tests/debug-template.test.ts b/apps/server/tests/debug-template.test.ts index 60fe601..b4c3677 100644 --- a/apps/server/tests/debug-template.test.ts +++ b/apps/server/tests/debug-template.test.ts @@ -34,7 +34,6 @@ describe('Template Processing Debug', () => { mockStorage.getTide.mockResolvedValue({ id: 'tide_test_123', name: 'Debug Test Tide', - flow_type: 'daily', description: 'Test tide for debugging templates', created_at: '2025-08-07T17:00:00.000Z', status: 'active' diff --git a/apps/server/tests/e2e/auth-check.test.ts b/apps/server/tests/e2e/auth-check.test.ts index b859679..d6a5ccd 100644 --- a/apps/server/tests/e2e/auth-check.test.ts +++ b/apps/server/tests/e2e/auth-check.test.ts @@ -149,14 +149,12 @@ const PROTECTED_TOOL_CALLS = [ name: 'tide_create', arguments: { name: 'Unauthorized Test Tide', - flow_type: 'daily', description: 'This should not be created' } }, { name: 'tide_list', arguments: { - flow_type: 'daily' } }, { @@ -277,7 +275,6 @@ describe('Authentication Check Tests - Unauthorized Access', () => { // Should NOT contain tide data expect(text).not.toMatch(/"tides"\s*:\s*\[/); expect(text).not.toMatch(/tide_\d+_[a-z0-9]+/); - expect(text).not.toMatch(/"flow_type"/); expect(text).not.toMatch(/"created_at"/); } }, testTimeout); @@ -294,7 +291,6 @@ describe('Authentication Check Tests - Unauthorized Access', () => { name: 'tide_create', arguments: { name: 'Unauthorized Tide Creation Test', - flow_type: 'daily', description: 'This should fail due to lack of authentication' } } diff --git a/apps/server/tests/e2e/health-check.test.ts b/apps/server/tests/e2e/health-check.test.ts index d86b3a2..5eb71a2 100644 --- a/apps/server/tests/e2e/health-check.test.ts +++ b/apps/server/tests/e2e/health-check.test.ts @@ -176,7 +176,6 @@ describe('Health Check Tests - All Environments', () => { name: 'tide_create', arguments: { name: tideName, - flow_type: 'daily', description: `Automated health check for ${env.name} environment` } }); @@ -188,7 +187,7 @@ describe('Health Check Tests - All Environments', () => { expect(tideData.tide_id).toBeDefined(); expect(tideData.tide_id).toMatch(/^tide_\d+_[a-z0-9]+$/); expect(tideData.name).toBe(tideName); - expect(tideData.flow_type).toBe('daily'); + expect(tideData.status).toBe('active'); expect(tideData.status).toBe('active'); expect(tideData.created_at).toBeDefined(); expect(tideData.description).toContain('Automated health check'); @@ -212,7 +211,6 @@ describe('Health Check Tests - All Environments', () => { name: 'tide_create', arguments: { name: `Storage Test Tide - ${env.name}`, - flow_type: 'project', description: 'Testing D1/R2 storage integration' } }); @@ -227,7 +225,6 @@ describe('Health Check Tests - All Environments', () => { const listResponse = await makeMCPRequest(env.url, env.apiKey, 'tools/call', { name: 'tide_list', arguments: { - flow_type: 'project' } }); @@ -239,7 +236,7 @@ describe('Health Check Tests - All Environments', () => { const foundTide = listData.tides.find((tide: any) => tide.id === createData.tide_id); expect(foundTide).toBeDefined(); expect(foundTide.name).toContain('Storage Test Tide'); - expect(foundTide.flow_type).toBe('project'); + expect(foundTide.status).toBe('active'); console.log(`✅ Storage test passed for ${env.name}: ${createData.tide_id}`); }, testTimeout); @@ -254,7 +251,6 @@ describe('Health Check Tests - All Environments', () => { name: 'tide_create', arguments: { name: `Analytics Test Tide - ${env.name}`, - flow_type: 'weekly', description: 'Testing analytics functionality' } }); @@ -276,7 +272,7 @@ describe('Health Check Tests - All Environments', () => { expect(reportData.report).toBeDefined(); expect(reportData.report.tide_id).toBe(createData.tide_id); expect(reportData.report.name).toContain('Analytics Test Tide'); - expect(reportData.report.flow_type).toBe('weekly'); + expect(reportData.report.status).toBe('active'); expect(reportData.report.total_flows).toBeDefined(); expect(reportData.report.created_at).toBeDefined(); @@ -293,7 +289,6 @@ describe('Health Check Tests - All Environments', () => { name: 'tide_create', arguments: { name: `Raw JSON Test Tide - ${env.name}`, - flow_type: 'daily', description: 'Testing raw JSON export functionality' } }); @@ -336,7 +331,7 @@ describe('Health Check Tests - All Environments', () => { // Verify complete data structure expect(rawData.data.id).toBe(createData.tide_id); expect(rawData.data.name).toContain('Raw JSON Test Tide'); - expect(rawData.data.flow_type).toBe('daily'); + expect(rawData.data.status).toBe('active'); expect(rawData.data.description).toContain('Testing raw JSON export'); // Verify all arrays are present and have data diff --git a/apps/server/tests/e2e/mcp-prompts.test.ts b/apps/server/tests/e2e/mcp-prompts.test.ts index 91e5534..c0c64e3 100644 --- a/apps/server/tests/e2e/mcp-prompts.test.ts +++ b/apps/server/tests/e2e/mcp-prompts.test.ts @@ -74,7 +74,6 @@ describe('MCP Prompts E2E Tests', () => { name: 'tide_create', arguments: { name: 'E2E Test Tide - MCP Prompts', - flow_type: 'project', description: 'Test tide for validating MCP prompts functionality' } }, diff --git a/apps/server/tests/fixtures/mock-tide-data.json b/apps/server/tests/fixtures/mock-tide-data.json index 948904f..db406aa 100644 --- a/apps/server/tests/fixtures/mock-tide-data.json +++ b/apps/server/tests/fixtures/mock-tide-data.json @@ -2,7 +2,6 @@ "id": "tide_1738366800000_comprehensive_test", "name": "Deep Work Sprint - Q1 Project", "description": "Comprehensive project tide for testing AI analysis prompts with varied patterns", - "flow_type": "project", "created_at": "2025-01-15T09:00:00.000Z", "updated_at": "2025-01-31T17:30:00.000Z", "status": "active", diff --git a/apps/server/tests/integration/multi-user-auth.test.ts b/apps/server/tests/integration/multi-user-auth.test.ts index 2dee969..aeffcaf 100644 --- a/apps/server/tests/integration/multi-user-auth.test.ts +++ b/apps/server/tests/integration/multi-user-auth.test.ts @@ -110,8 +110,8 @@ class MockD1StatementImpl implements MockD1Statement { if (this.query.includes('INSERT INTO tide_index')) { const tides = this.data.get('tide_index') || []; - const [id, user_id, name, flow_type, status, created_at, r2_path] = this.boundValues; - tides.push({ id, user_id, name, flow_type, status, created_at, r2_path, flow_count: 0, last_flow: null }); + const [id, user_id, name, status, created_at, r2_path] = this.boundValues; + tides.push({ id, user_id, name, status, created_at, r2_path, flow_count: 0, last_flow: null }); this.data.set('tide_index', tides); return { success: true }; } @@ -345,13 +345,11 @@ describe('Phase 4: Multi-User Authentication TDD', () => { // Create 2 tides for this user const tide1 = await storage.createTide({ name: `${user.id}'s First Tide`, - flow_type: 'daily', description: `Personal tide for ${user.id}` }); const tide2 = await storage.createTide({ name: `${user.id}'s Second Tide`, - flow_type: 'weekly', description: `Work tide for ${user.id}` }); @@ -418,7 +416,6 @@ describe('Phase 4: Multi-User Authentication TDD', () => { // Should use 'default-user' fallback for backwards compatibility const tide = await storage.createTide({ name: 'Default User Tide', - flow_type: 'daily', description: 'Test tide' }); @@ -484,13 +481,11 @@ describe('Phase 4: Multi-User Authentication TDD', () => { // Create 2 unique tides for this user const tide1 = await storage.createTide({ name: `${user.id} Daily Workflow`, - flow_type: 'daily', description: `Daily productivity tide for ${user.id}` }); const tide2 = await storage.createTide({ name: `${user.id} Project Focus`, - flow_type: 'project', description: `Project work tide for ${user.id}` }); @@ -591,7 +586,6 @@ describe('Phase 4: Multi-User Authentication TDD', () => { // Verify that operations work with proper auth context const tide = await storage.createTide({ name: 'Auth Test Tide', - flow_type: 'daily', description: 'Testing authentication integration' }); diff --git a/apps/server/tests/integration/r2-rest-storage.test.ts b/apps/server/tests/integration/r2-rest-storage.test.ts index 3cb74c3..ae5f63e 100644 --- a/apps/server/tests/integration/r2-rest-storage.test.ts +++ b/apps/server/tests/integration/r2-rest-storage.test.ts @@ -22,7 +22,6 @@ describe('R2RestApiStorage', () => { it('should create a tide and store via REST API', async () => { const input: CreateTideInput = { name: 'Test Tide', - flow_type: 'daily', description: 'A test tide' }; @@ -36,7 +35,7 @@ describe('R2RestApiStorage', () => { expect(tide.id).toMatch(/^tide_\d+_[a-z0-9]+$/); expect(tide.name).toBe('Test Tide'); - expect(tide.flow_type).toBe('daily'); + expect(tide.status).toBe('active'); expect(tide.description).toBe('A test tide'); expect(tide.status).toBe('active'); @@ -60,7 +59,6 @@ describe('R2RestApiStorage', () => { it('should handle API errors gracefully', async () => { const input: CreateTideInput = { name: 'Failed Tide', - flow_type: 'weekly' }; // Mock failed PUT response @@ -81,7 +79,6 @@ describe('R2RestApiStorage', () => { const tideData = { id: 'tide_123', name: 'Retrieved Tide', - flow_type: 'project' }; mockFetch.mockResolvedValueOnce({ @@ -132,7 +129,6 @@ describe('R2RestApiStorage', () => { { id: 'tide_1', name: 'First Tide', - flow_type: 'daily', status: 'active', created_at: '2025-07-31T10:00:00Z', flow_count: 5, @@ -141,7 +137,6 @@ describe('R2RestApiStorage', () => { { id: 'tide_2', name: 'Second Tide', - flow_type: 'weekly', status: 'completed', created_at: '2025-07-30T10:00:00Z', flow_count: 2, @@ -168,13 +163,12 @@ describe('R2RestApiStorage', () => { ); }); - it('should filter tides by flow_type', async () => { + it('should filter tides by active_only', async () => { const indexData = { tides: [ { id: 'tide_1', name: 'Daily Tide', - flow_type: 'daily', status: 'active', created_at: '2025-07-31T10:00:00Z', flow_count: 0, @@ -183,7 +177,6 @@ describe('R2RestApiStorage', () => { { id: 'tide_2', name: 'Weekly Tide', - flow_type: 'weekly', status: 'active', created_at: '2025-07-30T10:00:00Z', flow_count: 0, @@ -198,7 +191,7 @@ describe('R2RestApiStorage', () => { json: async () => indexData, }); - const result = await storage.listTides({ flow_type: 'daily' }); + const result = await storage.listTides({ active_only: true }); expect(result).toHaveLength(1); expect(result[0].name).toBe('Daily Tide'); diff --git a/apps/server/tests/integration/r2-storage.test.ts b/apps/server/tests/integration/r2-storage.test.ts index 39166ee..47dfe92 100644 --- a/apps/server/tests/integration/r2-storage.test.ts +++ b/apps/server/tests/integration/r2-storage.test.ts @@ -71,7 +71,6 @@ describe('R2TideStorage', () => { it('should create a tide and store as JSON file', async () => { const input: CreateTideInput = { name: 'Test Tide', - flow_type: 'daily', description: 'A test tide' }; @@ -79,7 +78,7 @@ describe('R2TideStorage', () => { expect(tide.id).toMatch(/^tide_\d+_[a-z0-9]+$/); expect(tide.name).toBe('Test Tide'); - expect(tide.flow_type).toBe('daily'); + expect(tide.status).toBe('active'); expect(tide.description).toBe('A test tide'); expect(tide.status).toBe('active'); expect(tide.created_at).toBeDefined(); @@ -102,14 +101,13 @@ describe('R2TideStorage', () => { it('should create tide without description', async () => { const input: CreateTideInput = { - name: 'Simple Tide', - flow_type: 'weekly' + name: 'Simple Tide' }; const tide = await storage.createTide(input); expect(tide.name).toBe('Simple Tide'); - expect(tide.flow_type).toBe('weekly'); + expect(tide.status).toBe('active'); expect(tide.description).toBeUndefined(); }); }); @@ -123,7 +121,6 @@ describe('R2TideStorage', () => { it('should return existing tide', async () => { const input: CreateTideInput = { name: 'Get Test Tide', - flow_type: 'project' }; const created = await storage.createTide(input); @@ -136,11 +133,11 @@ describe('R2TideStorage', () => { describe('listTides', () => { beforeEach(async () => { // Create test data - await storage.createTide({ name: 'Daily Tide', flow_type: 'daily' }); - await storage.createTide({ name: 'Weekly Tide', flow_type: 'weekly' }); + await storage.createTide({ name: 'Daily Tide' }); + await storage.createTide({ name: 'Weekly Tide' }); // Create an inactive tide - const inactiveTide = await storage.createTide({ name: 'Inactive Tide', flow_type: 'daily' }); + const inactiveTide = await storage.createTide({ name: 'Inactive Tide' }); await storage.updateTide(inactiveTide.id, { status: 'completed' }); }); @@ -149,10 +146,10 @@ describe('R2TideStorage', () => { expect(tides).toHaveLength(3); }); - it('should filter by flow_type', async () => { - const dailyTides = await storage.listTides({ flow_type: 'daily' }); - expect(dailyTides).toHaveLength(2); - expect(dailyTides.every(t => t.flow_type === 'daily')).toBe(true); + it('should filter by active_only', async () => { + const activeTides = await storage.listTides({ active_only: true }); + expect(activeTides).toHaveLength(2); + expect(activeTides.every(t => t.status === 'active')).toBe(true); }); it('should filter by active_only', async () => { @@ -162,12 +159,11 @@ describe('R2TideStorage', () => { }); it('should combine filters', async () => { - const activeDailyTides = await storage.listTides({ - flow_type: 'daily', + const activeTides = await storage.listTides({ active_only: true }); - expect(activeDailyTides).toHaveLength(1); - expect(activeDailyTides[0].name).toBe('Daily Tide'); + expect(activeTides).toHaveLength(2); + expect(activeTides.every(t => t.status === 'active')).toBe(true); }); it('should return empty array when no index exists', async () => { @@ -179,7 +175,7 @@ describe('R2TideStorage', () => { describe('updateTide', () => { it('should update existing tide', async () => { - const created = await storage.createTide({ name: 'Original', flow_type: 'daily' }); + const created = await storage.createTide({ name: 'Original' }); const updated = await storage.updateTide(created.id, { name: 'Updated Name', @@ -188,7 +184,7 @@ describe('R2TideStorage', () => { expect(updated.name).toBe('Updated Name'); expect(updated.status).toBe('paused'); - expect(updated.flow_type).toBe('daily'); // unchanged + expect(updated.name).toBe('Updated Name'); // name was changed // Check that file was updated const tideContent = mockR2.getContent(`tides/${created.id}.json`)!; @@ -207,7 +203,7 @@ describe('R2TideStorage', () => { let tideId: string; beforeEach(async () => { - const tide = await storage.createTide({ name: 'Session Test', flow_type: 'daily' }); + const tide = await storage.createTide({ name: 'Session Test' }); tideId = tide.id; }); @@ -257,7 +253,7 @@ describe('R2TideStorage', () => { let tideId: string; beforeEach(async () => { - const tide = await storage.createTide({ name: 'Energy Test', flow_type: 'daily' }); + const tide = await storage.createTide({ name: 'Energy Test' }); tideId = tide.id; }); @@ -290,7 +286,7 @@ describe('R2TideStorage', () => { let tideId: string; beforeEach(async () => { - const tide = await storage.createTide({ name: 'Link Test', flow_type: 'project' }); + const tide = await storage.createTide({ name: 'Link Test' }); tideId = tide.id; }); @@ -345,7 +341,7 @@ describe('R2TideStorage', () => { describe('index management', () => { it('should update index when tide flow count changes', async () => { - const tide = await storage.createTide({ name: 'Index Test', flow_type: 'daily' }); + const tide = await storage.createTide({ name: 'Index Test' }); // Add a flow session await storage.addFlowSession(tide.id, { @@ -366,7 +362,7 @@ describe('R2TideStorage', () => { it('should handle index updates gracefully on errors', async () => { // This test would require mocking R2 errors, but the main point is // that index update failures shouldn't break the main operation - const tide = await storage.createTide({ name: 'Error Test', flow_type: 'daily' }); + const tide = await storage.createTide({ name: 'Error Test' }); expect(tide).toBeDefined(); }); }); diff --git a/apps/server/tests/integration/storage-integration.test.ts b/apps/server/tests/integration/storage-integration.test.ts index 7a9efe8..a504a7f 100644 --- a/apps/server/tests/integration/storage-integration.test.ts +++ b/apps/server/tests/integration/storage-integration.test.ts @@ -84,7 +84,6 @@ describe('Critical Storage Integration Tests', () => { name: 'tide_create', arguments: { name: uniqueName, - flow_type: 'daily', description: `Storage integration test for ${env.name}` } }); @@ -101,7 +100,6 @@ describe('Critical Storage Integration Tests', () => { const listResponse = await makeMCPRequest(env.url, env.apiKey, 'tools/call', { name: 'tide_list', arguments: { - flow_type: 'daily' } }); @@ -124,7 +122,7 @@ describe('Critical Storage Integration Tests', () => { const retryResponse = await makeMCPRequest(env.url, env.apiKey, 'tools/call', { name: 'tide_list', - arguments: { flow_type: 'daily' } + arguments: {} }); const retryData = extractTideData(retryResponse); const retryFound = retryData.tides.find((tide: any) => tide.id === createData.tide_id); @@ -138,7 +136,6 @@ describe('Critical Storage Integration Tests', () => { expect(foundTide).toBeDefined(); expect(foundTide.name).toBe(uniqueName); - expect(foundTide.flow_type).toBe('daily'); console.log(`✅ Storage integration test passed for ${env.name}`); }, testTimeout); @@ -155,7 +152,6 @@ describe('Critical Storage Integration Tests', () => { name: 'tide_create', arguments: { name: uniqueName, - flow_type: 'project', description: `Rapid test cycle ${i}` } }); @@ -167,7 +163,7 @@ describe('Critical Storage Integration Tests', () => { // Immediately list const listResponse = await makeMCPRequest(env.url, env.apiKey, 'tools/call', { name: 'tide_list', - arguments: { flow_type: 'project' } + arguments: {} }); const listData = extractTideData(listResponse); @@ -198,7 +194,6 @@ describe('Critical Storage Integration Tests', () => { name: 'tide_create', arguments: { name: `${testName} - ENV001`, - flow_type: 'daily', description: 'Testing cross-environment isolation' } }); @@ -209,7 +204,7 @@ describe('Critical Storage Integration Tests', () => { // Verify it doesn't appear in tides-002 const listResponse2 = await makeMCPRequest(ENVIRONMENTS[1].url, ENVIRONMENTS[1].apiKey, 'tools/call', { name: 'tide_list', - arguments: { flow_type: 'daily' } + arguments: {} }); const listData2 = extractTideData(listResponse2); diff --git a/apps/server/tests/unit/storage.test.ts b/apps/server/tests/unit/storage.test.ts index 8fd2756..932796f 100644 --- a/apps/server/tests/unit/storage.test.ts +++ b/apps/server/tests/unit/storage.test.ts @@ -12,7 +12,6 @@ describe('MockTideStorage', () => { it('should create a tide with all required fields', async () => { const input: CreateTideInput = { name: 'Test Tide', - flow_type: 'daily', description: 'A test tide' }; @@ -20,7 +19,6 @@ describe('MockTideStorage', () => { expect(tide.id).toMatch(/^tide_\d+_\d+$/); expect(tide.name).toBe('Test Tide'); - expect(tide.flow_type).toBe('daily'); expect(tide.description).toBe('A test tide'); expect(tide.status).toBe('active'); expect(tide.created_at).toBeDefined(); @@ -31,14 +29,12 @@ describe('MockTideStorage', () => { it('should create a tide without description', async () => { const input: CreateTideInput = { - name: 'Simple Tide', - flow_type: 'weekly' + name: 'Simple Tide' }; const tide = await storage.createTide(input); expect(tide.name).toBe('Simple Tide'); - expect(tide.flow_type).toBe('weekly'); expect(tide.description).toBeUndefined(); }); }); @@ -52,7 +48,6 @@ describe('MockTideStorage', () => { it('should return existing tide', async () => { const input: CreateTideInput = { name: 'Get Test Tide', - flow_type: 'project' }; const created = await storage.createTide(input); @@ -65,11 +60,11 @@ describe('MockTideStorage', () => { describe('listTides', () => { beforeEach(async () => { // Create test data - await storage.createTide({ name: 'Daily Tide', flow_type: 'daily' }); - await storage.createTide({ name: 'Weekly Tide', flow_type: 'weekly' }); + await storage.createTide({ name: 'Daily Tide' }); + await storage.createTide({ name: 'Weekly Tide' }); // Create an inactive tide - const inactiveTide = await storage.createTide({ name: 'Inactive Tide', flow_type: 'daily' }); + const inactiveTide = await storage.createTide({ name: 'Inactive Tide' }); await storage.updateTide(inactiveTide.id, { status: 'completed' }); }); @@ -78,10 +73,9 @@ describe('MockTideStorage', () => { expect(tides).toHaveLength(3); }); - it('should filter by flow_type', async () => { - const dailyTides = await storage.listTides({ flow_type: 'daily' }); - expect(dailyTides).toHaveLength(2); - expect(dailyTides.every(t => t.flow_type === 'daily')).toBe(true); + it('should list all tides', async () => { + const allTides = await storage.listTides({}); + expect(allTides).toHaveLength(3); }); it('should filter by active_only', async () => { @@ -91,18 +85,16 @@ describe('MockTideStorage', () => { }); it('should combine filters', async () => { - const activeDailyTides = await storage.listTides({ - flow_type: 'daily', + const activeTides = await storage.listTides({ active_only: true }); - expect(activeDailyTides).toHaveLength(1); - expect(activeDailyTides[0].name).toBe('Daily Tide'); + expect(activeTides).toHaveLength(2); }); }); describe('updateTide', () => { it('should update existing tide', async () => { - const created = await storage.createTide({ name: 'Original', flow_type: 'daily' }); + const created = await storage.createTide({ name: 'Original' }); const updated = await storage.updateTide(created.id, { name: 'Updated Name', @@ -111,7 +103,7 @@ describe('MockTideStorage', () => { expect(updated.name).toBe('Updated Name'); expect(updated.status).toBe('paused'); - expect(updated.flow_type).toBe('daily'); // unchanged + expect(updated.name).toBe('Updated Name'); // name was changed }); it('should throw error for non-existent tide', async () => { @@ -124,7 +116,7 @@ describe('MockTideStorage', () => { let tideId: string; beforeEach(async () => { - const tide = await storage.createTide({ name: 'Session Test', flow_type: 'daily' }); + const tide = await storage.createTide({ name: 'Session Test' }); tideId = tide.id; }); @@ -168,7 +160,7 @@ describe('MockTideStorage', () => { let tideId: string; beforeEach(async () => { - const tide = await storage.createTide({ name: 'Energy Test', flow_type: 'daily' }); + const tide = await storage.createTide({ name: 'Energy Test' }); tideId = tide.id; }); @@ -208,7 +200,7 @@ describe('MockTideStorage', () => { let tideId: string; beforeEach(async () => { - const tide = await storage.createTide({ name: 'Link Test', flow_type: 'project' }); + const tide = await storage.createTide({ name: 'Link Test' }); tideId = tide.id; }); @@ -272,7 +264,7 @@ describe('MockTideStorage', () => { describe('helper methods', () => { it('should clear all data', () => { - storage.createTide({ name: 'Test', flow_type: 'daily' }); + storage.createTide({ name: 'Test' }); expect(storage.size()).toBe(1); storage.clear(); @@ -282,10 +274,10 @@ describe('MockTideStorage', () => { it('should return correct size', async () => { expect(storage.size()).toBe(0); - await storage.createTide({ name: 'Test 1', flow_type: 'daily' }); + await storage.createTide({ name: 'Test 1' }); expect(storage.size()).toBe(1); - await storage.createTide({ name: 'Test 2', flow_type: 'weekly' }); + await storage.createTide({ name: 'Test 2' }); expect(storage.size()).toBe(2); }); }); diff --git a/apps/server/tests/unit/tides-tools.test.ts b/apps/server/tests/unit/tides-tools.test.ts index c8af4a4..7549fca 100644 --- a/apps/server/tests/unit/tides-tools.test.ts +++ b/apps/server/tests/unit/tides-tools.test.ts @@ -12,45 +12,28 @@ describe('Tides Tools Functions', () => { it('should create a tide with valid input', async () => { const result = await tideTools.createTide({ name: 'Test Tide', - flow_type: 'daily', description: 'Test description' }, storage); expect(result.success).toBe(true); expect(result.name).toBe('Test Tide'); - expect(result.flow_type).toBe('daily'); expect(result.description).toBe('Test description'); expect(result.tide_id).toMatch(/^tide_\d+_\d+$/); expect(result.status).toBe('active'); expect(result.created_at).toBeDefined(); - expect(result.next_flow).toMatch(/^\d{4}-\d{2}-\d{2} 09:00$/); + expect(result.next_flow).toBeNull(); }); it('should create a tide without description', async () => { const result = await tideTools.createTide({ name: 'Minimal Tide', - flow_type: 'weekly' }, storage); expect(result.success).toBe(true); expect(result.name).toBe('Minimal Tide'); - expect(result.flow_type).toBe('weekly'); expect(result.description).toBe(''); }); - it('should handle all flow types', async () => { - const flowTypes = ['daily', 'weekly', 'project', 'seasonal'] as const; - - for (const flow_type of flowTypes) { - const result = await tideTools.createTide({ - name: `${flow_type} tide`, - flow_type - }, storage); - - expect(result.success).toBe(true); - expect(result.flow_type).toBe(flow_type); - } - }); }); describe('listTides', () => { @@ -58,13 +41,11 @@ describe('Tides Tools Functions', () => { // First create some test tides await tideTools.createTide({ name: 'Morning Deep Work', - flow_type: 'daily', description: '90-minute focus block for creative work' }, storage); await tideTools.createTide({ name: 'Weekly Review', - flow_type: 'weekly', description: 'Review progress and plan ahead' }, storage); @@ -79,7 +60,6 @@ describe('Tides Tools Functions', () => { const firstTide = result.tides[0]; expect(firstTide.id).toBeDefined(); expect(firstTide.name).toBeDefined(); - expect(firstTide.flow_type).toBeDefined(); expect(firstTide.status).toBeDefined(); expect(firstTide.created_at).toBeDefined(); }); @@ -88,22 +68,18 @@ describe('Tides Tools Functions', () => { // Create tides with different flow types await tideTools.createTide({ name: 'Daily Tide', - flow_type: 'daily' }, storage); await tideTools.createTide({ name: 'Weekly Tide', - flow_type: 'weekly' }, storage); const result = await tideTools.listTides({ - flow_type: 'daily', active_only: true }, storage); expect(result.success).toBe(true); - expect(result.tides).toHaveLength(1); - expect(result.tides[0].flow_type).toBe('daily'); + expect(result.tides).toHaveLength(2); // Both tides are active by default }); }); @@ -112,7 +88,6 @@ describe('Tides Tools Functions', () => { // First create a tide to flow with await tideTools.createTide({ name: 'Test Tide', - flow_type: 'daily' }, storage); const tides = await storage.listTides(); @@ -136,7 +111,6 @@ describe('Tides Tools Functions', () => { // First create a tide to flow with await tideTools.createTide({ name: 'Test Tide 2', - flow_type: 'weekly' }, storage); const tides = await storage.listTides(); @@ -164,7 +138,6 @@ describe('Tides Tools Functions', () => { // First create a tide to add energy to await tideTools.createTide({ name: 'Energy Test Tide', - flow_type: 'daily' }, storage); const tides = await storage.listTides(); @@ -189,7 +162,6 @@ describe('Tides Tools Functions', () => { // First create a tide to add energy to await tideTools.createTide({ name: 'Energy Test Tide 2', - flow_type: 'weekly' }, storage); const tides = await storage.listTides(); @@ -210,7 +182,6 @@ describe('Tides Tools Functions', () => { // First create a tide to link tasks to await tideTools.createTide({ name: 'Task Link Test Tide', - flow_type: 'project' }, storage); const tides = await storage.listTides(); @@ -237,7 +208,6 @@ describe('Tides Tools Functions', () => { // First create a tide to link tasks to await tideTools.createTide({ name: 'Task Link Test Tide 2', - flow_type: 'daily' }, storage); const tides = await storage.listTides(); @@ -259,7 +229,6 @@ describe('Tides Tools Functions', () => { // First create a tide and add some task links await tideTools.createTide({ name: 'List Links Test Tide', - flow_type: 'project' }, storage); const tides = await storage.listTides(); @@ -304,7 +273,6 @@ describe('Tides Tools Functions', () => { // Create a tide with some data await tideTools.createTide({ name: 'Raw JSON Test Tide', - flow_type: 'project', description: 'Test description' }, storage); @@ -359,7 +327,6 @@ describe('Tides Tools Functions', () => { // First create a tide with some data await tideTools.createTide({ name: 'Report Test Tide', - flow_type: 'daily' }, storage); const tides = await storage.listTides(); @@ -378,7 +345,6 @@ describe('Tides Tools Functions', () => { expect(result.report).toBeDefined(); expect((result as any).report.tide_id).toBe(tideId); expect((result as any).report.name).toBe('Report Test Tide'); - expect((result as any).report.flow_type).toBe('daily'); expect((result as any).report.total_flows).toBe(1); }); @@ -386,7 +352,6 @@ describe('Tides Tools Functions', () => { // First create a tide with some data await tideTools.createTide({ name: 'Markdown Report Tide', - flow_type: 'weekly' }, storage); const tides = await storage.listTides(); @@ -404,7 +369,7 @@ describe('Tides Tools Functions', () => { expect(result.format).toBe('markdown'); expect(result.content).toBeDefined(); expect(result.content).toContain('# Tide Report: Markdown Report Tide'); - expect(result.content).toContain('**Type:** weekly'); + expect(result.content).toContain('**Status:** active'); expect(result.content).toContain('## Energy Progression'); }); @@ -412,7 +377,6 @@ describe('Tides Tools Functions', () => { // First create a tide with some data await tideTools.createTide({ name: 'CSV Report Tide', - flow_type: 'project' }, storage); const tides = await storage.listTides(); @@ -484,7 +448,6 @@ describe('Tides Tools Functions', () => { const result = await tideTools.createTide({ name: 'Test Tide', - flow_type: 'daily' }, brokenStorage as any); expect(result.success).toBe(false); diff --git a/docs/README.md b/docs/README.md index 7cdb5e0..fca19e4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,7 @@ - **What is Tides?** → [Overview](core/what-is-tides.md) - **Development Setup** → [Development Guide](core/development.md) - **System Architecture** → [Architecture](core/architecture.md) -- **API Reference** → [API Docs](core/api-reference.md) +- **API Reference** → [API Docs](archive/api-reference.md) ## Main Sections @@ -45,7 +45,7 @@ Historical/deprecated documentation **New Developer:** [What is Tides?](core/what-is-tides.md) → [Development](core/development.md) **Mobile Dev:** [Mobile Docs](mobile/) → [Auth Flow](archive/mobile-mcp-auth-flow.md) -**Backend Dev:** [API Reference](core/api-reference.md) → [Auth System](auth/hybrid-auth-system.md) +**Backend Dev:** [API Reference](archive/api-reference.md) → [Auth System](auth/hybrid-auth-system.md) ## Contributing diff --git a/docs/adr/003-hierachal-tide-context.md b/docs/archive/003-hierachal-tide-context.md similarity index 98% rename from docs/adr/003-hierachal-tide-context.md rename to docs/archive/003-hierachal-tide-context.md index 294bb4a..a98926b 100644 --- a/docs/adr/003-hierachal-tide-context.md +++ b/docs/archive/003-hierachal-tide-context.md @@ -206,7 +206,7 @@ server.registerTool("tide_flow", { const flowSession = await createFlowSession(args); // Auto-create and link to hierarchical tides - const dailyTide = await getOrCreateDailyTide(today); + const dailyTide = await getOrCreateTide(today); const weeklyTide = await getOrCreateWeeklyTide(today); const monthlyTide = await getOrCreateMonthlyTide(today); @@ -301,7 +301,7 @@ const switchContext = async (newContext: "daily" | "weekly" | "monthly") => { ```typescript // Daily tide creation -async function getOrCreateDailyTide(date: string): Promise { +async function getOrCreateTide(date: string): Promise { const existing = await findTideByDateAndType(date, "daily"); if (existing) return existing; diff --git a/docs/core/api-reference.md b/docs/archive/api-reference.md similarity index 100% rename from docs/core/api-reference.md rename to docs/archive/api-reference.md diff --git a/docs/in-review/TIME_DISPLAY_STYLE_ISSUE.md b/docs/in-review/TIME_DISPLAY_STYLE_ISSUE.md new file mode 100644 index 0000000..4a7edcf --- /dev/null +++ b/docs/in-review/TIME_DISPLAY_STYLE_ISSUE.md @@ -0,0 +1,43 @@ +# Time Context Display Changes + +## Current State + +We have 3 time contexts: + +- **Daily**: Today's 24 hours +- **Weekly**: Sunday through Saturday (current week) +- **Monthly**: Full calendar view (current month) + +## Proposed Changes + +Replace the current "daily", "weekly", and "monthly" toggles with a new set of time period options: + +### New Time Context Options + +1. **1D** ("1day") + - Operates exactly the same as current "daily" + - Shows 24-hour period + +2. **3D** ("3day") + - Shows today + yesterday + day before (72-hour period) + - Most current day displayed on the right + - Oldest day displayed on the left + +3. **1W** ("1 week") + - Shows average rating of current day + last 6 days (7 days total) + - Current day positioned on the right side + +4. **1M** ("1 month") + - Shows today plus the last 29 days (30 days total) + +5. **3M** ("3 months") + - Shows today plus the last 89 days (90 days total) + +6. **1Y** ("1 year") + - Shows today plus the last 364 days (365 days total) + +## Implementation Notes + +- All new contexts maintain chronological order with current day on the right +- Multi-day contexts show individual days, not aggregated data (except 1W which shows averages) +- Transition from existing daily/weekly/monthly system to new 1D/3D/1W/1M/3M/1Y system diff --git a/shared/types/mcp-tools.ts b/shared/types/mcp-tools.ts index 74e2914..d7d498c 100644 --- a/shared/types/mcp-tools.ts +++ b/shared/types/mcp-tools.ts @@ -18,9 +18,7 @@ export const MCP_TOOLS = { TIDE_GET_RAW_JSON: 'tide_get_raw_json', TIDES_GET_PARTICIPANTS: 'tides_get_participants', - // Hierarchical Flow Tools - TIDE_GET_OR_CREATE_DAILY: 'tide_get_or_create_daily', - TIDE_START_HIERARCHICAL_FLOW: 'tide_start_hierarchical_flow', + tide_get_or_create: 'tide_get_or_create', TIDE_GET_TODAYS_SUMMARY: 'tide_get_todays_summary', TIDE_LIST_CONTEXTS: 'tide_list_contexts', TIDE_SWITCH_CONTEXT: 'tide_switch_context', @@ -29,12 +27,10 @@ export const MCP_TOOLS = { // Tool Parameter Types export interface TideCreateParams { name: string; - flow_type: 'daily' | 'weekly' | 'monthly' | 'project' | 'seasonal'; description?: string; } export interface TideListParams { - flow_type?: string; active_only?: boolean; } @@ -79,20 +75,11 @@ export interface TidesGetParticipantsParams { limit?: number; } -// Hierarchical Tool Parameters -export interface TideGetOrCreateDailyParams { +export interface TideGetOrCreateParams { timezone?: string; date?: string; } -export interface TideStartHierarchicalFlowParams { - intensity?: 'gentle' | 'moderate' | 'strong'; - duration?: number; - initial_energy?: string; - work_context?: string; - date?: string; -} - export interface TideGetTodaysSummaryParams { date?: string; } @@ -102,11 +89,6 @@ export interface TideListContextsParams { include_empty?: boolean; } -export interface TideSwitchContextParams { - context_type: 'daily' | 'weekly' | 'monthly' | 'project'; - date?: string; -} - // Response Types (common patterns) export interface MCPSuccessResponse { success: true; @@ -124,7 +106,6 @@ export type MCPResponse = MCPSuccessResponse | MCPErrorResponse; export interface FlowType { id: string; name: string; - flow_type: string; status: string; created_at: string; description?: string; @@ -162,7 +143,6 @@ export interface FlowSession { export interface TideCreateResponse extends MCPSuccessResponse { tide_id: string; name: string; - flow_type: string; created_at: string; status: string; description: string; @@ -174,23 +154,6 @@ export interface TideListResponse extends MCPSuccessResponse { count: number; } -export interface HierarchicalFlowResponse extends MCPSuccessResponse { - session_id: string; - date: string; - intensity: string; - duration: number; - started_at: string; - energy_level: string; - work_context: string; - contexts: Array<{ - context: string; - tide_id: string; - tide_name: string; - session_id: string; - created: boolean; - }>; - message: string; -} // Type guard utilities export function isMCPError(response: MCPResponse): response is MCPErrorResponse {