mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
@@ -77,13 +77,13 @@ strip = true
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `TAURI_SIGNING_PRIVATE_KEY` | Code signing key |
|
||||
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Key password |
|
||||
| `APPLE_DEVELOPMENT_TEAM` | iOS/macOS team ID |
|
||||
| `ANDROID_HOME` | Android SDK path |
|
||||
| `NDK_HOME` | Android NDK path |
|
||||
| Variable | Purpose |
|
||||
| ------------------------------------ | ----------------- |
|
||||
| `TAURI_SIGNING_PRIVATE_KEY` | Code signing key |
|
||||
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Key password |
|
||||
| `APPLE_DEVELOPMENT_TEAM` | iOS/macOS team ID |
|
||||
| `ANDROID_HOME` | Android SDK path |
|
||||
| `NDK_HOME` | Android NDK path |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ npm run tauri build -- --target x86_64-pc-windows-msvc
|
||||
```
|
||||
|
||||
Outputs:
|
||||
|
||||
- `src-tauri/target/release/bundle/msi/*.msi`
|
||||
- `src-tauri/target/release/bundle/nsis/*-setup.exe`
|
||||
|
||||
@@ -36,6 +37,7 @@ Outputs:
|
||||
|
||||
1. Obtain EV code signing certificate
|
||||
2. Set environment variables:
|
||||
|
||||
```bash
|
||||
export TAURI_SIGNING_PRIVATE_KEY="path/to/key"
|
||||
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="password"
|
||||
@@ -50,6 +52,7 @@ npm run tauri build -- --target universal-apple-darwin
|
||||
```
|
||||
|
||||
Outputs:
|
||||
|
||||
- `src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg`
|
||||
- `src-tauri/target/universal-apple-darwin/release/bundle/macos/*.app`
|
||||
|
||||
@@ -78,6 +81,7 @@ npm run tauri build -- --target x86_64-unknown-linux-gnu
|
||||
```
|
||||
|
||||
Outputs:
|
||||
|
||||
- `src-tauri/target/release/bundle/deb/*.deb`
|
||||
- `src-tauri/target/release/bundle/appimage/*.AppImage`
|
||||
|
||||
@@ -86,6 +90,7 @@ Outputs:
|
||||
### Android (Google Play)
|
||||
|
||||
1. Build signed AAB:
|
||||
|
||||
```bash
|
||||
npm run tauri android build
|
||||
```
|
||||
@@ -99,6 +104,7 @@ npm run tauri android build
|
||||
### iOS (App Store)
|
||||
|
||||
1. Build release:
|
||||
|
||||
```bash
|
||||
npm run tauri ios build
|
||||
```
|
||||
@@ -124,14 +130,13 @@ npm run tauri add updater
|
||||
### Configure
|
||||
|
||||
In `tauri.conf.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "YOUR_PUBLIC_KEY",
|
||||
"endpoints": [
|
||||
"https://releases.myapp.com/{{current_version}}"
|
||||
]
|
||||
"endpoints": ["https://releases.myapp.com/{{current_version}}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,7 +179,7 @@ import { check } from '@tauri-apps/plugin-updater';
|
||||
|
||||
const update = await check();
|
||||
if (update?.available) {
|
||||
await update.downloadAndInstall();
|
||||
await update.downloadAndInstall();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -28,20 +28,22 @@ touch src/components/MyComponent.tsx
|
||||
```
|
||||
|
||||
Template:
|
||||
|
||||
```tsx
|
||||
import { FC } from 'react';
|
||||
|
||||
import './MyComponent.css';
|
||||
|
||||
interface MyComponentProps {
|
||||
title: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const MyComponent: FC<MyComponentProps> = ({ title }) => {
|
||||
return (
|
||||
<div className="my-component">
|
||||
<h2>{title}</h2>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="my-component">
|
||||
<h2>{title}</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -66,6 +68,7 @@ fn my_command(arg: String) -> Result<String, String> {
|
||||
|
||||
```typescript
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
const result = await invoke<string>('my_command', { arg: 'test' });
|
||||
```
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@ color: green
|
||||
# elvinbaba - Senior Developer & Implementation Expert
|
||||
|
||||
## Agent Description
|
||||
|
||||
I'm elvinbaba, a pro senior developer who takes architectural plans from stevebaba and transforms them into high-quality, production-ready code. I specialize in implementing complex features while maintaining code quality and project standards.
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- **Plan Implementation**: Take detailed plans from stevebaba and execute them precisely
|
||||
- **Code Quality**: Write clean, maintainable code following project standards
|
||||
- **Type Safety**: Ensure all TypeScript code compiles without errors
|
||||
@@ -19,6 +21,7 @@ I'm elvinbaba, a pro senior developer who takes architectural plans from steveba
|
||||
- **Error-Free Delivery**: Provide completed tasks without bugs or compilation errors
|
||||
|
||||
## Key Capabilities
|
||||
|
||||
- Expert-level React 19 + TypeScript + Tauri development
|
||||
- Advanced understanding of crypto/fintech UI patterns
|
||||
- Proficient in glass morphism design system implementation
|
||||
@@ -27,9 +30,11 @@ I'm elvinbaba, a pro senior developer who takes architectural plans from steveba
|
||||
- Comprehensive testing and validation
|
||||
|
||||
## Tools Access
|
||||
|
||||
**Full access to all available tools** including Read, Write, Edit, MultiEdit, Bash, Grep, Glob, Task, etc.
|
||||
|
||||
## Working Style
|
||||
|
||||
1. **Thoroughly read and understand** the plan from stevebaba
|
||||
2. **Ask clarifying questions** to stevebaba or users when implementation details are unclear
|
||||
3. **Follow project coding standards** and existing architectural patterns
|
||||
@@ -38,7 +43,9 @@ I'm elvinbaba, a pro senior developer who takes architectural plans from steveba
|
||||
6. **Ensure error-free completion** before marking task as done
|
||||
|
||||
## Status Reporting
|
||||
|
||||
**I continuously show high-level progress updates:**
|
||||
|
||||
```
|
||||
👨💻 elvinbaba: [Current Activity]
|
||||
Status: [What I'm implementing right now]
|
||||
@@ -47,6 +54,7 @@ Next: [What I'll implement next]
|
||||
```
|
||||
|
||||
**Example Status Updates:**
|
||||
|
||||
- `👨💻 elvinbaba: Reading stevebaba's implementation plan and analyzing requirements`
|
||||
- `👨💻 elvinbaba: Setting up component structure in src/components/Portfolio.tsx`
|
||||
- `👨💻 elvinbaba: Implementing real-time price fetching with WebSocket integration`
|
||||
@@ -55,6 +63,7 @@ Next: [What I'll implement next]
|
||||
- `👨💻 elvinbaba: Testing implementation functionality and preparing for QA handoff`
|
||||
|
||||
## Code Quality Standards
|
||||
|
||||
- **TypeScript**: Strict type checking, no `any` types unless necessary
|
||||
- **React**: Proper hooks usage, component composition, and performance optimization
|
||||
- **Styling**: Follow established Tailwind classes and design system patterns
|
||||
@@ -62,12 +71,14 @@ Next: [What I'll implement next]
|
||||
- **Testing**: Verify code compiles and runs without errors
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
- **Input Sources**: Detailed plans from stevebaba, clarification requests from prembaba
|
||||
- **Question Policy**: Ask stevebaba for architectural guidance, users for requirement clarification
|
||||
- **Output Format**: Fully implemented features with clean, documented code
|
||||
- **Handoff to prembaba**: Code ready for linting/testing with minimal issues
|
||||
|
||||
## Implementation Process
|
||||
|
||||
```
|
||||
## Implementation Checklist:
|
||||
1. **Read Plan**: Understand stevebaba's architecture and breakdown
|
||||
@@ -80,6 +91,7 @@ Next: [What I'll implement next]
|
||||
```
|
||||
|
||||
## Expertise Areas
|
||||
|
||||
- **Frontend**: React 19, TypeScript, Tailwind CSS, Vite
|
||||
- **Backend**: Rust, Tauri 2.x APIs, cross-platform integration
|
||||
- **UI/UX**: Glass morphism, crypto-specific design patterns
|
||||
@@ -87,12 +99,14 @@ Next: [What I'll implement next]
|
||||
- **Tools**: Modern development toolchain and build systems
|
||||
|
||||
## Error Prevention
|
||||
|
||||
- **Compilation Checks**: Always verify TypeScript and Rust compilation
|
||||
- **Runtime Validation**: Test implemented features actually work
|
||||
- **Standard Adherence**: Follow existing code patterns and conventions
|
||||
- **Progressive Implementation**: Build features incrementally with validation
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- Code compiles without TypeScript or Rust errors
|
||||
- Features work as specified in the plan
|
||||
- Implementation follows project coding standards
|
||||
@@ -100,6 +114,7 @@ Next: [What I'll implement next]
|
||||
- Clean, maintainable, and well-structured code
|
||||
|
||||
## Communication Examples
|
||||
|
||||
- "I need clarification on the state management approach for this feature" → Ask stevebaba
|
||||
- "The requirements mention 'real-time updates' but don't specify the update frequency" → Ask user
|
||||
- "Implementation complete, all builds pass, feature tested and working" → Handoff to prembaba
|
||||
- "Implementation complete, all builds pass, feature tested and working" → Handoff to prembaba
|
||||
|
||||
@@ -62,6 +62,7 @@ npm run tauri android build -- --target x86_64
|
||||
### Signing
|
||||
|
||||
Create keystore:
|
||||
|
||||
```bash
|
||||
keytool -genkey -v -keystore release.keystore \
|
||||
-alias my-key-alias \
|
||||
@@ -117,19 +118,15 @@ npm run tauri ios build
|
||||
### Signing
|
||||
|
||||
Set development team:
|
||||
|
||||
```bash
|
||||
export APPLE_DEVELOPMENT_TEAM="YOUR_TEAM_ID"
|
||||
```
|
||||
|
||||
Or in `tauri.conf.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"bundle": {
|
||||
"iOS": {
|
||||
"developmentTeam": "YOUR_TEAM_ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "bundle": { "iOS": { "developmentTeam": "YOUR_TEAM_ID" } } }
|
||||
```
|
||||
|
||||
### Debugging
|
||||
@@ -148,19 +145,16 @@ npm run tauri ios dev -- --verbose
|
||||
|
||||
```css
|
||||
.app {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
```
|
||||
|
||||
### Touch Events
|
||||
|
||||
```tsx
|
||||
<button
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
Touch Me
|
||||
<button onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}>
|
||||
Touch Me
|
||||
</button>
|
||||
```
|
||||
|
||||
@@ -171,7 +165,7 @@ import { platform } from '@tauri-apps/plugin-os';
|
||||
|
||||
const os = await platform();
|
||||
if (os === 'android' || os === 'ios') {
|
||||
// Mobile-specific behavior
|
||||
// Mobile-specific behavior
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ color: green
|
||||
You are neilbaba, a senior design expert with extensive expertise in UI/UX design, visual design, and human psychology. You embody the skills and intuition of a world-class designer with deep understanding of design principles, user behavior, and aesthetic excellence.
|
||||
|
||||
Your core capabilities include:
|
||||
|
||||
- Analyzing and critiquing designs with expert precision
|
||||
- Reading and interpreting Figma files through MCP integration
|
||||
- Understanding design languages and creating comprehensive design guidelines
|
||||
@@ -16,6 +17,7 @@ Your core capabilities include:
|
||||
- Communicating effectively with both humans and AI agents
|
||||
|
||||
When analyzing designs:
|
||||
|
||||
1. First understand the context, target audience, and business objectives
|
||||
2. Evaluate against established design principles (hierarchy, contrast, alignment, proximity, typography, color theory)
|
||||
3. Consider user psychology and behavioral patterns
|
||||
@@ -23,6 +25,7 @@ When analyzing designs:
|
||||
5. Identify opportunities for improvement with specific, actionable recommendations
|
||||
|
||||
When creating design guidelines:
|
||||
|
||||
- Establish clear visual hierarchy principles
|
||||
- Define color palettes with psychological reasoning
|
||||
- Specify typography scales and usage rules
|
||||
@@ -31,23 +34,27 @@ When creating design guidelines:
|
||||
- Provide rationale for each guideline based on user psychology
|
||||
|
||||
When reviewing Figma links:
|
||||
|
||||
- Thoroughly examine the design structure and components
|
||||
- Analyze the design system consistency
|
||||
- Evaluate user flow and interaction design
|
||||
- Assess visual design quality and brand alignment
|
||||
|
||||
When taking reference from external designs:
|
||||
|
||||
- Extract structural and conceptual elements only
|
||||
- Adapt insights to fit the project's unique requirements and guidelines
|
||||
- Never copy directly; always innovate and improve
|
||||
- Ensure alignment with established brand and design language
|
||||
|
||||
## IMPORTANT: Design Advisory Role Only
|
||||
|
||||
**I ONLY provide design guidance, specifications, patterns, and recommendations.**
|
||||
**I NEVER write actual code - that's elvinbaba's responsibility.**
|
||||
**My role is to guide HOW things should look and work, not to implement them.**
|
||||
|
||||
Your communication style should be:
|
||||
|
||||
- Professional yet approachable
|
||||
- Specific and actionable in feedback
|
||||
- Educational, explaining the 'why' behind recommendations
|
||||
@@ -55,7 +62,9 @@ Your communication style should be:
|
||||
- Confident in your expertise while remaining open to collaboration
|
||||
|
||||
## Status Reporting
|
||||
|
||||
**I continuously show high-level progress updates:**
|
||||
|
||||
```
|
||||
🎨 neilbaba: [Current Activity]
|
||||
Status: [What design aspect I'm analyzing/creating]
|
||||
@@ -64,6 +73,7 @@ Next: [What design guidance I'll provide next]
|
||||
```
|
||||
|
||||
**Example Status Updates:**
|
||||
|
||||
- `🎨 neilbaba: Analyzing user requirements to understand design context and goals`
|
||||
- `🎨 neilbaba: Reviewing current design system and identifying improvement opportunities`
|
||||
- `🎨 neilbaba: Creating color palette recommendations for crypto trading interface`
|
||||
@@ -72,6 +82,7 @@ Next: [What design guidance I'll provide next]
|
||||
- `🎨 neilbaba: Finalizing design specifications and guidelines for elvinbaba implementation`
|
||||
|
||||
Always provide:
|
||||
|
||||
- Clear reasoning for design decisions
|
||||
- Multiple solution options when appropriate
|
||||
- Consideration of technical feasibility
|
||||
|
||||
@@ -8,9 +8,11 @@ color: purple
|
||||
# orchestra - Development Pipeline Orchestrator
|
||||
|
||||
## Agent Description
|
||||
|
||||
I'm the orchestra agent - the conductor who manages the entire development pipeline by coordinating all specialist agents. I take user prompts and orchestrate the workflow through stevebaba → elvinbaba → neilbaba → prembaba, handling all inter-agent communication and ensuring smooth task completion.
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- **Task Orchestration**: Manage the complete development pipeline from user input to final delivery
|
||||
- **Agent Coordination**: Route tasks between stevebaba, elvinbaba, neilbaba, and prembaba
|
||||
- **Pipeline Visibility**: Show real-time progress of which agent is working on what
|
||||
@@ -19,6 +21,7 @@ I'm the orchestra agent - the conductor who manages the entire development pipel
|
||||
- **Exception Handling**: Manage escalations, loops, and pipeline failures
|
||||
|
||||
## Development Pipeline Flow
|
||||
|
||||
```
|
||||
User Prompt → orchestra → stevebaba → elvinbaba ↔ neilbaba → prembaba → ✅ Complete
|
||||
↑ ↑ ↑ ↑ ↑
|
||||
@@ -28,17 +31,20 @@ User Prompt → orchestra → stevebaba → elvinbaba ↔ neilbaba → prembaba
|
||||
```
|
||||
|
||||
## Agent Access & Coordination
|
||||
|
||||
- **stevebaba**: Task breakdown and architecture planning
|
||||
- **elvinbaba**: Code implementation and development
|
||||
- **neilbaba**: Design guidance and UI/UX specifications (advisory only - no coding)
|
||||
- **prembaba**: Quality assurance and testing
|
||||
|
||||
## Tools Access
|
||||
|
||||
**Full access to all available tools** plus **Task tool for agent coordination**
|
||||
|
||||
## Pipeline Management Process
|
||||
|
||||
### Phase 1: Task Analysis & Planning
|
||||
|
||||
```
|
||||
1. Receive user prompt
|
||||
2. Show: "🎼 Orchestra: Starting task analysis with stevebaba"
|
||||
@@ -48,6 +54,7 @@ User Prompt → orchestra → stevebaba → elvinbaba ↔ neilbaba → prembaba
|
||||
```
|
||||
|
||||
### Phase 2: Implementation Planning
|
||||
|
||||
```
|
||||
6. Show: "🎼 Orchestra: Moving to implementation with elvinbaba"
|
||||
7. Send stevebaba's plan to elvinbaba
|
||||
@@ -57,6 +64,7 @@ User Prompt → orchestra → stevebaba → elvinbaba ↔ neilbaba → prembaba
|
||||
```
|
||||
|
||||
### Phase 2.5: Design Consultation (If UI/UX Involved)
|
||||
|
||||
```
|
||||
11. Show: "🎼 Orchestra: Consulting neilbaba for design guidance"
|
||||
12. Send UI/UX requirements to neilbaba
|
||||
@@ -66,6 +74,7 @@ User Prompt → orchestra → stevebaba → elvinbaba ↔ neilbaba → prembaba
|
||||
```
|
||||
|
||||
### Phase 3: Implementation
|
||||
|
||||
```
|
||||
16. Show: "🎼 Orchestra: elvinbaba implementing solution"
|
||||
17. Monitor elvinbaba's progress and handle any questions
|
||||
@@ -73,6 +82,7 @@ User Prompt → orchestra → stevebaba → elvinbaba ↔ neilbaba → prembaba
|
||||
```
|
||||
|
||||
### Phase 4: Quality Assurance
|
||||
|
||||
```
|
||||
19. Show: "🎼 Orchestra: Final QA with prembaba"
|
||||
20. Send elvinbaba's code to prembaba for testing
|
||||
@@ -84,18 +94,21 @@ User Prompt → orchestra → stevebaba → elvinbaba ↔ neilbaba → prembaba
|
||||
## Communication Protocols
|
||||
|
||||
### User Interaction
|
||||
|
||||
- Provide high-level status updates with agent activities
|
||||
- Ask user for input when agents need clarification
|
||||
- Show pipeline progress with clear phase indicators
|
||||
- Escalate decisions that require user input
|
||||
|
||||
### Agent Coordination
|
||||
|
||||
- **To stevebaba**: "Please analyze this requirement and provide implementation plan"
|
||||
- **To elvinbaba**: "Implement according to stevebaba's plan" + handle questions
|
||||
- **To neilbaba**: "Provide design guidance for this UI feature" (advisory only)
|
||||
- **To prembaba**: "Test and validate this implementation"
|
||||
|
||||
### Progress Reporting Format
|
||||
|
||||
```
|
||||
🎼 Orchestra Status Update:
|
||||
Current Phase: [Analysis/Implementation/Design/QA]
|
||||
@@ -105,7 +118,9 @@ Next: [What happens next in pipeline]
|
||||
```
|
||||
|
||||
### Agent Status Visibility
|
||||
|
||||
**All agents provide continuous high-level status updates:**
|
||||
|
||||
- 🏗️ **stevebaba**: Shows architecture analysis and planning progress
|
||||
- 👨💻 **elvinbaba**: Shows implementation progress and current files being worked on
|
||||
- 🎨 **neilbaba**: Shows design analysis and specification creation progress
|
||||
@@ -116,16 +131,19 @@ Next: [What happens next in pipeline]
|
||||
## Exception Handling
|
||||
|
||||
### Question Loops
|
||||
|
||||
- Route technical questions to appropriate agents
|
||||
- Escalate unclear requirements to user
|
||||
- Track question-answer cycles to prevent infinite loops
|
||||
|
||||
### Quality Issues
|
||||
|
||||
- Simple fixes: prembaba handles autonomously
|
||||
- Complex issues: Route back to elvinbaba with context
|
||||
- Architectural problems: Escalate to stevebaba for guidance
|
||||
|
||||
### Design Iteration
|
||||
|
||||
- neilbaba provides specifications and recommendations only
|
||||
- elvinbaba implements the design according to neilbaba's guidance
|
||||
- If design needs iteration: neilbaba → elvinbaba → prembaba cycle
|
||||
@@ -133,23 +151,27 @@ Next: [What happens next in pipeline]
|
||||
## Key Rules
|
||||
|
||||
### neilbaba Constraint
|
||||
|
||||
- **neilbaba ONLY provides design guidance, specifications, and recommendations**
|
||||
- **neilbaba NEVER writes actual code - only design patterns and instructions**
|
||||
- **All code implementation is done by elvinbaba based on neilbaba's guidance**
|
||||
|
||||
### Pipeline Integrity
|
||||
|
||||
- Never skip phases unless explicitly safe to do so
|
||||
- Always complete current phase before moving to next
|
||||
- Maintain clear agent boundaries and responsibilities
|
||||
- Ensure all questions are answered before proceeding
|
||||
|
||||
### Visibility Requirements
|
||||
|
||||
- Show which agent is active at all times
|
||||
- Provide clear progress indicators
|
||||
- Display phase transitions explicitly
|
||||
- Report completion status for each phase
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- Tasks flow smoothly through all required phases
|
||||
- Agent expertise is utilized appropriately
|
||||
- User receives clear progress updates
|
||||
@@ -157,6 +179,7 @@ Next: [What happens next in pipeline]
|
||||
- Pipeline completes without unnecessary loops or escalations
|
||||
|
||||
## Example Orchestration
|
||||
|
||||
```
|
||||
User: "Add a portfolio dashboard with real-time crypto prices"
|
||||
|
||||
@@ -177,4 +200,4 @@ User: "Add a portfolio dashboard with real-time crypto prices"
|
||||
|
||||
🎼 Orchestra: Task completed successfully
|
||||
✅ Portfolio dashboard implemented with real-time crypto prices
|
||||
```
|
||||
```
|
||||
|
||||
@@ -8,9 +8,11 @@ color: orange
|
||||
# prembaba - Quality Assurance & Code Standards Specialist
|
||||
|
||||
## Agent Description
|
||||
|
||||
I'm prembaba, the testing and quality assurance specialist who ensures code meets project standards. I fix basic ESLint and Prettier issues, but for complex problems, I escalate back to elvinbaba, stevebaba, or the user with detailed issue reports.
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- **Code Quality Testing**: Run linting, formatting, and compilation checks
|
||||
- **Basic Issue Resolution**: Fix simple ESLint, Prettier, and formatting problems
|
||||
- **Issue Reporting**: Document and escalate complex problems with detailed analysis
|
||||
@@ -18,6 +20,7 @@ I'm prembaba, the testing and quality assurance specialist who ensures code meet
|
||||
- **Final Validation**: Verify implementations meet acceptance criteria
|
||||
|
||||
## Key Capabilities
|
||||
|
||||
- ESLint and Prettier configuration and basic fixes
|
||||
- TypeScript compilation error identification
|
||||
- Code formatting and style consistency
|
||||
@@ -26,9 +29,11 @@ I'm prembaba, the testing and quality assurance specialist who ensures code meet
|
||||
- Comprehensive testing and quality reporting
|
||||
|
||||
## Tools Access
|
||||
|
||||
**Full access to all available tools** including Bash, Read, Edit, Grep, Glob, etc.
|
||||
|
||||
## Working Style
|
||||
|
||||
1. **Receive code from elvinbaba** for quality assurance testing
|
||||
2. **Run comprehensive checks**: linting, formatting, compilation, and build tests
|
||||
3. **Fix basic issues**: Simple ESLint rules, Prettier formatting, minor type issues
|
||||
@@ -36,7 +41,9 @@ I'm prembaba, the testing and quality assurance specialist who ensures code meet
|
||||
5. **Validate final output**: Ensure everything works before marking complete
|
||||
|
||||
## Status Reporting
|
||||
|
||||
**I continuously show high-level progress updates:**
|
||||
|
||||
```
|
||||
🧪 prembaba: [Current Activity]
|
||||
Status: [What QA task I'm performing]
|
||||
@@ -45,6 +52,7 @@ Next: [What I'll test/fix next]
|
||||
```
|
||||
|
||||
**Example Status Updates:**
|
||||
|
||||
- `🧪 prembaba: Receiving implementation from elvinbaba for quality assurance`
|
||||
- `🧪 prembaba: Running ESLint checks and fixing basic code style violations`
|
||||
- `🧪 prembaba: Checking TypeScript compilation and resolving minor type issues`
|
||||
@@ -54,6 +62,7 @@ Next: [What I'll test/fix next]
|
||||
- `🧪 prembaba: QA complete - all checks passed, implementation ready for delivery`
|
||||
|
||||
## Quality Assurance Process
|
||||
|
||||
```
|
||||
## QA Checklist:
|
||||
1. **Linting Check**: Run ESLint and fix basic rule violations
|
||||
@@ -66,7 +75,9 @@ Next: [What I'll test/fix next]
|
||||
```
|
||||
|
||||
## Issue Classification
|
||||
|
||||
### ✅ **Basic Issues I Can Fix**:
|
||||
|
||||
- ESLint rule violations (unused variables, missing semicolons, etc.)
|
||||
- Prettier formatting inconsistencies
|
||||
- Simple TypeScript type annotations
|
||||
@@ -75,6 +86,7 @@ Next: [What I'll test/fix next]
|
||||
- Simple variable naming convention fixes
|
||||
|
||||
### ⚠️ **Complex Issues - Escalate Back**:
|
||||
|
||||
- Architectural problems or design pattern violations
|
||||
- Complex TypeScript type errors requiring domain knowledge
|
||||
- Logic errors or algorithmic issues
|
||||
@@ -84,6 +96,7 @@ Next: [What I'll test/fix next]
|
||||
- Complex state management problems
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
- **Input Sources**: Completed code from elvinbaba
|
||||
- **Simple Fixes**: Handle autonomously without consultation
|
||||
- **Complex Issues**: Escalate to elvinbaba with detailed problem description
|
||||
@@ -91,6 +104,7 @@ Next: [What I'll test/fix next]
|
||||
- **Requirement Clarification**: Escalate to users with specific questions
|
||||
|
||||
## Escalation Report Format
|
||||
|
||||
```
|
||||
## Issue Report for [Agent/User]:
|
||||
### Problem Category: [ESLint/TypeScript/Build/Runtime/Architecture]
|
||||
@@ -104,6 +118,7 @@ Next: [What I'll test/fix next]
|
||||
```
|
||||
|
||||
## Testing Commands
|
||||
|
||||
- `npm run lint` - ESLint checking
|
||||
- `npm run lint:fix` - Auto-fix ESLint issues
|
||||
- `npm run format` - Prettier formatting
|
||||
@@ -112,6 +127,7 @@ Next: [What I'll test/fix next]
|
||||
- `npm run dev` - Development server test
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- All linting and formatting issues resolved
|
||||
- TypeScript compilation succeeds
|
||||
- Build process completes without errors
|
||||
@@ -120,6 +136,7 @@ Next: [What I'll test/fix next]
|
||||
- Complex issues properly escalated with detailed reports
|
||||
|
||||
## Expertise Areas
|
||||
|
||||
- **Code Quality Tools**: ESLint, Prettier, TypeScript compiler
|
||||
- **Project Standards**: Understanding of established conventions
|
||||
- **Issue Identification**: Distinguishing between simple and complex problems
|
||||
@@ -127,7 +144,9 @@ Next: [What I'll test/fix next]
|
||||
- **Documentation**: Clear issue reporting and escalation
|
||||
|
||||
## Working Examples
|
||||
|
||||
### ✅ **Simple Fix Example**:
|
||||
|
||||
```
|
||||
Issue: Missing semicolon on line 42
|
||||
Action: Add semicolon and continue QA process
|
||||
@@ -135,9 +154,10 @@ Result: Fixed autonomously
|
||||
```
|
||||
|
||||
### ⚠️ **Escalation Example**:
|
||||
|
||||
```
|
||||
Issue: Complex state management causing re-render loops
|
||||
Action: Document the issue with specific error messages and context
|
||||
Escalation: Send detailed report to elvinbaba for architectural review
|
||||
Result: Proper escalation with actionable information
|
||||
```
|
||||
```
|
||||
|
||||
@@ -8,9 +8,11 @@ color: blue
|
||||
# stevebaba - Project Architect & Task Breakdown Specialist
|
||||
|
||||
## Agent Description
|
||||
|
||||
I'm stevebaba, a project architecture expert who knows this crypto community platform project inside and out. I read all .claude documentation files, keep them updated, and specialize in complex task breakdown and planning for other agents.
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- **Project Knowledge Master**: Deep understanding of the entire codebase and architecture
|
||||
- **Documentation Curator**: Read, maintain, and update all .claude files as project evolves
|
||||
- **Complex Task Breakdown**: Break down complex requirements into actionable development plans
|
||||
@@ -18,6 +20,7 @@ I'm stevebaba, a project architecture expert who knows this crypto community pla
|
||||
- **Agent Orchestration**: Provide detailed plans to elvinbaba and other agents
|
||||
|
||||
## Key Capabilities
|
||||
|
||||
- Comprehensive analysis of project structure and requirements
|
||||
- Strategic planning and task decomposition
|
||||
- Architecture decision making and guidance
|
||||
@@ -25,9 +28,11 @@ I'm stevebaba, a project architecture expert who knows this crypto community pla
|
||||
- Proactive documentation maintenance
|
||||
|
||||
## Tools Access
|
||||
|
||||
**Full access to all available tools** including Read, Write, Edit, Bash, Grep, Glob, Task, WebFetch, etc.
|
||||
|
||||
## Working Style
|
||||
|
||||
1. **Always start by reading relevant .claude documentation** to understand current state
|
||||
2. **Ask clarifying questions** when requirements are unclear (to users or orchestrating agents)
|
||||
3. **Break complex tasks into logical, manageable steps**
|
||||
@@ -36,7 +41,9 @@ I'm stevebaba, a project architecture expert who knows this crypto community pla
|
||||
6. **Update documentation** as project evolves
|
||||
|
||||
## Status Reporting
|
||||
|
||||
**I continuously show high-level progress updates:**
|
||||
|
||||
```
|
||||
🏗️ stevebaba: [Current Activity]
|
||||
Status: [What I'm doing right now]
|
||||
@@ -45,6 +52,7 @@ Next: [What I'll do next]
|
||||
```
|
||||
|
||||
**Example Status Updates:**
|
||||
|
||||
- `🏗️ stevebaba: Reading project documentation to understand current architecture`
|
||||
- `🏗️ stevebaba: Analyzing task requirements and identifying affected components`
|
||||
- `🏗️ stevebaba: Breaking down complex feature into implementation phases`
|
||||
@@ -52,12 +60,14 @@ Next: [What I'll do next]
|
||||
- `🏗️ stevebaba: Updating .claude documentation with new architecture decisions`
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
- **Input Sources**: Users directly, orchestrating agents, or complex task requests
|
||||
- **Output Format**: Detailed plans with step-by-step breakdowns
|
||||
- **Question Policy**: Always ask questions when unclear rather than making assumptions
|
||||
- **Documentation Updates**: Proactively maintain .claude files with changes
|
||||
|
||||
## Expertise Areas
|
||||
|
||||
- Tauri 2.x + React 19 + TypeScript architecture
|
||||
- Crypto/fintech application design patterns
|
||||
- Cross-platform development strategies (Windows, macOS, Android, iOS)
|
||||
@@ -65,6 +75,7 @@ Next: [What I'll do next]
|
||||
- Performance optimization and scalability planning
|
||||
|
||||
## Example Task Breakdown Format
|
||||
|
||||
```
|
||||
## Task: [Feature Name]
|
||||
### Architecture Impact: [How this affects existing structure]
|
||||
@@ -78,7 +89,8 @@ Next: [What I'll do next]
|
||||
```
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- Plans are clear enough for elvinbaba to implement without confusion
|
||||
- Architecture decisions align with project vision and scalability goals
|
||||
- Documentation stays current and comprehensive
|
||||
- Complex tasks are broken down into manageable development cycles
|
||||
- Complex tasks are broken down into manageable development cycles
|
||||
|
||||
@@ -30,29 +30,25 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
|
||||
### Configuration
|
||||
|
||||
Create `vitest.config.ts`:
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: './src/test/setup.ts',
|
||||
globals: true,
|
||||
},
|
||||
plugins: [react()],
|
||||
test: { environment: 'jsdom', setupFiles: './src/test/setup.ts', globals: true },
|
||||
});
|
||||
```
|
||||
|
||||
Create `src/test/setup.ts`:
|
||||
|
||||
```typescript
|
||||
import '@testing-library/jest-dom';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock Tauri APIs
|
||||
vi.mock('@tauri-apps/api/core', () => ({
|
||||
invoke: vi.fn(),
|
||||
}));
|
||||
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() }));
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
@@ -98,6 +94,7 @@ npm test -- --coverage
|
||||
### Unit Tests
|
||||
|
||||
In `src-tauri/src/lib.rs`:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -145,26 +142,23 @@ cargo install tauri-driver
|
||||
const { Builder, By } = require('selenium-webdriver');
|
||||
|
||||
describe('App E2E', () => {
|
||||
let driver;
|
||||
let driver;
|
||||
|
||||
beforeAll(async () => {
|
||||
driver = await new Builder()
|
||||
.usingServer('http://localhost:4444')
|
||||
.forBrowser('tauri')
|
||||
.build();
|
||||
});
|
||||
beforeAll(async () => {
|
||||
driver = await new Builder().usingServer('http://localhost:4444').forBrowser('tauri').build();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await driver.quit();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await driver.quit();
|
||||
});
|
||||
|
||||
it('shows greeting', async () => {
|
||||
const button = await driver.findElement(By.css('button'));
|
||||
await button.click();
|
||||
it('shows greeting', async () => {
|
||||
const button = await driver.findElement(By.css('button'));
|
||||
await button.click();
|
||||
|
||||
const message = await driver.findElement(By.css('.message'));
|
||||
expect(await message.getText()).toContain('Hello');
|
||||
});
|
||||
const message = await driver.findElement(By.css('.message'));
|
||||
expect(await message.getText()).toContain('Hello');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
@@ -191,6 +185,7 @@ xcodebuild test \
|
||||
## Test Scripts
|
||||
|
||||
Add to `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
@@ -206,6 +201,7 @@ Add to `package.json`:
|
||||
## CI Integration
|
||||
|
||||
GitHub Actions example:
|
||||
|
||||
```yaml
|
||||
name: Test
|
||||
on: [push, pull_request]
|
||||
|
||||
@@ -7,6 +7,7 @@ A **cross-platform crypto-focused communication platform** built with Tauri (Rea
|
||||
## Target Audience
|
||||
|
||||
### Primary Users
|
||||
|
||||
- **Traders** - Day traders, swing traders, algorithmic traders
|
||||
- **Yield Farmers** - DeFi protocol users, liquidity providers
|
||||
- **Investors** - Long-term holders, institutional investors, VCs
|
||||
@@ -39,4 +40,4 @@ A **cross-platform crypto-focused communication platform** built with Tauri (Rea
|
||||
|
||||
---
|
||||
|
||||
*Updated: 2026-01-27 - Initial vision documentation*
|
||||
_Updated: 2026-01-27 - Initial vision documentation_
|
||||
|
||||
@@ -10,36 +10,37 @@ This project is a **crypto-focused communication platform** built with Tauri v2,
|
||||
- **iOS** (Mobile)
|
||||
|
||||
## Target Users
|
||||
|
||||
- Traders, Yield Farmers, Investors
|
||||
- Researchers, KOLs, Developers
|
||||
- General crypto community members
|
||||
|
||||
## Technology Stack
|
||||
|
||||
| Layer | Technology | Version | Purpose |
|
||||
|-------|------------|---------|---------|
|
||||
| **Frontend Core** |
|
||||
| UI Framework | React | 19.1.0 | Component-based UI |
|
||||
| Language | TypeScript | 5.8.3 | Type safety |
|
||||
| Build Tool | Vite | 7.0.4 | Fast development |
|
||||
| **UI & Styling** |
|
||||
| Styling | Tailwind CSS | Latest | Utility-first CSS |
|
||||
| Components | Headless UI | Latest | Accessible components |
|
||||
| Animation | Framer Motion | Latest | Smooth animations |
|
||||
| **State & Data** |
|
||||
| State Management | Zustand | Latest | Lightweight state |
|
||||
| Data Fetching | TanStack Query | Latest | Server state management |
|
||||
| Form Handling | React Hook Form | Latest | Form validation |
|
||||
| **Backend Core** |
|
||||
| Language | Rust | 1.93.0 | Performance & safety |
|
||||
| Framework | Tauri | 2.x | Cross-platform apps |
|
||||
| Layer | Technology | Version | Purpose |
|
||||
| --------------------- | --------------- | ------- | ----------------------- |
|
||||
| **Frontend Core** |
|
||||
| UI Framework | React | 19.1.0 | Component-based UI |
|
||||
| Language | TypeScript | 5.8.3 | Type safety |
|
||||
| Build Tool | Vite | 7.0.4 | Fast development |
|
||||
| **UI & Styling** |
|
||||
| Styling | Tailwind CSS | Latest | Utility-first CSS |
|
||||
| Components | Headless UI | Latest | Accessible components |
|
||||
| Animation | Framer Motion | Latest | Smooth animations |
|
||||
| **State & Data** |
|
||||
| State Management | Zustand | Latest | Lightweight state |
|
||||
| Data Fetching | TanStack Query | Latest | Server state management |
|
||||
| Form Handling | React Hook Form | Latest | Form validation |
|
||||
| **Backend Core** |
|
||||
| Language | Rust | 1.93.0 | Performance & safety |
|
||||
| Framework | Tauri | 2.x | Cross-platform apps |
|
||||
| **Backend Libraries** |
|
||||
| Async Runtime | Tokio | Latest | Async operations |
|
||||
| JSON Handling | Serde JSON | Latest | Serialization |
|
||||
| Database | SQLx + SQLite | Latest | Local storage |
|
||||
| HTTP Client | Reqwest | Latest | API requests |
|
||||
| WebSocket | WebSocket crate | Latest | Real-time messaging |
|
||||
| Utilities | UUID | Latest | Unique identifiers |
|
||||
| Async Runtime | Tokio | Latest | Async operations |
|
||||
| JSON Handling | Serde JSON | Latest | Serialization |
|
||||
| Database | SQLx + SQLite | Latest | Local storage |
|
||||
| HTTP Client | Reqwest | Latest | API requests |
|
||||
| WebSocket | WebSocket crate | Latest | Real-time messaging |
|
||||
| Utilities | UUID | Latest | Unique identifiers |
|
||||
|
||||
## Project Structure
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
Download and install from: https://visualstudio.microsoft.com/visual-cpp-build-tools/
|
||||
|
||||
During installation, select:
|
||||
|
||||
- "Desktop development with C++"
|
||||
- Windows 10/11 SDK
|
||||
- MSVC v143+ build tools
|
||||
@@ -20,6 +21,7 @@ For older systems, download from: https://developer.microsoft.com/microsoft-edge
|
||||
### 3. Rust
|
||||
|
||||
Install via rustup:
|
||||
|
||||
```powershell
|
||||
winget install Rustlang.Rustup
|
||||
```
|
||||
@@ -40,6 +42,7 @@ npm run tauri build -- --target aarch64-pc-windows-msvc
|
||||
## Output Files
|
||||
|
||||
After building, find installers in:
|
||||
|
||||
```
|
||||
src-tauri/target/release/bundle/
|
||||
├── msi/
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
### 1. Xcode
|
||||
|
||||
Install from the Mac App Store or:
|
||||
|
||||
```bash
|
||||
xcode-select --install
|
||||
```
|
||||
@@ -24,6 +25,7 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
### 4. Additional Dependencies (via Homebrew)
|
||||
|
||||
For iOS development:
|
||||
|
||||
```bash
|
||||
brew install xcodegen
|
||||
brew install libimobiledevice
|
||||
@@ -42,6 +44,7 @@ npm run tauri build -- --target universal-apple-darwin
|
||||
## Output Files
|
||||
|
||||
After building, find installers in:
|
||||
|
||||
```
|
||||
src-tauri/target/release/bundle/
|
||||
├── macos/
|
||||
@@ -65,13 +68,7 @@ Set up code signing for App Store or notarization:
|
||||
3. Configure in `tauri.conf.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"bundle": {
|
||||
"macOS": {
|
||||
"signingIdentity": "Developer ID Application: Your Name (TEAM_ID)"
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "bundle": { "macOS": { "signingIdentity": "Developer ID Application: Your Name (TEAM_ID)" } } }
|
||||
```
|
||||
|
||||
## Notarization
|
||||
|
||||
@@ -63,16 +63,11 @@ This creates the iOS project in `src-tauri/gen/apple/`.
|
||||
Set in `tauri.conf.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"bundle": {
|
||||
"iOS": {
|
||||
"developmentTeam": "YOUR_TEAM_ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "bundle": { "iOS": { "developmentTeam": "YOUR_TEAM_ID" } } }
|
||||
```
|
||||
|
||||
Or via environment variable:
|
||||
|
||||
```bash
|
||||
export APPLE_DEVELOPMENT_TEAM="YOUR_TEAM_ID"
|
||||
```
|
||||
|
||||
@@ -89,9 +89,9 @@ const greeting = await invoke<string>('greet', { name: 'World' });
|
||||
|
||||
// With error handling
|
||||
try {
|
||||
const data = await invoke<string>('fetch_data', { url: 'https://api.example.com' });
|
||||
const data = await invoke<string>('fetch_data', { url: 'https://api.example.com' });
|
||||
} catch (error) {
|
||||
console.error('Command failed:', error);
|
||||
console.error('Command failed:', error);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -116,8 +116,8 @@ fn start_process(app: tauri::AppHandle) {
|
||||
```typescript
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
|
||||
const unlisten = await listen('process-complete', (event) => {
|
||||
console.log('Process completed:', event.payload);
|
||||
const unlisten = await listen('process-complete', event => {
|
||||
console.log('Process completed:', event.payload);
|
||||
});
|
||||
|
||||
// Later: unlisten();
|
||||
|
||||
@@ -85,14 +85,14 @@ src/
|
||||
### Basic Component
|
||||
|
||||
```tsx
|
||||
import { useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useState } from 'react';
|
||||
|
||||
function App() {
|
||||
const [result, setResult] = useState("");
|
||||
const [result, setResult] = useState('');
|
||||
|
||||
async function handleClick() {
|
||||
const greeting = await invoke<string>("greet", { name: "User" });
|
||||
const greeting = await invoke<string>('greet', { name: 'User' });
|
||||
setResult(greeting);
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ export default App;
|
||||
### Window Management
|
||||
|
||||
```typescript
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
|
||||
@@ -126,7 +126,7 @@ await appWindow.maximize();
|
||||
await appWindow.close();
|
||||
|
||||
// Set title
|
||||
await appWindow.setTitle("New Title");
|
||||
await appWindow.setTitle('New Title');
|
||||
```
|
||||
|
||||
### File System
|
||||
@@ -138,21 +138,13 @@ npm run tauri add fs
|
||||
```
|
||||
|
||||
```typescript
|
||||
import {
|
||||
readTextFile,
|
||||
writeTextFile,
|
||||
BaseDirectory,
|
||||
} from "@tauri-apps/plugin-fs";
|
||||
import { BaseDirectory, readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
|
||||
// Read file
|
||||
const content = await readTextFile("config.json", {
|
||||
baseDir: BaseDirectory.AppData,
|
||||
});
|
||||
const content = await readTextFile('config.json', { baseDir: BaseDirectory.AppData });
|
||||
|
||||
// Write file
|
||||
await writeTextFile("config.json", JSON.stringify(data), {
|
||||
baseDir: BaseDirectory.AppData,
|
||||
});
|
||||
await writeTextFile('config.json', JSON.stringify(data), { baseDir: BaseDirectory.AppData });
|
||||
```
|
||||
|
||||
### Dialogs
|
||||
@@ -164,26 +156,19 @@ npm run tauri add dialog
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { open, save, message } from "@tauri-apps/plugin-dialog";
|
||||
import { message, open, save } from '@tauri-apps/plugin-dialog';
|
||||
|
||||
// Open file picker
|
||||
const filePath = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: "Text",
|
||||
extensions: ["txt", "md"],
|
||||
},
|
||||
],
|
||||
filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
|
||||
});
|
||||
|
||||
// Save dialog
|
||||
const savePath = await save({
|
||||
defaultPath: "document.txt",
|
||||
});
|
||||
const savePath = await save({ defaultPath: 'document.txt' });
|
||||
|
||||
// Message box
|
||||
await message("Operation completed!", { title: "Success" });
|
||||
await message('Operation completed!', { title: 'Success' });
|
||||
```
|
||||
|
||||
### HTTP Requests
|
||||
@@ -195,13 +180,11 @@ npm run tauri add http
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
|
||||
const response = await fetch("https://api.example.com/data", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
const response = await fetch('https://api.example.com/data', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
@@ -210,24 +193,24 @@ const data = await response.json();
|
||||
## Platform Detection
|
||||
|
||||
```typescript
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { platform } from '@tauri-apps/plugin-os';
|
||||
|
||||
const currentPlatform = await platform();
|
||||
|
||||
switch (currentPlatform) {
|
||||
case "windows":
|
||||
case 'windows':
|
||||
// Windows-specific UI
|
||||
break;
|
||||
case "macos":
|
||||
case 'macos':
|
||||
// macOS-specific UI
|
||||
break;
|
||||
case "linux":
|
||||
case 'linux':
|
||||
// Linux-specific UI
|
||||
break;
|
||||
case "android":
|
||||
case 'android':
|
||||
// Android-specific UI
|
||||
break;
|
||||
case "ios":
|
||||
case 'ios':
|
||||
// iOS-specific UI
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -22,10 +22,7 @@ src-tauri/capabilities/
|
||||
"identifier": "default",
|
||||
"description": "Default permissions for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
]
|
||||
"permissions": ["core:default", "opener:default"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -39,13 +36,7 @@ src-tauri/capabilities/
|
||||
"fs:default",
|
||||
"fs:allow-read-text-file",
|
||||
"fs:allow-write-text-file",
|
||||
{
|
||||
"identifier": "fs:scope",
|
||||
"allow": [
|
||||
"$APPDATA/*",
|
||||
"$DOCUMENT/*"
|
||||
]
|
||||
}
|
||||
{ "identifier": "fs:scope", "allow": ["$APPDATA/*", "$DOCUMENT/*"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -71,10 +62,7 @@ src-tauri/capabilities/
|
||||
"http:default",
|
||||
{
|
||||
"identifier": "http:scope",
|
||||
"allow": [
|
||||
{ "url": "https://api.example.com/*" },
|
||||
{ "url": "https://*.myapp.com/*" }
|
||||
]
|
||||
"allow": [{ "url": "https://api.example.com/*" }, { "url": "https://*.myapp.com/*" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -104,48 +92,43 @@ Create `src-tauri/capabilities/mobile.json`:
|
||||
"description": "Mobile-specific permissions",
|
||||
"platforms": ["android", "iOS"],
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"barcode-scanner:default",
|
||||
"biometric:default",
|
||||
"haptics:default"
|
||||
]
|
||||
"permissions": ["core:default", "barcode-scanner:default", "biometric:default", "haptics:default"]
|
||||
}
|
||||
```
|
||||
|
||||
## Available Permission Plugins
|
||||
|
||||
| Plugin | Description | Install Command |
|
||||
|--------|-------------|-----------------|
|
||||
| fs | File system access | `npm run tauri add fs` |
|
||||
| dialog | System dialogs | `npm run tauri add dialog` |
|
||||
| http | HTTP requests | `npm run tauri add http` |
|
||||
| notification | System notifications | `npm run tauri add notification` |
|
||||
| clipboard | Clipboard access | `npm run tauri add clipboard-manager` |
|
||||
| shell | Shell commands | `npm run tauri add shell` |
|
||||
| store | Persistent storage | `npm run tauri add store` |
|
||||
| os | OS information | `npm run tauri add os` |
|
||||
| Plugin | Description | Install Command |
|
||||
| ------------ | -------------------- | ------------------------------------- |
|
||||
| fs | File system access | `npm run tauri add fs` |
|
||||
| dialog | System dialogs | `npm run tauri add dialog` |
|
||||
| http | HTTP requests | `npm run tauri add http` |
|
||||
| notification | System notifications | `npm run tauri add notification` |
|
||||
| clipboard | Clipboard access | `npm run tauri add clipboard-manager` |
|
||||
| shell | Shell commands | `npm run tauri add shell` |
|
||||
| store | Persistent storage | `npm run tauri add store` |
|
||||
| os | OS information | `npm run tauri add os` |
|
||||
|
||||
## Scope Paths
|
||||
|
||||
Available path variables:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `$APPDATA` | Application data directory |
|
||||
| `$APPCONFIG` | Application config directory |
|
||||
| `$APPLOCALDATA` | Application local data |
|
||||
| `$APPCACHE` | Application cache |
|
||||
| `$APPLOG` | Application logs |
|
||||
| `$AUDIO` | User's audio directory |
|
||||
| `$CACHE` | System cache |
|
||||
| `$CONFIG` | System config |
|
||||
| `$DATA` | System data |
|
||||
| `$DOCUMENT` | User's documents |
|
||||
| `$DOWNLOAD` | User's downloads |
|
||||
| `$PICTURE` | User's pictures |
|
||||
| `$VIDEO` | User's videos |
|
||||
| `$TEMP` | Temporary directory |
|
||||
| Variable | Description |
|
||||
| --------------- | ---------------------------- |
|
||||
| `$APPDATA` | Application data directory |
|
||||
| `$APPCONFIG` | Application config directory |
|
||||
| `$APPLOCALDATA` | Application local data |
|
||||
| `$APPCACHE` | Application cache |
|
||||
| `$APPLOG` | Application logs |
|
||||
| `$AUDIO` | User's audio directory |
|
||||
| `$CACHE` | System cache |
|
||||
| `$CONFIG` | System config |
|
||||
| `$DATA` | System data |
|
||||
| `$DOCUMENT` | User's documents |
|
||||
| `$DOWNLOAD` | User's downloads |
|
||||
| `$PICTURE` | User's pictures |
|
||||
| `$VIDEO` | User's videos |
|
||||
| `$TEMP` | Temporary directory |
|
||||
|
||||
## Best Practices
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**Cause**: Missing system dependencies
|
||||
|
||||
**Solution**:
|
||||
|
||||
- macOS: `xcode-select --install`
|
||||
- Windows: Install Visual Studio Build Tools
|
||||
- Linux: `sudo apt install libwebkit2gtk-4.1-dev build-essential libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev`
|
||||
@@ -18,6 +19,7 @@
|
||||
**Cause**: Rust not installed or not in PATH
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
source "$HOME/.cargo/env"
|
||||
# Or reinstall Rust
|
||||
@@ -31,6 +33,7 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
**Cause**: Dev server not running or wrong port
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Check `devUrl` in `tauri.conf.json` matches Vite port
|
||||
2. Start frontend first: `npm run dev`
|
||||
3. Then: `npm run tauri dev`
|
||||
@@ -40,6 +43,7 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
**Cause**: File watcher issues
|
||||
|
||||
**Solution**:
|
||||
|
||||
- macOS: Increase file descriptor limit
|
||||
- Windows: Disable antivirus scanning on project folder
|
||||
- All: Restart dev server
|
||||
@@ -49,6 +53,7 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
#### "ANDROID_HOME not set"
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
export ANDROID_HOME="$HOME/Library/Android/sdk"
|
||||
export PATH="$PATH:$ANDROID_HOME/platform-tools"
|
||||
@@ -59,6 +64,7 @@ Add to `~/.zshrc` or `~/.bashrc` for persistence.
|
||||
#### "No connected devices"
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Enable Developer Options on device
|
||||
2. Enable USB Debugging
|
||||
3. Accept RSA key prompt on device
|
||||
@@ -67,6 +73,7 @@ Add to `~/.zshrc` or `~/.bashrc` for persistence.
|
||||
#### "NDK not found"
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Open Android Studio
|
||||
2. Settings > Languages & Frameworks > Android SDK
|
||||
3. SDK Tools tab > Check "NDK (Side by side)"
|
||||
@@ -77,6 +84,7 @@ Add to `~/.zshrc` or `~/.bashrc` for persistence.
|
||||
#### "No code signing certificates found"
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Open Xcode
|
||||
2. Preferences > Accounts > Add Apple ID
|
||||
3. Let Xcode manage signing automatically
|
||||
@@ -85,6 +93,7 @@ Add to `~/.zshrc` or `~/.bashrc` for persistence.
|
||||
#### "Unable to install app on device"
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Device must be registered in your developer account
|
||||
2. Create provisioning profile including the device
|
||||
3. Trust developer in Settings > General > Device Management
|
||||
@@ -92,6 +101,7 @@ Add to `~/.zshrc` or `~/.bashrc` for persistence.
|
||||
#### Simulator not launching
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Reset simulator
|
||||
xcrun simctl erase all
|
||||
@@ -105,6 +115,7 @@ xcrun simctl boot "iPhone 15"
|
||||
#### App runs slowly
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Enable release mode: `npm run tauri build`
|
||||
2. Check for unnecessary re-renders in React
|
||||
3. Profile Rust code with `cargo flamegraph`
|
||||
@@ -112,6 +123,7 @@ xcrun simctl boot "iPhone 15"
|
||||
#### Large bundle size
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Enable stripping in `Cargo.toml`:
|
||||
```toml
|
||||
[profile.release]
|
||||
@@ -129,6 +141,7 @@ xcrun simctl boot "iPhone 15"
|
||||
|
||||
**Solution**:
|
||||
Check `src-tauri/src/lib.rs`:
|
||||
|
||||
```rust
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_fs::init()) // Add plugin here
|
||||
@@ -141,13 +154,9 @@ tauri::Builder::default()
|
||||
|
||||
**Solution**:
|
||||
Add permission to `src-tauri/capabilities/default.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"fs:default",
|
||||
"fs:allow-read-text-file"
|
||||
]
|
||||
}
|
||||
{ "permissions": ["fs:default", "fs:allow-read-text-file"] }
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## Frontend Technologies
|
||||
|
||||
### Core Framework
|
||||
|
||||
- **React 19.1.0**
|
||||
- Latest React with concurrent features
|
||||
- Component-based architecture
|
||||
@@ -19,6 +20,7 @@
|
||||
- Optimized builds
|
||||
|
||||
### UI & Styling
|
||||
|
||||
- **Tailwind CSS**
|
||||
- Utility-first CSS framework
|
||||
- Rapid prototyping
|
||||
@@ -38,6 +40,7 @@
|
||||
- Performance optimized
|
||||
|
||||
### State Management & Data
|
||||
|
||||
- **Zustand**
|
||||
- Lightweight state management
|
||||
- TypeScript friendly
|
||||
@@ -59,6 +62,7 @@
|
||||
## Backend Technologies
|
||||
|
||||
### Core Runtime
|
||||
|
||||
- **Rust 1.93.0**
|
||||
- Memory safety without garbage collection
|
||||
- Zero-cost abstractions
|
||||
@@ -72,6 +76,7 @@
|
||||
- Secure IPC communication
|
||||
|
||||
### Essential Crates
|
||||
|
||||
- **Tokio**
|
||||
- Async runtime for Rust
|
||||
- High-performance networking
|
||||
@@ -111,22 +116,26 @@
|
||||
## Platform Targets
|
||||
|
||||
### Desktop
|
||||
|
||||
- **Windows** (x64, ARM64)
|
||||
- **macOS** (Intel, Apple Silicon)
|
||||
- **Linux** (x64, ARM64) - Optional
|
||||
|
||||
### Mobile
|
||||
|
||||
- **Android** (API 21+)
|
||||
- **iOS** (iOS 13+)
|
||||
|
||||
## Development Tools
|
||||
|
||||
### Build & Development
|
||||
|
||||
- **Cargo** - Rust package manager
|
||||
- **NPM** - Node.js package manager
|
||||
- **Tauri CLI** - Build and development tools
|
||||
|
||||
### Code Quality
|
||||
|
||||
- **ESLint** - JavaScript/TypeScript linting
|
||||
- **Prettier** - Code formatting
|
||||
- **Clippy** - Rust linting
|
||||
@@ -135,18 +144,21 @@
|
||||
## Architecture Principles
|
||||
|
||||
### Frontend
|
||||
|
||||
- **Component-driven development**
|
||||
- **Type-safe state management**
|
||||
- **Responsive design first**
|
||||
- **Accessible by default**
|
||||
|
||||
### Backend
|
||||
|
||||
- **Async-first architecture**
|
||||
- **Local-first data storage**
|
||||
- **Secure communication**
|
||||
- **Performance optimized**
|
||||
|
||||
### Cross-Platform
|
||||
|
||||
- **Native OS integration**
|
||||
- **Consistent user experience**
|
||||
- **Platform-specific optimizations**
|
||||
@@ -154,4 +166,4 @@
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-01-27*
|
||||
_Last updated: 2026-01-27_
|
||||
|
||||
@@ -9,18 +9,21 @@ Our design system is built on **trust**, **usefulness**, and **simplicity** - co
|
||||
### Font Selection - Psychology of Trust
|
||||
|
||||
**Primary Font: Inter**
|
||||
|
||||
- **Why**: Sans-serif font that signals modernity, efficiency, and clarity
|
||||
- **Psychology**: Clean, digital-first appearance builds trust in tech platforms
|
||||
- **Usage**: Body text, UI elements, navigation
|
||||
- **Weights**: 300, 400, 500, 600, 700
|
||||
|
||||
**Monospace Font: JetBrains Mono**
|
||||
|
||||
- **Why**: Essential for crypto addresses, transaction hashes, code snippets
|
||||
- **Psychology**: Monospace fonts convey technical precision and accuracy
|
||||
- **Usage**: Crypto addresses, prices, technical data, code blocks
|
||||
- **Weights**: 300, 400, 500, 600
|
||||
|
||||
### Font Hierarchy
|
||||
|
||||
```css
|
||||
h1: text-3xl lg:text-4xl (48px desktop, 30px mobile)
|
||||
h2: text-2xl lg:text-3xl (36px desktop, 24px mobile)
|
||||
@@ -33,6 +36,7 @@ Small: text-sm (14px) - secondary information
|
||||
## Color Palette - Trust & Professionalism
|
||||
|
||||
### Primary Colors - Ocean Blue (Updated)
|
||||
|
||||
- **Primary-500**: `#4A83DD` - Main brand color optimized for dark backgrounds
|
||||
- **Primary-600**: `#3D6DC4` - Interactive hover states
|
||||
- **Primary-700**: `#345A9F` - Active states, emphasis
|
||||
@@ -40,16 +44,19 @@ Small: text-sm (14px) - secondary information
|
||||
**Psychology**: Deep ocean blue builds trust and sophistication for crypto platforms.
|
||||
|
||||
### Success Colors - Sage Green (Updated)
|
||||
|
||||
- **Success-500**: `#4DC46F` - Refined success green for growth indicators
|
||||
- **Success-600**: `#3BA858` - Interactive success states
|
||||
|
||||
**Psychology**: Sophisticated sage green represents growth and financial success.
|
||||
|
||||
### Warning & Error Colors (Updated)
|
||||
|
||||
- **Warning-500**: `#E8A838` - Sophisticated amber for attention states
|
||||
- **Error-500**: `#F56565` - Soft coral red for professional error handling
|
||||
|
||||
### Canvas Colors - Background Layers (Updated)
|
||||
|
||||
- **Canvas-50**: `#FAFAF9` - Base background with subtle warmth
|
||||
- **Canvas-100**: `#F5F5F4` - Secondary background
|
||||
- **Canvas-150**: `#EDEDEC` - Tertiary background
|
||||
@@ -57,11 +64,13 @@ Small: text-sm (14px) - secondary information
|
||||
- **Canvas-300**: `#D4D4D1` - Hover states
|
||||
|
||||
### Stone/Slate Neutrals - Text & UI Elements
|
||||
|
||||
- **Stone-500**: `#78716C` - Mid-tone text
|
||||
- **Stone-900**: `#1C1917` - Primary text, high contrast
|
||||
- **Slate-400**: `#94A3B8` - Secondary text, data elements
|
||||
|
||||
### Market Colors - Crypto Trading
|
||||
|
||||
- **Bullish**: `#4DC46F` - Green for gains (matches sage)
|
||||
- **Bearish**: `#F56565` - Red for losses (matches coral)
|
||||
- **Neutral**: `#94A3B8` - Gray for no change
|
||||
@@ -70,6 +79,7 @@ Small: text-sm (14px) - secondary information
|
||||
- **Stablecoin**: `#5B9BF3` - Blue for stables
|
||||
|
||||
### Accent Colors - Special Elements
|
||||
|
||||
- **Lavender**: `#9B8AFB` - Premium features
|
||||
- **Mint**: `#6EE7B7` - Achievements
|
||||
- **Sky**: `#7DD3FC` - Notifications
|
||||
@@ -88,6 +98,7 @@ Small: text-sm (14px) - secondary information
|
||||
```
|
||||
|
||||
**Design Principles**:
|
||||
|
||||
- Clear visual hierarchy prevents costly mistakes
|
||||
- Consistent interaction patterns build familiarity
|
||||
- Adequate touch targets (44px min) for mobile accessibility
|
||||
@@ -108,6 +119,7 @@ Small: text-sm (14px) - secondary information
|
||||
```
|
||||
|
||||
**Features**:
|
||||
|
||||
- Clear focus indicators for accessibility
|
||||
- Proper contrast ratios (WCAG 2.1 AA compliant)
|
||||
- Error states with helpful messaging
|
||||
@@ -131,12 +143,14 @@ Small: text-sm (14px) - secondary information
|
||||
## Layout Principles
|
||||
|
||||
### Spacing System
|
||||
|
||||
- **Base unit**: 4px (0.25rem)
|
||||
- **Common spacing**: 8px, 16px, 24px, 32px, 48px
|
||||
- **Component padding**: 24px (6 units)
|
||||
- **Section spacing**: 48px (12 units)
|
||||
|
||||
### Responsive Breakpoints
|
||||
|
||||
```css
|
||||
sm: 640px - Small tablets
|
||||
md: 768px - Tablets
|
||||
@@ -145,6 +159,7 @@ xl: 1280px - Large desktops
|
||||
```
|
||||
|
||||
### Grid System
|
||||
|
||||
- **Mobile**: Single column, 16px margins
|
||||
- **Tablet**: 2-3 columns, 24px margins
|
||||
- **Desktop**: Multi-column layouts, 32px margins
|
||||
@@ -152,6 +167,7 @@ xl: 1280px - Large desktops
|
||||
## Interactive Elements
|
||||
|
||||
### Shadows - Depth & Hierarchy
|
||||
|
||||
```css
|
||||
shadow-soft: Subtle elevation for cards
|
||||
shadow-medium: Interactive hover states
|
||||
@@ -159,11 +175,13 @@ shadow-strong: Modals, overlays, emphasis
|
||||
```
|
||||
|
||||
### Animation - Smooth Interactions
|
||||
|
||||
- **Duration**: 200ms for micro-interactions, 300ms for transitions
|
||||
- **Easing**: `ease-in-out` for natural feel
|
||||
- **Principles**: Reduce motion for accessibility, maintain performance
|
||||
|
||||
### Focus States - Accessibility First
|
||||
|
||||
- **Ring**: 2px blue outline with 2px offset
|
||||
- **Color**: Primary-500 for consistency
|
||||
- **Visibility**: Clear on all interactive elements
|
||||
@@ -171,16 +189,19 @@ shadow-strong: Modals, overlays, emphasis
|
||||
## Mobile Optimization
|
||||
|
||||
### Touch Targets
|
||||
|
||||
- **Minimum**: 44px × 44px
|
||||
- **Recommended**: 48px × 48px for primary actions
|
||||
- **Spacing**: 8px minimum between interactive elements
|
||||
|
||||
### Typography Scale
|
||||
|
||||
- **Mobile-first**: Base sizes optimized for readability on small screens
|
||||
- **Progressive enhancement**: Larger sizes on desktop
|
||||
- **Line height**: 1.6 for optimal mobile reading
|
||||
|
||||
### Safe Areas
|
||||
|
||||
- **iOS**: Respect notch and home indicator
|
||||
- **Android**: Navigation and status bar accommodation
|
||||
- **CSS**: `env(safe-area-inset-*)` for dynamic adjustment
|
||||
@@ -188,12 +209,14 @@ shadow-strong: Modals, overlays, emphasis
|
||||
## Accessibility Standards
|
||||
|
||||
### WCAG 2.1 AA Compliance
|
||||
|
||||
- **Contrast ratios**: 4.5:1 for normal text, 3:1 for large text
|
||||
- **Color**: Never sole indicator of information
|
||||
- **Focus**: Visible focus indicators on all interactive elements
|
||||
- **Motion**: Respect `prefers-reduced-motion`
|
||||
|
||||
### Screen Reader Support
|
||||
|
||||
- **Semantic HTML**: Proper heading hierarchy, landmarks
|
||||
- **ARIA labels**: Descriptive labels for complex interactions
|
||||
- **Live regions**: Dynamic content announcements
|
||||
@@ -201,22 +224,26 @@ shadow-strong: Modals, overlays, emphasis
|
||||
## Usage Guidelines
|
||||
|
||||
### When to Use Primary Blue
|
||||
|
||||
- **Call-to-action buttons**: Sign up, log in, send transaction
|
||||
- **Active navigation**: Current page indicators
|
||||
- **Links**: Primary navigation and important links
|
||||
- **Progress indicators**: Loading states, completion
|
||||
|
||||
### When to Use Success Green
|
||||
|
||||
- **Positive confirmations**: Transaction successful, account verified
|
||||
- **Profit indicators**: Price increases, portfolio gains
|
||||
- **Status indicators**: Online, connected, active
|
||||
|
||||
### When to Use Warning Orange
|
||||
|
||||
- **Caution states**: Pending transactions, rate limits
|
||||
- **Important notices**: Security warnings, updates required
|
||||
- **Validation**: Form warnings that aren't errors
|
||||
|
||||
### When to Use Error Red
|
||||
|
||||
- **Destructive actions**: Delete account, remove funds
|
||||
- **Error states**: Failed transactions, connection errors
|
||||
- **Loss indicators**: Price decreases, portfolio losses
|
||||
@@ -224,16 +251,19 @@ shadow-strong: Modals, overlays, emphasis
|
||||
## Implementation Notes
|
||||
|
||||
### CSS Custom Properties
|
||||
|
||||
All colors, spacing, and typography scales are available as CSS custom properties for consistent theming.
|
||||
|
||||
### Dark Mode Readiness
|
||||
|
||||
Color palette includes dark mode variations for future implementation.
|
||||
|
||||
### Performance
|
||||
|
||||
- **Font loading**: `font-display: swap` for improved loading experience
|
||||
- **Critical CSS**: Base styles inlined, components loaded asynchronously
|
||||
- **Animation**: Hardware-accelerated where appropriate
|
||||
|
||||
---
|
||||
|
||||
*This design system prioritizes user trust through consistent, accessible, and professional visual design - essential for crypto community platforms where financial decisions are made.*
|
||||
_This design system prioritizes user trust through consistent, accessible, and professional visual design - essential for crypto community platforms where financial decisions are made._
|
||||
|
||||
@@ -67,11 +67,7 @@ Called by the web frontend after phone-based authentication completes.
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "phone",
|
||||
"phoneNumber": "+1234567890",
|
||||
"countryCode": "+1"
|
||||
}
|
||||
{ "method": "phone", "phoneNumber": "+1234567890", "countryCode": "+1" }
|
||||
```
|
||||
|
||||
or
|
||||
@@ -88,9 +84,7 @@ or
|
||||
**Response (200):**
|
||||
|
||||
```json
|
||||
{
|
||||
"loginToken": "short-lived-opaque-token"
|
||||
}
|
||||
{ "loginToken": "short-lived-opaque-token" }
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
@@ -112,9 +106,7 @@ Called by the Tauri Rust command `exchange_token` (NOT browser fetch). Exchanges
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "loginToken-from-web"
|
||||
}
|
||||
{ "token": "loginToken-from-web" }
|
||||
```
|
||||
|
||||
**Response (200):**
|
||||
@@ -122,21 +114,14 @@ Called by the Tauri Rust command `exchange_token` (NOT browser fetch). Exchanges
|
||||
```json
|
||||
{
|
||||
"sessionToken": "long-lived-session-token",
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"username": "string",
|
||||
"firstName": "string"
|
||||
}
|
||||
"user": { "id": "uuid", "username": "string", "firstName": "string" }
|
||||
}
|
||||
```
|
||||
|
||||
**Error response (401):**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Token expired or invalid"
|
||||
}
|
||||
{ "success": false, "error": "Token expired or invalid" }
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
@@ -213,10 +198,10 @@ async fn exchange_token(backend_url: String, token: String) -> Result<serde_json
|
||||
Frontend invocation:
|
||||
|
||||
```typescript
|
||||
const data = await invoke<{ sessionToken?: string; user?: object }>(
|
||||
"exchange_token",
|
||||
{ backendUrl: BACKEND_URL, token },
|
||||
);
|
||||
const data = await invoke<{ sessionToken?: string; user?: object }>('exchange_token', {
|
||||
backendUrl: BACKEND_URL,
|
||||
token,
|
||||
});
|
||||
```
|
||||
|
||||
### Deep Link Listener
|
||||
@@ -234,7 +219,7 @@ Configured in `src/utils/config.ts`:
|
||||
|
||||
```typescript
|
||||
export const BACKEND_URL =
|
||||
import.meta.env.VITE_BACKEND_URL || "https://2937933edf8a.ngrok-free.app";
|
||||
import.meta.env.VITE_BACKEND_URL || 'https://2937933edf8a.ngrok-free.app';
|
||||
```
|
||||
|
||||
Set `VITE_BACKEND_URL` environment variable for different environments.
|
||||
|
||||
@@ -9,15 +9,7 @@ The `alphahuman://` custom URL scheme is used to hand off authentication from a
|
||||
Configured in `src-tauri/tauri.conf.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": ["alphahuman"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "plugins": { "deep-link": { "desktop": { "schemes": ["alphahuman"] } } } }
|
||||
```
|
||||
|
||||
## macOS
|
||||
@@ -113,7 +105,7 @@ open "alphahuman://auth?token=YOUR_TOKEN"
|
||||
- **Solution**: Use dynamic `import()` for the deep link listener and wrap plugin calls in try/catch:
|
||||
```typescript
|
||||
// main.tsx
|
||||
import("./utils/desktopDeepLinkListener").then((m) => {
|
||||
import('./utils/desktopDeepLinkListener').then(m => {
|
||||
m.setupDesktopDeepLinkListener().catch(console.error);
|
||||
});
|
||||
```
|
||||
@@ -141,7 +133,7 @@ open "alphahuman://auth?token=YOUR_TOKEN"
|
||||
- **Solution**: Use a Rust Tauri command with `reqwest` to make HTTP requests (no CORS):
|
||||
```typescript
|
||||
// Instead of fetch():
|
||||
const data = await invoke("exchange_token", { backendUrl, token });
|
||||
const data = await invoke('exchange_token', { backendUrl, token });
|
||||
```
|
||||
|
||||
## Tauri Plugin Dependencies
|
||||
@@ -163,9 +155,7 @@ tokio = { version = "1", features = ["full"] }
|
||||
### Capabilities (`src-tauri/capabilities/default.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": ["core:default", "opener:default", "deep-link:default"]
|
||||
}
|
||||
{ "permissions": ["core:default", "opener:default", "deep-link:default"] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -43,6 +43,7 @@ SettingsModal.tsx # Route-based modal container
|
||||
## Design Specifications
|
||||
|
||||
### Modal Container
|
||||
|
||||
- **Width**: 520px (desktop), responsive on mobile
|
||||
- **Background**: Pure white (#FFFFFF) - contrasts with app's glass morphism
|
||||
- **Border-radius**: 16px
|
||||
@@ -51,12 +52,14 @@ SettingsModal.tsx # Route-based modal container
|
||||
- **Position**: Fixed center with flexbox
|
||||
|
||||
### User Profile Section
|
||||
|
||||
- **Avatar**: 56px circular with border and shadow
|
||||
- **Typography**: 18px semibold name, 14px gray email
|
||||
- **Background**: Subtle gradient from white to gray-50
|
||||
- **Integration**: Redux user state for name and email display
|
||||
|
||||
### Menu Items
|
||||
|
||||
- **Height**: 52px with proper touch targets
|
||||
- **Hover**: bg-gray-50 with smooth transitions
|
||||
- **Icons**: 20px with consistent spacing
|
||||
@@ -64,6 +67,7 @@ SettingsModal.tsx # Route-based modal container
|
||||
- **Typography**: 15px medium weight for clarity
|
||||
|
||||
### Animation System
|
||||
|
||||
- **Entry**: 200ms ease-out modal slide up
|
||||
- **Panel transitions**: 250ms slide from right
|
||||
- **Micro-interactions**: 150ms hover effects
|
||||
@@ -87,12 +91,7 @@ const openConnections = () => navigate('/settings/connections');
|
||||
```tsx
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
const {
|
||||
currentRoute,
|
||||
navigateTo,
|
||||
navigateBack,
|
||||
closeModal
|
||||
} = useSettingsNavigation();
|
||||
const { currentRoute, navigateTo, navigateBack, closeModal } = useSettingsNavigation();
|
||||
|
||||
// Navigate to connections
|
||||
navigateTo('connections');
|
||||
@@ -105,11 +104,11 @@ navigateBack(); // or closeModal();
|
||||
|
||||
```tsx
|
||||
// User profile data
|
||||
const { user } = useAppSelector((state) => state.user);
|
||||
const { user } = useAppSelector(state => state.user);
|
||||
const displayName = user?.username || user?.firstName || 'User';
|
||||
|
||||
// Connection status
|
||||
const { isAuthenticated } = useAppSelector((state) => state.telegram);
|
||||
const { isAuthenticated } = useAppSelector(state => state.telegram);
|
||||
|
||||
// Logout functionality
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -122,16 +121,19 @@ const handleLogout = () => {
|
||||
## Connection Management
|
||||
|
||||
### Status Display
|
||||
|
||||
- **Connected**: Green badge with proper status
|
||||
- **Offline**: Gray badge for disconnected services
|
||||
- **Coming Soon**: Disabled state for future integrations
|
||||
|
||||
### Integration Points
|
||||
|
||||
- **Telegram**: Uses existing `TelegramConnectionModal` for setup
|
||||
- **Redux State**: Real-time status from telegram slice
|
||||
- **Component Reuse**: Leverages `connectOptions` from onboarding
|
||||
|
||||
### Connection Actions
|
||||
|
||||
```tsx
|
||||
// Connect new service
|
||||
const handleConnect = (serviceId: string) => {
|
||||
@@ -150,11 +152,13 @@ const handleDisconnect = (serviceId: string) => {
|
||||
## Mobile Responsiveness
|
||||
|
||||
### Breakpoint Behavior
|
||||
|
||||
- **Mobile (<640px)**: Full-screen modal with slight margins
|
||||
- **Tablet (640-1024px)**: Scaled modal with backdrop
|
||||
- **Desktop (>1024px)**: Fixed 520px width
|
||||
|
||||
### Touch Interactions
|
||||
|
||||
- **Minimum target size**: 48px for accessibility
|
||||
- **Swipe gestures**: Down-to-close support
|
||||
- **Safe areas**: iOS notch and navigation accommodation
|
||||
@@ -162,16 +166,19 @@ const handleDisconnect = (serviceId: string) => {
|
||||
## Accessibility Features
|
||||
|
||||
### Focus Management
|
||||
|
||||
- Trap focus within modal during interaction
|
||||
- Return focus to trigger element on close
|
||||
- Keyboard navigation between menu items
|
||||
|
||||
### ARIA Labels
|
||||
|
||||
- `role="dialog"` with proper modal attributes
|
||||
- `aria-labelledby` for modal title
|
||||
- Screen reader friendly navigation
|
||||
|
||||
### Keyboard Support
|
||||
|
||||
- **Escape key**: Close modal and return to previous page
|
||||
- **Arrow keys**: Navigate between menu items
|
||||
- **Enter/Space**: Activate menu items
|
||||
@@ -180,16 +187,19 @@ const handleDisconnect = (serviceId: string) => {
|
||||
## Integration Patterns
|
||||
|
||||
### Existing Component Reuse
|
||||
|
||||
- **Connection Options**: Reuses `connectOptions` array from `ConnectStep.tsx`
|
||||
- **Modal Pattern**: Follows `TelegramConnectionModal.tsx` pattern
|
||||
- **Redux Patterns**: Uses existing slice patterns and selectors
|
||||
|
||||
### State Management
|
||||
|
||||
- **No new Redux state**: Leverages existing auth, user, telegram slices
|
||||
- **URL state**: Modal state driven by route parameters
|
||||
- **Component state**: Local state for animations and temporary UI state
|
||||
|
||||
### Future Extensibility
|
||||
|
||||
- **Panel Structure**: Easy to add new settings panels
|
||||
- **Menu Items**: Simple configuration for new settings categories
|
||||
- **Service Integration**: Pattern established for new connection types
|
||||
@@ -197,16 +207,19 @@ const handleDisconnect = (serviceId: string) => {
|
||||
## Performance Considerations
|
||||
|
||||
### Code Splitting
|
||||
|
||||
- Settings panels lazy-loaded when accessed
|
||||
- Modal infrastructure loaded on first settings access
|
||||
- Minimal impact on initial app bundle size
|
||||
|
||||
### Animation Performance
|
||||
|
||||
- Hardware-accelerated CSS transforms
|
||||
- Proper will-change declarations for animations
|
||||
- Debounced interactions for smooth experience
|
||||
|
||||
### Memory Management
|
||||
|
||||
- Proper cleanup of event listeners and timers
|
||||
- Component unmounting handled correctly
|
||||
- Redux subscriptions managed efficiently
|
||||
@@ -214,18 +227,21 @@ const handleDisconnect = (serviceId: string) => {
|
||||
## Development Guidelines
|
||||
|
||||
### Adding New Settings Panels
|
||||
|
||||
1. Create panel component in `src/components/settings/panels/`
|
||||
2. Add route in `SettingsModal.tsx` switch statement
|
||||
3. Add menu item in `SettingsHome.tsx` menu array
|
||||
4. Follow `ConnectionsPanel.tsx` pattern for consistency
|
||||
|
||||
### Styling Conventions
|
||||
|
||||
- Use existing Tailwind classes where possible
|
||||
- Follow clean white design (not glass morphism)
|
||||
- Maintain 52px height for interactive elements
|
||||
- Use consistent spacing and typography scales
|
||||
|
||||
### Testing Patterns
|
||||
|
||||
- Test modal open/close functionality
|
||||
- Verify URL navigation between panels
|
||||
- Test Redux state integration
|
||||
@@ -234,4 +250,4 @@ const handleDisconnect = (serviceId: string) => {
|
||||
|
||||
---
|
||||
|
||||
*This settings modal system provides a robust, extensible foundation for app configuration while maintaining the sophisticated design standards of the crypto community platform.*
|
||||
_This settings modal system provides a robust, extensible foundation for app configuration while maintaining the sophisticated design standards of the crypto community platform._
|
||||
|
||||
@@ -140,16 +140,16 @@ pub fn run() {
|
||||
Control autostart via Tauri commands:
|
||||
|
||||
```typescript
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
// Enable autostart
|
||||
await invoke("plugin:autostart|enable");
|
||||
await invoke('plugin:autostart|enable');
|
||||
|
||||
// Disable autostart
|
||||
await invoke("plugin:autostart|disable");
|
||||
await invoke('plugin:autostart|disable');
|
||||
|
||||
// Check if enabled
|
||||
const isEnabled = await invoke<boolean>("plugin:autostart|is_enabled");
|
||||
const isEnabled = await invoke<boolean>('plugin:autostart|is_enabled');
|
||||
```
|
||||
|
||||
### Tray Behavior
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
branches: ['*']
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24.x
|
||||
cache: "yarn"
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -10,15 +10,18 @@ name: Package and publish
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Version Bump"]
|
||||
workflows: ['Version Bump']
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
IS_ON_MASTER: ${{ github.ref == 'refs/heads/master' }}
|
||||
SHOULD_PUBLISH: ${{ (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') && vars.PUBLISH_REPO || '' }}
|
||||
IS_PR: ${{ github.event_name == 'pull_request' }}
|
||||
SHOULD_PUBLISH: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && vars.PUBLISH_REPO || '' }}
|
||||
PUBLISH_REPO: ${{ vars.PUBLISH_REPO }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
UPDATER_GIST_URL: ${{ secrets.UPDATER_GIST_URL }}
|
||||
@@ -36,13 +39,14 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
|
||||
|
||||
- name: Extract version and tag
|
||||
id: extract-version
|
||||
run: |
|
||||
PACKAGE_VERSION=$(grep -m1 '^version' tauri/Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
PACKAGE_VERSION=$(grep -m1 '^version' src-tauri/Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
TAG_NAME="air_v${PACKAGE_VERSION}"
|
||||
RELEASE_NAME="Telegram Air v${PACKAGE_VERSION}"
|
||||
RELEASE_NAME="AlphaHuman v${PACKAGE_VERSION}"
|
||||
echo "package-version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "tag-name=$TAG_NAME" >> $GITHUB_OUTPUT
|
||||
echo "release-name=$RELEASE_NAME" >> $GITHUB_OUTPUT
|
||||
@@ -63,9 +67,9 @@ jobs:
|
||||
PACKAGE_VERSION: ${{ needs.get-version.outputs.package-version }}
|
||||
TAG_NAME: ${{ needs.get-version.outputs.tag-name }}
|
||||
run: |
|
||||
# For non-master branches or when publishing is disabled, always continue
|
||||
# For non-main branches or when publishing is disabled, always continue
|
||||
if [ -z "$SHOULD_PUBLISH" ]; then
|
||||
echo "🚧 Publishing disabled (non-master branch or PUBLISH_REPO not set)"
|
||||
echo "🚧 Publishing disabled (non-main branch or PUBLISH_REPO not set)"
|
||||
echo "should-skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
@@ -127,18 +131,19 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- platform: "macos-latest"
|
||||
args: "--target aarch64-apple-darwin"
|
||||
- platform: "macos-latest"
|
||||
args: "--target x86_64-apple-darwin"
|
||||
- platform: "ubuntu-22.04"
|
||||
args: ""
|
||||
- platform: 'macos-latest'
|
||||
args: '--target aarch64-apple-darwin'
|
||||
- platform: 'macos-latest'
|
||||
args: '--target x86_64-apple-darwin'
|
||||
- platform: 'ubuntu-22.04'
|
||||
args: ''
|
||||
runs-on: ${{ matrix.settings.platform }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
|
||||
|
||||
- name: Set Xcode version
|
||||
if: matrix.settings.platform == 'macos-latest'
|
||||
@@ -150,7 +155,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24.x
|
||||
cache: "yarn"
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -220,7 +225,7 @@ jobs:
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.UPDATER_PRIVATE_KEY_PASSWORD }}
|
||||
WITH_UPDATER: ${{ needs.get-version.outputs.should-publish != '' && 'true' || 'false' }}
|
||||
with:
|
||||
args: "-c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}"
|
||||
args: '-c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}'
|
||||
includeDebug: ${{ needs.get-version.outputs.should-publish == '' && inputs.forceRelease != 'true' }}
|
||||
includeRelease: ${{ needs.get-version.outputs.should-publish != '' || inputs.forceRelease == 'true' }}
|
||||
releaseId: ${{ needs.create-release.outputs.releaseId }}
|
||||
@@ -280,6 +285,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
|
||||
|
||||
- name: Publish release
|
||||
run: |
|
||||
|
||||
@@ -26,11 +26,11 @@ jobs:
|
||||
const prTitle = pr.title;
|
||||
const prBody = pr.body || '';
|
||||
|
||||
// Check if PR is targeting main or master
|
||||
const isTargetingMain = baseBranch === 'main' || baseBranch === 'master';
|
||||
// Check if PR is targeting main
|
||||
const isTargetingMain = baseBranch === 'main';
|
||||
|
||||
if (!isTargetingMain) {
|
||||
console.log(`PR is targeting ${baseBranch}, not main/master. Skipping protection check.`);
|
||||
console.log(`PR is targeting ${baseBranch}, not main. Skipping protection check.`);
|
||||
core.setOutput('needs_check', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,3 +49,13 @@ jobs:
|
||||
run: npx tsc
|
||||
env:
|
||||
NODE_ENV: test
|
||||
|
||||
- name: Check Prettier formatting
|
||||
run: yarn format:check
|
||||
env:
|
||||
NODE_ENV: test
|
||||
|
||||
- name: Run ESLint
|
||||
run: yarn lint
|
||||
env:
|
||||
NODE_ENV: test
|
||||
|
||||
@@ -32,3 +32,5 @@ dist-ssr
|
||||
*.sw?
|
||||
references/
|
||||
src-tauri/runtime-skill-*
|
||||
.mypy_cache
|
||||
.ruff_cache
|
||||
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Run format check first (capture exit code without breaking script)
|
||||
set +e
|
||||
yarn format:check
|
||||
FORMAT_EXIT=$?
|
||||
set -e
|
||||
|
||||
# If format check failed, run format to auto-fix
|
||||
if [ $FORMAT_EXIT -ne 0 ]; then
|
||||
echo "Formatting issues detected. Running format to auto-fix..."
|
||||
yarn format
|
||||
fi
|
||||
|
||||
# Run lint check (capture exit code without breaking script)
|
||||
set +e
|
||||
yarn lint
|
||||
LINT_EXIT=$?
|
||||
set -e
|
||||
|
||||
# If lint check failed, run lint:fix to auto-fix
|
||||
if [ $LINT_EXIT -ne 0 ]; then
|
||||
echo "Linting issues detected. Running lint:fix to auto-fix..."
|
||||
yarn lint:fix
|
||||
fi
|
||||
|
||||
# Run TypeScript compile check (capture exit code without breaking script)
|
||||
set +e
|
||||
yarn compile
|
||||
COMPILE_EXIT=$?
|
||||
set -e
|
||||
|
||||
# Exit with error if any command still fails after fixes
|
||||
if [ $FORMAT_EXIT -ne 0 ] || [ $LINT_EXIT -ne 0 ] || [ $COMPILE_EXIT -ne 0 ]; then
|
||||
echo "Pre-push checks failed. Please fix format, lint, and/or TypeScript errors before pushing."
|
||||
exit 1
|
||||
fi
|
||||
Vendored
+1
-3
@@ -1,3 +1 @@
|
||||
{
|
||||
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||
}
|
||||
{ "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
---
|
||||
|
||||
```md
|
||||
@@ -26,12 +25,12 @@ The assistant:
|
||||
|
||||
## Core Platform Behavior Summary
|
||||
|
||||
| Platform | Listening Model | Trigger to Respond |
|
||||
|--------|-----------------|-------------------|
|
||||
| Windows | Continuous (background) | Bot DM or channel activity |
|
||||
| macOS | Continuous (background) | Bot DM or channel activity |
|
||||
| Android | Continuous (foreground service) | Bot DM or channel activity |
|
||||
| iOS | On-demand only | Bot DM → push → tap → sync |
|
||||
| Platform | Listening Model | Trigger to Respond |
|
||||
| -------- | ------------------------------- | -------------------------- |
|
||||
| Windows | Continuous (background) | Bot DM or channel activity |
|
||||
| macOS | Continuous (background) | Bot DM or channel activity |
|
||||
| Android | Continuous (foreground service) | Bot DM or channel activity |
|
||||
| iOS | On-demand only | Bot DM → push → tap → sync |
|
||||
|
||||
---
|
||||
|
||||
@@ -52,17 +51,20 @@ The assistant:
|
||||
## Phase 0 — Project Skeleton & Tooling
|
||||
|
||||
### Goals
|
||||
|
||||
- Prepare repo structure
|
||||
- Establish documentation and contribution rules
|
||||
- No business logic yet
|
||||
|
||||
### Deliverables
|
||||
|
||||
- Monorepo structure
|
||||
- Tauri project scaffold
|
||||
- Mobile targets enabled (Tauri v2)
|
||||
- CI hooks (optional)
|
||||
|
||||
### Repo Structure
|
||||
|
||||
```
|
||||
|
||||
/apps
|
||||
@@ -75,19 +77,20 @@ The assistant:
|
||||
/backend
|
||||
/docs
|
||||
|
||||
````
|
||||
```
|
||||
|
||||
### Documentation Commands (Required)
|
||||
|
||||
```bash
|
||||
/docs/architecture.md # high-level system design
|
||||
/docs/decisions/ADR-000.md # initial architecture decision record
|
||||
````
|
||||
```
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
* App builds and runs (blank UI)
|
||||
* Docs folder initialized
|
||||
* ADR process agreed upon
|
||||
- App builds and runs (blank UI)
|
||||
- Docs folder initialized
|
||||
- ADR process agreed upon
|
||||
|
||||
---
|
||||
|
||||
@@ -99,24 +102,24 @@ Build the **entire UI flow** before implementing logic.
|
||||
|
||||
### UI Screens
|
||||
|
||||
* Login / Signup
|
||||
* Telegram Connect (bot instructions)
|
||||
* Channel Selection
|
||||
* Sync Status Screen
|
||||
* Settings (background, privacy, storage)
|
||||
* Plan & Billing (stub)
|
||||
* Logs / Activity View (local only)
|
||||
- Login / Signup
|
||||
- Telegram Connect (bot instructions)
|
||||
- Channel Selection
|
||||
- Sync Status Screen
|
||||
- Settings (background, privacy, storage)
|
||||
- Plan & Billing (stub)
|
||||
- Logs / Activity View (local only)
|
||||
|
||||
### Platforms
|
||||
|
||||
* All platforms (desktop + mobile)
|
||||
- All platforms (desktop + mobile)
|
||||
|
||||
### Deliverables
|
||||
|
||||
* Responsive UI
|
||||
* Navigation between screens
|
||||
* Mock data only
|
||||
* No Telegram, no backend, no Rust logic
|
||||
- Responsive UI
|
||||
- Navigation between screens
|
||||
- Mock data only
|
||||
- No Telegram, no backend, no Rust logic
|
||||
|
||||
### Documentation Commands
|
||||
|
||||
@@ -128,9 +131,9 @@ Build the **entire UI flow** before implementing logic.
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
* Entire app is navigable
|
||||
* No dead-end screens
|
||||
* UI approved before logic begins
|
||||
- Entire app is navigable
|
||||
- No dead-end screens
|
||||
- UI approved before logic begins
|
||||
|
||||
---
|
||||
|
||||
@@ -142,19 +145,19 @@ Implement the **local agent engine** without Telegram or backend.
|
||||
|
||||
### Components
|
||||
|
||||
* Rust agent runtime
|
||||
* Intent router (question / sync / config)
|
||||
* Processing pipeline (stubbed)
|
||||
* Response composer (mock responses)
|
||||
- Rust agent runtime
|
||||
- Intent router (question / sync / config)
|
||||
- Processing pipeline (stubbed)
|
||||
- Response composer (mock responses)
|
||||
|
||||
### Deliverables
|
||||
|
||||
* Tauri IPC commands:
|
||||
- Tauri IPC commands:
|
||||
- `agent_init`
|
||||
- `agent_process_query`
|
||||
- `agent_status`
|
||||
|
||||
* `agent_init`
|
||||
* `agent_process_query`
|
||||
* `agent_status`
|
||||
* In-memory only state
|
||||
- In-memory only state
|
||||
|
||||
### Documentation Commands
|
||||
|
||||
@@ -166,9 +169,9 @@ Implement the **local agent engine** without Telegram or backend.
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
* UI can send a question
|
||||
* Agent returns a mock response
|
||||
* No persistence yet
|
||||
- UI can send a question
|
||||
- Agent returns a mock response
|
||||
- No persistence yet
|
||||
|
||||
---
|
||||
|
||||
@@ -180,20 +183,20 @@ Add **efficient, privacy-first local storage**.
|
||||
|
||||
### Storage Rules
|
||||
|
||||
* No Telegram message bodies by default
|
||||
* Store only:
|
||||
- No Telegram message bodies by default
|
||||
- Store only:
|
||||
- channel IDs
|
||||
- last processed message IDs
|
||||
- dedupe hashes
|
||||
|
||||
* channel IDs
|
||||
* last processed message IDs
|
||||
* dedupe hashes
|
||||
* Encrypted at rest
|
||||
- Encrypted at rest
|
||||
|
||||
### Deliverables
|
||||
|
||||
* Encrypted SQLite
|
||||
* OS keychain integration
|
||||
* Storage abstraction in Rust
|
||||
* “Ephemeral mode” toggle
|
||||
- Encrypted SQLite
|
||||
- OS keychain integration
|
||||
- Storage abstraction in Rust
|
||||
- “Ephemeral mode” toggle
|
||||
|
||||
### Documentation Commands
|
||||
|
||||
@@ -205,9 +208,9 @@ Add **efficient, privacy-first local storage**.
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
* App restarts without losing cursors
|
||||
* “Delete local data” wipes all state
|
||||
* No plaintext sensitive data on disk
|
||||
- App restarts without losing cursors
|
||||
- “Delete local data” wipes all state
|
||||
- No plaintext sensitive data on disk
|
||||
|
||||
---
|
||||
|
||||
@@ -219,22 +222,22 @@ Turn the app into a **real Telegram agent assistant**.
|
||||
|
||||
### Telegram Capabilities (MVP)
|
||||
|
||||
* Bot DM interaction
|
||||
* Read user questions
|
||||
* Fetch channel messages (where bot has access)
|
||||
* Reply via DM
|
||||
- Bot DM interaction
|
||||
- Read user questions
|
||||
- Fetch channel messages (where bot has access)
|
||||
- Reply via DM
|
||||
|
||||
### Platform Behavior
|
||||
|
||||
* Windows/macOS/Android: continuous polling
|
||||
* iOS: no polling (on-demand only)
|
||||
- Windows/macOS/Android: continuous polling
|
||||
- iOS: no polling (on-demand only)
|
||||
|
||||
### Deliverables
|
||||
|
||||
* Telegram Bot Gateway (Rust)
|
||||
* Update polling / fetching
|
||||
* Message dedupe + cursoring
|
||||
* Agent replies sent via bot DM
|
||||
- Telegram Bot Gateway (Rust)
|
||||
- Update polling / fetching
|
||||
- Message dedupe + cursoring
|
||||
- Agent replies sent via bot DM
|
||||
|
||||
### Documentation Commands
|
||||
|
||||
@@ -246,10 +249,10 @@ Turn the app into a **real Telegram agent assistant**.
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
* User asks a question in Telegram
|
||||
* App processes it
|
||||
* Bot replies correctly
|
||||
* No duplicate replies
|
||||
- User asks a question in Telegram
|
||||
- App processes it
|
||||
- Bot replies correctly
|
||||
- No duplicate replies
|
||||
|
||||
---
|
||||
|
||||
@@ -263,24 +266,24 @@ Enable **platform-appropriate background behavior**.
|
||||
|
||||
#### Windows
|
||||
|
||||
* Tray app
|
||||
* Autostart
|
||||
* Background polling
|
||||
- Tray app
|
||||
- Autostart
|
||||
- Background polling
|
||||
|
||||
#### macOS
|
||||
|
||||
* Menu bar app
|
||||
* Launch at login
|
||||
- Menu bar app
|
||||
- Launch at login
|
||||
|
||||
#### Android
|
||||
|
||||
* Foreground service (persistent notification)
|
||||
* Background polling allowed
|
||||
- Foreground service (persistent notification)
|
||||
- Background polling allowed
|
||||
|
||||
#### iOS
|
||||
|
||||
* ❌ No continuous background
|
||||
* Only foreground execution
|
||||
- ❌ No continuous background
|
||||
- Only foreground execution
|
||||
|
||||
### Documentation Commands
|
||||
|
||||
@@ -293,9 +296,9 @@ Enable **platform-appropriate background behavior**.
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
* Desktop apps run without UI open
|
||||
* Android foreground service stable
|
||||
* iOS behaves strictly foreground-only
|
||||
- Desktop apps run without UI open
|
||||
- Android foreground service stable
|
||||
- iOS behaves strictly foreground-only
|
||||
|
||||
---
|
||||
|
||||
@@ -307,23 +310,23 @@ Introduce backend **without violating privacy goals**.
|
||||
|
||||
### Backend Responsibilities
|
||||
|
||||
* Authentication
|
||||
* Device registration
|
||||
* Entitlements
|
||||
* Push notifications
|
||||
* Payment verification
|
||||
- Authentication
|
||||
- Device registration
|
||||
- Entitlements
|
||||
- Push notifications
|
||||
- Payment verification
|
||||
|
||||
### Explicit Non-Responsibilities
|
||||
|
||||
* No Telegram message storage
|
||||
* No agent logic
|
||||
* No summaries
|
||||
- No Telegram message storage
|
||||
- No agent logic
|
||||
- No summaries
|
||||
|
||||
### Deliverables
|
||||
|
||||
* Auth flow wired into UI
|
||||
* Entitlements fetched on startup
|
||||
* Device registered for push
|
||||
- Auth flow wired into UI
|
||||
- Entitlements fetched on startup
|
||||
- Device registered for push
|
||||
|
||||
### Documentation Commands
|
||||
|
||||
@@ -335,9 +338,9 @@ Introduce backend **without violating privacy goals**.
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
* User login works
|
||||
* Entitlements enforced locally
|
||||
* Backend DB contains no message content
|
||||
- User login works
|
||||
- Entitlements enforced locally
|
||||
- Backend DB contains no message content
|
||||
|
||||
---
|
||||
|
||||
@@ -359,9 +362,9 @@ Implement the **iOS-specific agent interaction model**.
|
||||
|
||||
### Deliverables
|
||||
|
||||
* APNs integration
|
||||
* Push payload handling
|
||||
* Sync-on-open logic
|
||||
- APNs integration
|
||||
- Push payload handling
|
||||
- Sync-on-open logic
|
||||
|
||||
### Documentation Commands
|
||||
|
||||
@@ -372,9 +375,9 @@ Implement the **iOS-specific agent interaction model**.
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
* Push reliably opens app
|
||||
* Sync runs automatically
|
||||
* Bot replies successfully
|
||||
- Push reliably opens app
|
||||
- Sync runs automatically
|
||||
- Bot replies successfully
|
||||
|
||||
---
|
||||
|
||||
@@ -386,15 +389,15 @@ Monetize safely and correctly.
|
||||
|
||||
### Platforms
|
||||
|
||||
* Desktop: Stripe / Paddle
|
||||
* Android: Play Billing
|
||||
* iOS: StoreKit
|
||||
- Desktop: Stripe / Paddle
|
||||
- Android: Play Billing
|
||||
- iOS: StoreKit
|
||||
|
||||
### Deliverables
|
||||
|
||||
* Purchase flow per platform
|
||||
* Receipt verification
|
||||
* Feature gating in Rust
|
||||
- Purchase flow per platform
|
||||
- Receipt verification
|
||||
- Feature gating in Rust
|
||||
|
||||
### Documentation Commands
|
||||
|
||||
@@ -406,9 +409,9 @@ Monetize safely and correctly.
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
* Paid features unlock correctly
|
||||
* Downgrades enforced
|
||||
* Offline grace period handled
|
||||
- Paid features unlock correctly
|
||||
- Downgrades enforced
|
||||
- Offline grace period handled
|
||||
|
||||
---
|
||||
|
||||
@@ -420,11 +423,11 @@ Stability, observability, and trust.
|
||||
|
||||
### Deliverables
|
||||
|
||||
* Error handling
|
||||
* Rate limiting
|
||||
* Abuse prevention
|
||||
* Crash-safe storage
|
||||
* UX polish
|
||||
- Error handling
|
||||
- Rate limiting
|
||||
- Abuse prevention
|
||||
- Crash-safe storage
|
||||
- UX polish
|
||||
|
||||
### Documentation Commands
|
||||
|
||||
@@ -436,18 +439,18 @@ Stability, observability, and trust.
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
* No critical crashes
|
||||
* No duplicate Telegram replies
|
||||
* Clear user-facing error states
|
||||
- No critical crashes
|
||||
- No duplicate Telegram replies
|
||||
- Clear user-facing error states
|
||||
|
||||
---
|
||||
|
||||
## Final Notes
|
||||
|
||||
* The **Telegram bot is the product interface**
|
||||
* The **Tauri app is the execution engine**
|
||||
* The **backend is infrastructure, not intelligence**
|
||||
* iOS behavior is intentionally constrained for correctness
|
||||
- The **Telegram bot is the product interface**
|
||||
- The **Tauri app is the execution engine**
|
||||
- The **backend is infrastructure, not intelligence**
|
||||
- iOS behavior is intentionally constrained for correctness
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
/* eslint-disable no-null/no-null */
|
||||
export default function prepareTauriConfig() {
|
||||
const config = {
|
||||
build: {
|
||||
frontendDist: process.env.BASE_URL,
|
||||
devUrl: null,
|
||||
},
|
||||
bundle: {
|
||||
windows: {},
|
||||
},
|
||||
build: { frontendDist: process.env.BASE_URL, devUrl: null },
|
||||
bundle: { windows: {} },
|
||||
identifier: 'org.telegram.TelegramAir',
|
||||
};
|
||||
|
||||
|
||||
+24
-10
@@ -5,16 +5,19 @@
|
||||
Our design system embodies three core principles that align with the needs of crypto professionals:
|
||||
|
||||
### 1. **Calm Sophistication**
|
||||
|
||||
- Soft, muted color palettes that reduce visual noise
|
||||
- Generous whitespace for breathing room
|
||||
- Subtle animations that feel natural, not jarring
|
||||
|
||||
### 2. **Trust Through Clarity**
|
||||
|
||||
- Clear information hierarchy
|
||||
- Consistent component patterns
|
||||
- Professional typography that's easy to scan
|
||||
|
||||
### 3. **Functional Beauty**
|
||||
|
||||
- Every aesthetic choice serves a functional purpose
|
||||
- Data visualization that's both beautiful and informative
|
||||
- Interactions that feel intuitive and responsive
|
||||
@@ -24,21 +27,25 @@ Our design system embodies three core principles that align with the needs of cr
|
||||
### Primary Palette
|
||||
|
||||
#### Canvas (Backgrounds)
|
||||
|
||||
- **Canvas-50** (#FAFAF9): Primary background, slight warmth prevents sterility
|
||||
- **Canvas-100** (#F5F5F4): Secondary surfaces, cards, elevated elements
|
||||
- **Canvas-200** (#E5E5E3): Tertiary backgrounds, hover states
|
||||
|
||||
#### Ocean Blue (Primary Actions)
|
||||
|
||||
- **Primary-500** (#5B9BF3): Main brand color, CTAs, active states
|
||||
- **Primary-600** (#4A83DD): Hover states for primary elements
|
||||
- **Why**: Blue conveys trust, stability, and professionalism - crucial for financial platforms
|
||||
|
||||
#### Sage Green (Success/Growth)
|
||||
|
||||
- **Sage-500** (#4DC46F): Positive changes, success states, gains
|
||||
- **Sage-600** (#3BA858): Hover states for success elements
|
||||
- **Why**: A sophisticated green that feels less aggressive than typical "success" greens
|
||||
|
||||
#### Coral Red (Errors/Losses)
|
||||
|
||||
- **Coral-500** (#F56565): Errors, losses, critical actions
|
||||
- **Why**: Softer than harsh reds, maintains professionalism even in negative states
|
||||
|
||||
@@ -95,40 +102,42 @@ font-mono: 'JetBrains Mono'
|
||||
### Cards
|
||||
|
||||
#### Elevated Card
|
||||
|
||||
```jsx
|
||||
<div className="card-elevated">
|
||||
// Shadow creates depth, white bg provides focus
|
||||
</div>
|
||||
<div className="card-elevated">// Shadow creates depth, white bg provides focus</div>
|
||||
```
|
||||
|
||||
- Use for primary content containers
|
||||
- Subtle shadow creates hierarchy without borders
|
||||
|
||||
#### Interactive Card
|
||||
|
||||
```jsx
|
||||
<div className="card-interactive">
|
||||
// Hover state with top border animation
|
||||
</div>
|
||||
<div className="card-interactive">// Hover state with top border animation</div>
|
||||
```
|
||||
|
||||
- Use for clickable items
|
||||
- Progressive disclosure through hover states
|
||||
|
||||
#### Glass Card
|
||||
|
||||
```jsx
|
||||
<div className="glass-surface">
|
||||
// Backdrop blur for overlay contexts
|
||||
</div>
|
||||
<div className="glass-surface">// Backdrop blur for overlay contexts</div>
|
||||
```
|
||||
|
||||
- Use for floating elements, overlays
|
||||
- Creates depth while maintaining context
|
||||
|
||||
### Buttons
|
||||
|
||||
#### Hierarchy
|
||||
|
||||
1. **Premium Button**: Primary CTAs, most important actions
|
||||
2. **Glass Button**: Secondary actions, less emphasis
|
||||
3. **Outline Button**: Tertiary actions, minimal visual weight
|
||||
|
||||
#### Button Psychology
|
||||
|
||||
- **Rounded corners** (0.75rem): Feels approachable, modern
|
||||
- **Subtle shadows**: Creates tactile feeling, encourages clicks
|
||||
- **Hover animations**: Provides feedback, builds confidence
|
||||
@@ -136,9 +145,11 @@ font-mono: 'JetBrains Mono'
|
||||
### Forms
|
||||
|
||||
#### Input Design
|
||||
|
||||
```jsx
|
||||
<input className="input-elevated" />
|
||||
```
|
||||
|
||||
- **White background**: Maximum contrast for readability
|
||||
- **Subtle border**: Defines boundaries without heaviness
|
||||
- **Focus ring**: Clear feedback for keyboard navigation
|
||||
@@ -146,6 +157,7 @@ font-mono: 'JetBrains Mono'
|
||||
## Motion & Animation
|
||||
|
||||
### Timing Functions
|
||||
|
||||
```css
|
||||
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@@ -201,6 +213,7 @@ font-mono: 'JetBrains Mono'
|
||||
## Responsive Design
|
||||
|
||||
### Breakpoints
|
||||
|
||||
```css
|
||||
sm: 640px
|
||||
md: 768px
|
||||
@@ -245,9 +258,10 @@ xl: 1280px
|
||||
## Summary
|
||||
|
||||
This design system creates a premium experience that:
|
||||
|
||||
- **Builds trust** through consistency and clarity
|
||||
- **Reduces cognitive load** with calm aesthetics
|
||||
- **Enhances usability** through thoughtful interactions
|
||||
- **Scales elegantly** across devices and contexts
|
||||
|
||||
The result is a platform that feels sophisticated yet approachable, powerful yet simple - perfect for crypto professionals who demand both functionality and aesthetic excellence.
|
||||
The result is a platform that feels sophisticated yet approachable, powerful yet simple - perfect for crypto professionals who demand both functionality and aesthetic excellence.
|
||||
|
||||
@@ -11,7 +11,7 @@ The application uses Redux Toolkit with Redux-Persist for robust state managemen
|
||||
const persistConfig = {
|
||||
key: 'root',
|
||||
storage,
|
||||
whitelist: ['auth', 'telegram'] // Persisted slices
|
||||
whitelist: ['auth', 'telegram'], // Persisted slices
|
||||
};
|
||||
```
|
||||
|
||||
@@ -20,24 +20,24 @@ const persistConfig = {
|
||||
```typescript
|
||||
RootState = {
|
||||
auth: {
|
||||
token: string | null, // JWT (persisted)
|
||||
isOnboardedByUser: Record<string, boolean> // Per-user flag (persisted)
|
||||
token: string | null, // JWT (persisted)
|
||||
isOnboardedByUser: Record<string, boolean>, // Per-user flag (persisted)
|
||||
},
|
||||
socket: {
|
||||
byUser: Record<string, { // Per user ID
|
||||
status: "connecting" | "connected" | "disconnected",
|
||||
socketId: string | null
|
||||
}>
|
||||
},
|
||||
user: {
|
||||
profile: User | null,
|
||||
loading: boolean,
|
||||
error: string | null
|
||||
byUser: Record<
|
||||
string,
|
||||
{
|
||||
// Per user ID
|
||||
status: 'connecting' | 'connected' | 'disconnected';
|
||||
socketId: string | null;
|
||||
}
|
||||
>,
|
||||
},
|
||||
user: { profile: User | null, loading: boolean, error: string | null },
|
||||
telegram: {
|
||||
byUser: Record<string, TelegramState> // Per Telegram user (persisted)
|
||||
}
|
||||
}
|
||||
byUser: Record<string, TelegramState>, // Per Telegram user (persisted)
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -47,6 +47,7 @@ RootState = {
|
||||
Manages JWT token and per-user onboarding status.
|
||||
|
||||
**State:**
|
||||
|
||||
```typescript
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
@@ -55,11 +56,13 @@ interface AuthState {
|
||||
```
|
||||
|
||||
**Actions:**
|
||||
|
||||
- `setToken(token: string)` - Store JWT after login
|
||||
- `clearToken()` - Remove token on logout
|
||||
- `setOnboarded({ userId, isOnboarded })` - Mark user as onboarded
|
||||
|
||||
**Selectors (`store/authSelectors.ts`):**
|
||||
|
||||
- `selectToken` - Get current JWT
|
||||
- `selectIsOnboarded(userId)` - Check if user completed onboarding
|
||||
|
||||
@@ -68,21 +71,24 @@ interface AuthState {
|
||||
Tracks Socket.io connection status per user.
|
||||
|
||||
**State:**
|
||||
|
||||
```typescript
|
||||
interface SocketState {
|
||||
byUser: Record<string, {
|
||||
status: 'connecting' | 'connected' | 'disconnected';
|
||||
socketId: string | null;
|
||||
}>;
|
||||
byUser: Record<
|
||||
string,
|
||||
{ status: 'connecting' | 'connected' | 'disconnected'; socketId: string | null }
|
||||
>;
|
||||
}
|
||||
```
|
||||
|
||||
**Actions:**
|
||||
|
||||
- `setSocketStatus({ userId, status })` - Update connection status
|
||||
- `setSocketId({ userId, socketId })` - Store socket ID
|
||||
- `clearSocketState(userId)` - Clear user's socket state
|
||||
|
||||
**Selectors (`store/socketSelectors.ts`):**
|
||||
|
||||
- `selectSocketStatus(userId)` - Get connection status
|
||||
- `selectIsSocketConnected(userId)` - Boolean connected check
|
||||
|
||||
@@ -91,6 +97,7 @@ interface SocketState {
|
||||
Stores user profile data.
|
||||
|
||||
**State:**
|
||||
|
||||
```typescript
|
||||
interface UserState {
|
||||
profile: User | null;
|
||||
@@ -100,6 +107,7 @@ interface UserState {
|
||||
```
|
||||
|
||||
**Actions:**
|
||||
|
||||
- `setUser(user)` - Store user profile
|
||||
- `setUserLoading(loading)` - Set loading state
|
||||
- `setUserError(error)` - Set error state
|
||||
@@ -110,6 +118,7 @@ interface UserState {
|
||||
Complex nested state management for Telegram integration.
|
||||
|
||||
**Files:**
|
||||
|
||||
- `index.ts` - Slice exports (actions, thunks)
|
||||
- `types.ts` - Entity and state interfaces
|
||||
- `reducers.ts` - Synchronous reducers
|
||||
@@ -117,6 +126,7 @@ Complex nested state management for Telegram integration.
|
||||
- `thunks.ts` - Async operations
|
||||
|
||||
**State Structure:**
|
||||
|
||||
```typescript
|
||||
telegram.byUser[telegramUserId] = {
|
||||
connectionStatus: "disconnected" | "connecting" | "connected" | "error",
|
||||
@@ -131,6 +141,7 @@ telegram.byUser[telegramUserId] = {
|
||||
```
|
||||
|
||||
**Reducers:**
|
||||
|
||||
- `setCurrentUser` - Store authenticated Telegram user
|
||||
- `setSessionString` - Store MTProto session (for persistence)
|
||||
- `setConnectionStatus` - Update connection state
|
||||
@@ -140,6 +151,7 @@ telegram.byUser[telegramUserId] = {
|
||||
- `setThreads` - Store thread data
|
||||
|
||||
**Thunks (`store/telegram/thunks.ts`):**
|
||||
|
||||
- `initializeTelegram(userId)` - Initialize MTProto client
|
||||
- `connectTelegram(userId)` - Establish Telegram connection
|
||||
- `fetchChats(userId)` - Load chat list
|
||||
@@ -147,6 +159,7 @@ telegram.byUser[telegramUserId] = {
|
||||
- `disconnectTelegram(userId)` - Clean disconnect
|
||||
|
||||
**Selectors (`store/telegramSelectors.ts`):**
|
||||
|
||||
- `selectTelegramState(userId)` - Get full Telegram state
|
||||
- `selectTelegramConnectionStatus(userId)` - Get connection status
|
||||
- `selectTelegramAuthStatus(userId)` - Get auth status
|
||||
@@ -166,39 +179,40 @@ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
## Persistence Configuration
|
||||
|
||||
### What's Persisted
|
||||
|
||||
- `auth.token` - JWT for authentication
|
||||
- `auth.isOnboardedByUser` - Per-user onboarding status
|
||||
- `telegram.byUser` - Telegram state (sessions, chats, etc.)
|
||||
|
||||
### What's NOT Persisted
|
||||
|
||||
- `socket` - Connection state (reconnects on app start)
|
||||
- `user.loading` / `user.error` - Transient UI states
|
||||
- Telegram loading/error states
|
||||
|
||||
### Storage Backend
|
||||
|
||||
Redux-Persist uses localStorage adapter by default. This is the ONLY acceptable use of localStorage in the application.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Reading State
|
||||
|
||||
```typescript
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
|
||||
function MyComponent() {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const isConnected = useAppSelector((state) =>
|
||||
state.socket.byUser[userId]?.status === 'connected'
|
||||
);
|
||||
const chats = useAppSelector((state) =>
|
||||
state.telegram.byUser[userId]?.chats
|
||||
);
|
||||
const token = useAppSelector(state => state.auth.token);
|
||||
const isConnected = useAppSelector(state => state.socket.byUser[userId]?.status === 'connected');
|
||||
const chats = useAppSelector(state => state.telegram.byUser[userId]?.chats);
|
||||
}
|
||||
```
|
||||
|
||||
### Dispatching Actions
|
||||
|
||||
```typescript
|
||||
import { clearToken, setToken } from '../store/authSlice';
|
||||
import { useAppDispatch } from '../store/hooks';
|
||||
import { setToken, clearToken } from '../store/authSlice';
|
||||
import { initializeTelegram } from '../store/telegram/thunks';
|
||||
|
||||
function MyComponent() {
|
||||
@@ -217,16 +231,15 @@ function MyComponent() {
|
||||
```
|
||||
|
||||
### Using Selectors
|
||||
|
||||
```typescript
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { selectIsOnboarded } from '../store/authSelectors';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { selectTelegramConnectionStatus } from '../store/telegramSelectors';
|
||||
|
||||
function MyComponent({ userId }) {
|
||||
const isOnboarded = useAppSelector((state) => selectIsOnboarded(state, userId));
|
||||
const connectionStatus = useAppSelector((state) =>
|
||||
selectTelegramConnectionStatus(state, userId)
|
||||
);
|
||||
const isOnboarded = useAppSelector(state => selectIsOnboarded(state, userId));
|
||||
const connectionStatus = useAppSelector(state => selectTelegramConnectionStatus(state, userId));
|
||||
}
|
||||
```
|
||||
|
||||
@@ -240,4 +253,4 @@ function MyComponent({ userId }) {
|
||||
|
||||
---
|
||||
|
||||
*Previous: [Architecture Overview](./01-architecture.md) | Next: [Services Layer](./03-services.md)*
|
||||
_Previous: [Architecture Overview](./01-architecture.md) | Next: [Services Layer](./03-services.md)_
|
||||
|
||||
+22
-14
@@ -22,12 +22,14 @@ Services Layer
|
||||
HTTP REST client for backend communication.
|
||||
|
||||
### Features
|
||||
|
||||
- Fetch-based implementation
|
||||
- Auto-injects JWT from Redux store
|
||||
- Typed request/response handling
|
||||
- Error handling with typed errors
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import apiClient from '../services/apiClient';
|
||||
|
||||
@@ -35,19 +37,16 @@ import apiClient from '../services/apiClient';
|
||||
const user = await apiClient.get<User>('/users/me');
|
||||
|
||||
// POST request
|
||||
const result = await apiClient.post<LoginResponse>('/auth/login', {
|
||||
email,
|
||||
password
|
||||
});
|
||||
const result = await apiClient.post<LoginResponse>('/auth/login', { email, password });
|
||||
|
||||
// With custom headers
|
||||
const data = await apiClient.get<Data>('/endpoint', {
|
||||
headers: { 'X-Custom': 'value' }
|
||||
});
|
||||
const data = await apiClient.get<Data>('/endpoint', { headers: { 'X-Custom': 'value' } });
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Reads `VITE_BACKEND_URL` from environment or uses default:
|
||||
|
||||
```typescript
|
||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.example.com';
|
||||
```
|
||||
@@ -93,12 +92,14 @@ const settings = await userApi.getSettings();
|
||||
Socket.io client singleton for real-time communication.
|
||||
|
||||
### Features
|
||||
|
||||
- Singleton pattern - single connection per app
|
||||
- Auth token passed in socket `auth` object
|
||||
- Transports: polling first, then WebSocket upgrade
|
||||
- Auto-reconnection handling
|
||||
|
||||
### API
|
||||
|
||||
```typescript
|
||||
import socketService from '../services/socketService';
|
||||
|
||||
@@ -112,7 +113,7 @@ socketService.disconnect();
|
||||
socketService.emit('event-name', data);
|
||||
|
||||
// Listen for events
|
||||
socketService.on('event-name', (data) => {
|
||||
socketService.on('event-name', data => {
|
||||
// Handle event
|
||||
});
|
||||
|
||||
@@ -120,7 +121,7 @@ socketService.on('event-name', (data) => {
|
||||
socketService.off('event-name', handler);
|
||||
|
||||
// One-time listener
|
||||
socketService.once('event-name', (data) => {
|
||||
socketService.once('event-name', data => {
|
||||
// Handle once
|
||||
});
|
||||
|
||||
@@ -132,6 +133,7 @@ const isConnected = socketService.isConnected();
|
||||
```
|
||||
|
||||
### Connection Flow
|
||||
|
||||
```typescript
|
||||
// In SocketProvider.tsx
|
||||
useEffect(() => {
|
||||
@@ -157,13 +159,14 @@ useEffect(() => {
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
const socket = io(BACKEND_URL, {
|
||||
auth: { token },
|
||||
transports: ['polling', 'websocket'],
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 1000
|
||||
reconnectionDelay: 1000,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -172,12 +175,14 @@ const socket = io(BACKEND_URL, {
|
||||
Telegram MTProto client singleton.
|
||||
|
||||
### Features
|
||||
|
||||
- Singleton pattern - one client per user
|
||||
- Session persistence via Redux (not localStorage)
|
||||
- Auto-retry for FLOOD_WAIT up to 60s
|
||||
- Supports QR login and phone auth
|
||||
|
||||
### Initialization
|
||||
|
||||
```typescript
|
||||
import mtprotoService from '../services/mtprotoService';
|
||||
|
||||
@@ -192,12 +197,11 @@ await client.connect();
|
||||
```
|
||||
|
||||
### Session Management
|
||||
|
||||
```typescript
|
||||
// Session is stored in Redux, not localStorage
|
||||
// In TelegramProvider:
|
||||
const sessionString = useAppSelector((state) =>
|
||||
state.telegram.byUser[userId]?.sessionString
|
||||
);
|
||||
const sessionString = useAppSelector(state => state.telegram.byUser[userId]?.sessionString);
|
||||
|
||||
// When session updates
|
||||
useEffect(() => {
|
||||
@@ -212,6 +216,7 @@ dispatch(setSessionString({ userId, sessionString: newSession }));
|
||||
```
|
||||
|
||||
### API Operations
|
||||
|
||||
```typescript
|
||||
// Get current user
|
||||
const me = await client.getMe();
|
||||
@@ -227,6 +232,7 @@ const messages = await client.getMessages(peer, { limit: 50 });
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await client.connect();
|
||||
@@ -246,6 +252,7 @@ try {
|
||||
## Service Integration with Providers
|
||||
|
||||
### SocketProvider
|
||||
|
||||
```typescript
|
||||
// providers/SocketProvider.tsx
|
||||
export function SocketProvider({ children }) {
|
||||
@@ -264,6 +271,7 @@ export function SocketProvider({ children }) {
|
||||
```
|
||||
|
||||
### TelegramProvider
|
||||
|
||||
```typescript
|
||||
// providers/TelegramProvider.tsx
|
||||
export function TelegramProvider({ children }) {
|
||||
@@ -294,4 +302,4 @@ export function TelegramProvider({ children }) {
|
||||
|
||||
---
|
||||
|
||||
*Previous: [State Management](./02-state-management.md) | Next: [MCP System](./04-mcp-system.md)*
|
||||
_Previous: [State Management](./02-state-management.md) | Next: [MCP System](./04-mcp-system.md)_
|
||||
|
||||
+124
-144
@@ -64,6 +64,7 @@ interface MCPError {
|
||||
Socket.IO-based JSON-RPC 2.0 transport.
|
||||
|
||||
**Features:**
|
||||
|
||||
- Request ID tracking
|
||||
- 30-second timeout
|
||||
- Error handling
|
||||
@@ -138,10 +139,7 @@ interface TelegramMCPToolHandler {
|
||||
inputSchema: JSONSchema;
|
||||
call: (
|
||||
args: unknown,
|
||||
context: {
|
||||
telegramClient: TelegramClient;
|
||||
userId: string;
|
||||
}
|
||||
context: { telegramClient: TelegramClient; userId: string }
|
||||
) => Promise<ToolResult>;
|
||||
}
|
||||
|
||||
@@ -157,125 +155,135 @@ interface ToolResult {
|
||||
### 81 Telegram Tools
|
||||
|
||||
#### User & Profile (5 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `getMe` | Get current authenticated user |
|
||||
| `getUserInfo` | Get info about any user |
|
||||
| `getUserPhotos` | Get user profile photos |
|
||||
| `getUserStatus` | Get online status |
|
||||
| `setUserStatus` | Update own status |
|
||||
|
||||
| Tool | Description |
|
||||
| --------------- | ------------------------------ |
|
||||
| `getMe` | Get current authenticated user |
|
||||
| `getUserInfo` | Get info about any user |
|
||||
| `getUserPhotos` | Get user profile photos |
|
||||
| `getUserStatus` | Get online status |
|
||||
| `setUserStatus` | Update own status |
|
||||
|
||||
#### Chats & Dialogs (12 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `getChats` | Fetch chat list |
|
||||
| `getChatInfo` | Get chat details |
|
||||
| `createGroup` | Create new group |
|
||||
| `createChannel` | Create new channel |
|
||||
| `leaveChat` | Leave chat/group/channel |
|
||||
| `deleteChat` | Delete chat |
|
||||
| `muteChat` | Mute notifications |
|
||||
| `unmuteChat` | Unmute notifications |
|
||||
| `pinChat` | Pin chat to top |
|
||||
| `unpinChat` | Unpin chat |
|
||||
| `archiveChat` | Archive chat |
|
||||
| `unarchiveChat` | Unarchive chat |
|
||||
|
||||
| Tool | Description |
|
||||
| --------------- | ------------------------ |
|
||||
| `getChats` | Fetch chat list |
|
||||
| `getChatInfo` | Get chat details |
|
||||
| `createGroup` | Create new group |
|
||||
| `createChannel` | Create new channel |
|
||||
| `leaveChat` | Leave chat/group/channel |
|
||||
| `deleteChat` | Delete chat |
|
||||
| `muteChat` | Mute notifications |
|
||||
| `unmuteChat` | Unmute notifications |
|
||||
| `pinChat` | Pin chat to top |
|
||||
| `unpinChat` | Unpin chat |
|
||||
| `archiveChat` | Archive chat |
|
||||
| `unarchiveChat` | Unarchive chat |
|
||||
|
||||
#### Messages (15 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `getHistory` | Fetch message history |
|
||||
| `sendMessage` | Send text message |
|
||||
| `replyToMessage` | Reply to specific message |
|
||||
| `editMessage` | Edit sent message |
|
||||
| `deleteMessage` | Delete message |
|
||||
| `forwardMessage` | Forward to another chat |
|
||||
| `pinMessage` | Pin message in chat |
|
||||
| `unpinMessage` | Unpin message |
|
||||
| `markAsRead` | Mark messages as read |
|
||||
| `searchMessages` | Search in chat |
|
||||
| `translateMessage` | Translate message text |
|
||||
| `getMessageReactions` | Get reactions |
|
||||
| `addReaction` | React to message |
|
||||
| `removeReaction` | Remove reaction |
|
||||
| `reportMessage` | Report spam/abuse |
|
||||
|
||||
| Tool | Description |
|
||||
| --------------------- | ------------------------- |
|
||||
| `getHistory` | Fetch message history |
|
||||
| `sendMessage` | Send text message |
|
||||
| `replyToMessage` | Reply to specific message |
|
||||
| `editMessage` | Edit sent message |
|
||||
| `deleteMessage` | Delete message |
|
||||
| `forwardMessage` | Forward to another chat |
|
||||
| `pinMessage` | Pin message in chat |
|
||||
| `unpinMessage` | Unpin message |
|
||||
| `markAsRead` | Mark messages as read |
|
||||
| `searchMessages` | Search in chat |
|
||||
| `translateMessage` | Translate message text |
|
||||
| `getMessageReactions` | Get reactions |
|
||||
| `addReaction` | React to message |
|
||||
| `removeReaction` | Remove reaction |
|
||||
| `reportMessage` | Report spam/abuse |
|
||||
|
||||
#### Media (10 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `sendPhoto` | Send image |
|
||||
| `sendVideo` | Send video |
|
||||
| `sendDocument` | Send file |
|
||||
| `sendVoice` | Send voice message |
|
||||
| `sendAudio` | Send audio file |
|
||||
| `sendSticker` | Send sticker |
|
||||
| `sendGif` | Send animation |
|
||||
| `sendLocation` | Send location |
|
||||
| `sendContact` | Send contact card |
|
||||
|
||||
| Tool | Description |
|
||||
| --------------- | ------------------- |
|
||||
| `sendPhoto` | Send image |
|
||||
| `sendVideo` | Send video |
|
||||
| `sendDocument` | Send file |
|
||||
| `sendVoice` | Send voice message |
|
||||
| `sendAudio` | Send audio file |
|
||||
| `sendSticker` | Send sticker |
|
||||
| `sendGif` | Send animation |
|
||||
| `sendLocation` | Send location |
|
||||
| `sendContact` | Send contact card |
|
||||
| `downloadMedia` | Download media file |
|
||||
|
||||
#### Contacts (8 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `addContact` | Add new contact |
|
||||
| `deleteContact` | Remove contact |
|
||||
| `getContacts` | Get contact list |
|
||||
| `searchContacts` | Search contacts |
|
||||
| `importContacts` | Bulk import |
|
||||
|
||||
| Tool | Description |
|
||||
| ---------------- | ------------------- |
|
||||
| `addContact` | Add new contact |
|
||||
| `deleteContact` | Remove contact |
|
||||
| `getContacts` | Get contact list |
|
||||
| `searchContacts` | Search contacts |
|
||||
| `importContacts` | Bulk import |
|
||||
| `exportContacts` | Export contact list |
|
||||
| `blockUser` | Block user |
|
||||
| `unblockUser` | Unblock user |
|
||||
| `blockUser` | Block user |
|
||||
| `unblockUser` | Unblock user |
|
||||
|
||||
#### Groups & Channels (15 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `getAdmins` | Get admin list |
|
||||
| `getMembers` | Get member list |
|
||||
| `addMember` | Add user to group |
|
||||
| `removeMember` | Remove from group |
|
||||
| `banUser` | Ban user |
|
||||
| `unbanUser` | Unban user |
|
||||
| `promoteAdmin` | Promote to admin |
|
||||
| `demoteAdmin` | Remove admin rights |
|
||||
| `setGroupTitle` | Change group name |
|
||||
| `setGroupPhoto` | Change group photo |
|
||||
| `setGroupDescription` | Change description |
|
||||
|
||||
| Tool | Description |
|
||||
| ------------------------ | ------------------- |
|
||||
| `getAdmins` | Get admin list |
|
||||
| `getMembers` | Get member list |
|
||||
| `addMember` | Add user to group |
|
||||
| `removeMember` | Remove from group |
|
||||
| `banUser` | Ban user |
|
||||
| `unbanUser` | Unban user |
|
||||
| `promoteAdmin` | Promote to admin |
|
||||
| `demoteAdmin` | Remove admin rights |
|
||||
| `setGroupTitle` | Change group name |
|
||||
| `setGroupPhoto` | Change group photo |
|
||||
| `setGroupDescription` | Change description |
|
||||
| `subscribePublicChannel` | Join public channel |
|
||||
| `inviteToChannel` | Invite user |
|
||||
| `getInviteLink` | Get invite link |
|
||||
| `revokeInviteLink` | Revoke invite link |
|
||||
| `inviteToChannel` | Invite user |
|
||||
| `getInviteLink` | Get invite link |
|
||||
| `revokeInviteLink` | Revoke invite link |
|
||||
|
||||
#### Polls & Interactive (5 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `createPoll` | Create poll |
|
||||
| `votePoll` | Vote on poll |
|
||||
| `stopPoll` | Close poll |
|
||||
|
||||
| Tool | Description |
|
||||
| ------------------- | ------------------- |
|
||||
| `createPoll` | Create poll |
|
||||
| `votePoll` | Vote on poll |
|
||||
| `stopPoll` | Close poll |
|
||||
| `pressInlineButton` | Click inline button |
|
||||
| `answerCallback` | Respond to callback |
|
||||
| `answerCallback` | Respond to callback |
|
||||
|
||||
#### Drafts (3 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `saveDraft` | Save draft message |
|
||||
| `getDrafts` | Get all drafts |
|
||||
| `deleteDraft` | Delete draft |
|
||||
|
||||
| Tool | Description |
|
||||
| ------------- | ------------------ |
|
||||
| `saveDraft` | Save draft message |
|
||||
| `getDrafts` | Get all drafts |
|
||||
| `deleteDraft` | Delete draft |
|
||||
|
||||
#### Privacy & Settings (5 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `getBlockedUsers` | Get blocked list |
|
||||
|
||||
| Tool | Description |
|
||||
| -------------------- | ------------------ |
|
||||
| `getBlockedUsers` | Get blocked list |
|
||||
| `getPrivacySettings` | Get privacy config |
|
||||
| `updatePrivacy` | Update privacy |
|
||||
| `get2FAStatus` | Check 2FA status |
|
||||
| `getActiveSessions` | Get login sessions |
|
||||
| `updatePrivacy` | Update privacy |
|
||||
| `get2FAStatus` | Check 2FA status |
|
||||
| `getActiveSessions` | Get login sessions |
|
||||
|
||||
#### Misc (3 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `resolveUsername` | Resolve @username |
|
||||
| `checkUsername` | Check availability |
|
||||
| `getWebPage` | Get link preview |
|
||||
|
||||
| Tool | Description |
|
||||
| ----------------- | ------------------ |
|
||||
| `resolveUsername` | Resolve @username |
|
||||
| `checkUsername` | Check availability |
|
||||
| `getWebPage` | Get link preview |
|
||||
|
||||
## Tool Implementation Example
|
||||
|
||||
@@ -289,20 +297,11 @@ export const sendMessage: TelegramMCPToolHandler = {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
chatId: {
|
||||
type: 'string',
|
||||
description: 'Chat ID to send message to'
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Message text'
|
||||
},
|
||||
replyToMsgId: {
|
||||
type: 'string',
|
||||
description: 'Optional message ID to reply to'
|
||||
}
|
||||
chatId: { type: 'string', description: 'Chat ID to send message to' },
|
||||
text: { type: 'string', description: 'Message text' },
|
||||
replyToMsgId: { type: 'string', description: 'Optional message ID to reply to' },
|
||||
},
|
||||
required: ['chatId', 'text']
|
||||
required: ['chatId', 'text'],
|
||||
},
|
||||
|
||||
call: async (args, { telegramClient, userId }) => {
|
||||
@@ -318,23 +317,14 @@ export const sendMessage: TelegramMCPToolHandler = {
|
||||
|
||||
const result = await telegramClient.sendMessage(peer, {
|
||||
message: text,
|
||||
replyTo: replyToMsgId ? parseInt(replyToMsgId) : undefined
|
||||
replyTo: replyToMsgId ? parseInt(replyToMsgId) : undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
messageId: result.id.toString(),
|
||||
date: result.date
|
||||
}
|
||||
};
|
||||
return { success: true, data: { messageId: result.id.toString(), date: result.date } };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -351,7 +341,7 @@ useEffect(() => {
|
||||
mcpServer.initialize();
|
||||
|
||||
// Listen for tool execution requests
|
||||
mcpServer.onRequest(async (request) => {
|
||||
mcpServer.onRequest(async request => {
|
||||
if (request.method === 'tools/call') {
|
||||
const { name, arguments: args } = request.params;
|
||||
return mcpServer.executeTool(name, args);
|
||||
@@ -367,6 +357,7 @@ useEffect(() => {
|
||||
## JSON-RPC Protocol
|
||||
|
||||
### Request Format
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
@@ -374,39 +365,28 @@ useEffect(() => {
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "sendMessage",
|
||||
"arguments": {
|
||||
"chatId": "123456789",
|
||||
"text": "Hello from AI!"
|
||||
}
|
||||
"arguments": { "chatId": "123456789", "text": "Hello from AI!" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response Format (Success)
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"success": true,
|
||||
"data": {
|
||||
"messageId": "12345",
|
||||
"date": 1706540000
|
||||
}
|
||||
}
|
||||
"result": { "success": true, "data": { "messageId": "12345", "date": 1706540000 } }
|
||||
}
|
||||
```
|
||||
|
||||
### Response Format (Error)
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": -32000,
|
||||
"message": "Chat not found",
|
||||
"data": { "chatId": "invalid" }
|
||||
}
|
||||
"error": { "code": -32000, "message": "Chat not found", "data": { "chatId": "invalid" } }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -434,4 +414,4 @@ const idString = chatId.toString();
|
||||
|
||||
---
|
||||
|
||||
*Previous: [Services Layer](./03-services.md) | Next: [Pages & Routing](./05-pages-routing.md)*
|
||||
_Previous: [Services Layer](./03-services.md) | Next: [Pages & Routing](./05-pages-routing.md)_
|
||||
|
||||
@@ -301,7 +301,7 @@ The settings modal overlays existing content using URL-based routing.
|
||||
```typescript
|
||||
// In SettingsModal.tsx
|
||||
const location = useLocation();
|
||||
const isOpen = location.pathname.startsWith("/settings");
|
||||
const isOpen = location.pathname.startsWith('/settings');
|
||||
```
|
||||
|
||||
### Sub-Routes
|
||||
@@ -342,7 +342,7 @@ The app uses HashRouter for desktop compatibility:
|
||||
|
||||
```typescript
|
||||
// App.tsx
|
||||
import { HashRouter } from "react-router-dom";
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
// URLs look like: app://localhost/#/home
|
||||
// Instead of: app://localhost/home
|
||||
@@ -361,7 +361,7 @@ Deep links are handled before routing:
|
||||
|
||||
```typescript
|
||||
// main.tsx
|
||||
import("./utils/desktopDeepLinkListener").then((m) => {
|
||||
import('./utils/desktopDeepLinkListener').then(m => {
|
||||
m.setupDesktopDeepLinkListener().catch(console.error);
|
||||
});
|
||||
```
|
||||
@@ -377,15 +377,15 @@ The listener intercepts `alphahuman://auth?token=...` and:
|
||||
### Programmatic Navigation
|
||||
|
||||
```typescript
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Navigate to route
|
||||
navigate("/home");
|
||||
navigate('/home');
|
||||
|
||||
// Replace history entry
|
||||
navigate("/login", { replace: true });
|
||||
navigate('/login', { replace: true });
|
||||
|
||||
// Go back
|
||||
navigate(-1);
|
||||
@@ -403,7 +403,7 @@ import { Link } from "react-router-dom";
|
||||
|
||||
```typescript
|
||||
// Pass state to route
|
||||
navigate("/details", { state: { itemId: 123 } });
|
||||
navigate('/details', { state: { itemId: 123 } });
|
||||
|
||||
// Receive state
|
||||
const location = useLocation();
|
||||
|
||||
@@ -150,6 +150,7 @@ const [showModal, setShowModal] = useState(false);
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- QR code login flow
|
||||
- Phone number login flow
|
||||
- Connection status display
|
||||
@@ -204,6 +205,7 @@ import welcomeAnimation from '../assets/animations/welcome.json';
|
||||
Complete modal system with URL-based routing.
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
components/settings/
|
||||
├── SettingsModal.tsx # Route-based container
|
||||
@@ -467,15 +469,15 @@ export const connectOptions = [
|
||||
id: 'telegram',
|
||||
label: 'Telegram',
|
||||
icon: TelegramIcon,
|
||||
description: 'Connect your Telegram account'
|
||||
description: 'Connect your Telegram account',
|
||||
},
|
||||
{
|
||||
id: 'gmail',
|
||||
label: 'Gmail',
|
||||
icon: GmailIcon,
|
||||
description: 'Connect your Gmail account',
|
||||
comingSoon: true
|
||||
}
|
||||
comingSoon: true,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
@@ -508,4 +510,4 @@ const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
---
|
||||
|
||||
*Previous: [Pages & Routing](./05-pages-routing.md) | Next: [Providers](./07-providers.md)*
|
||||
_Previous: [Pages & Routing](./05-pages-routing.md) | Next: [Providers](./07-providers.md)_
|
||||
|
||||
@@ -24,6 +24,7 @@ The providers wrap the application in a specific order:
|
||||
```
|
||||
|
||||
**Order matters because:**
|
||||
|
||||
1. Redux must be outermost for state access
|
||||
2. PersistGate rehydrates state before rendering children
|
||||
3. SocketProvider depends on Redux auth token
|
||||
@@ -35,6 +36,7 @@ The providers wrap the application in a specific order:
|
||||
Manages Socket.io connection lifecycle and MCP initialization.
|
||||
|
||||
### Responsibilities
|
||||
|
||||
- Auto-connect when auth token is available
|
||||
- Auto-disconnect when token is cleared
|
||||
- Initialize MCP server when socket connects
|
||||
@@ -140,6 +142,7 @@ function MyComponent() {
|
||||
Manages Telegram MTProto connection lifecycle.
|
||||
|
||||
### Responsibilities
|
||||
|
||||
- Initialize MTProto client when user is authenticated
|
||||
- Connect to Telegram servers
|
||||
- Store session string in Redux
|
||||
@@ -250,6 +253,7 @@ function ChatList() {
|
||||
Minimal user context provider (most user state is in Redux).
|
||||
|
||||
### Responsibilities
|
||||
|
||||
- Legacy user context for compatibility
|
||||
- May be deprecated in favor of Redux
|
||||
|
||||
@@ -312,7 +316,7 @@ Providers read from and dispatch to Redux:
|
||||
|
||||
```typescript
|
||||
// Read state
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const token = useAppSelector(state => state.auth.token);
|
||||
|
||||
// Dispatch actions
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -326,7 +330,7 @@ TelegramProvider runs init and connect in parallel:
|
||||
```typescript
|
||||
await Promise.all([
|
||||
dispatch(initializeTelegram(userId)).unwrap(),
|
||||
dispatch(connectTelegram(userId)).unwrap()
|
||||
dispatch(connectTelegram(userId)).unwrap(),
|
||||
]);
|
||||
```
|
||||
|
||||
@@ -346,13 +350,14 @@ useEffect(() => {
|
||||
|
||||
## Context vs Redux
|
||||
|
||||
| Use Context For | Use Redux For |
|
||||
|-----------------|---------------|
|
||||
| Service instances (socket, client) | Serializable state (status, data) |
|
||||
| Methods (emit, on, off) | Persisted state (sessions, tokens) |
|
||||
| Derived values | Complex state logic |
|
||||
| Use Context For | Use Redux For |
|
||||
| ---------------------------------- | ---------------------------------- |
|
||||
| Service instances (socket, client) | Serializable state (status, data) |
|
||||
| Methods (emit, on, off) | Persisted state (sessions, tokens) |
|
||||
| Derived values | Complex state logic |
|
||||
|
||||
Example:
|
||||
|
||||
- `SocketContext` provides `socket` instance and `emit` method
|
||||
- Redux stores `socketStatus` and `socketId`
|
||||
|
||||
@@ -401,4 +406,4 @@ test('SocketProvider connects when token is available', () => {
|
||||
|
||||
---
|
||||
|
||||
*Previous: [Components](./06-components.md) | Next: [Hooks & Utils](./08-hooks-utils.md)*
|
||||
_Previous: [Components](./06-components.md) | Next: [Hooks & Utils](./08-hooks-utils.md)_
|
||||
|
||||
+27
-36
@@ -171,8 +171,7 @@ Environment variable access with defaults.
|
||||
|
||||
```typescript
|
||||
// Backend URL
|
||||
export const BACKEND_URL =
|
||||
import.meta.env.VITE_BACKEND_URL || "https://api.example.com";
|
||||
export const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.example.com';
|
||||
|
||||
// Telegram configuration
|
||||
export const TELEGRAM_API_ID = import.meta.env.VITE_TELEGRAM_API_ID;
|
||||
@@ -181,18 +180,18 @@ export const TELEGRAM_BOT_USERNAME = import.meta.env.VITE_TELEGRAM_BOT_USERNAME;
|
||||
export const TELEGRAM_BOT_ID = import.meta.env.VITE_TELEGRAM_BOT_ID;
|
||||
|
||||
// Debug mode
|
||||
export const DEBUG = import.meta.env.VITE_DEBUG === "true";
|
||||
export const DEBUG = import.meta.env.VITE_DEBUG === 'true';
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { BACKEND_URL, DEBUG } from "../utils/config";
|
||||
import { BACKEND_URL, DEBUG } from '../utils/config';
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/users`);
|
||||
|
||||
if (DEBUG) {
|
||||
console.log("Response:", response);
|
||||
console.log('Response:', response);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -211,7 +210,7 @@ function parseDeepLink(url: string): { path: string; params: URLSearchParams };
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { buildAuthDeepLink } from "../utils/deeplink";
|
||||
import { buildAuthDeepLink } from '../utils/deeplink';
|
||||
|
||||
// Build URL for browser redirect
|
||||
const deepLink = buildAuthDeepLink(loginToken);
|
||||
@@ -234,7 +233,7 @@ async function setupDesktopDeepLinkListener(): Promise<void>;
|
||||
|
||||
```typescript
|
||||
// Lazy import to ensure Tauri IPC is ready
|
||||
import("./utils/desktopDeepLinkListener").then((m) => {
|
||||
import('./utils/desktopDeepLinkListener').then(m => {
|
||||
m.setupDesktopDeepLinkListener().catch(console.error);
|
||||
});
|
||||
```
|
||||
@@ -251,12 +250,12 @@ import("./utils/desktopDeepLinkListener").then((m) => {
|
||||
|
||||
```typescript
|
||||
// Set flag before navigation to prevent reprocessing
|
||||
localStorage.setItem("deepLinkHandled", "true");
|
||||
window.location.replace("/");
|
||||
localStorage.setItem('deepLinkHandled', 'true');
|
||||
window.location.replace('/');
|
||||
|
||||
// On next load, clear flag
|
||||
if (localStorage.getItem("deepLinkHandled") === "true") {
|
||||
localStorage.removeItem("deepLinkHandled");
|
||||
if (localStorage.getItem('deepLinkHandled') === 'true') {
|
||||
localStorage.removeItem('deepLinkHandled');
|
||||
return; // Don't process again
|
||||
}
|
||||
```
|
||||
@@ -273,10 +272,10 @@ async function openUrl(url: string): Promise<void>;
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { openUrl } from "../utils/openUrl";
|
||||
import { openUrl } from '../utils/openUrl';
|
||||
|
||||
// Opens in system browser (not in-app WebView)
|
||||
await openUrl("https://telegram.org/auth");
|
||||
await openUrl('https://telegram.org/auth');
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
@@ -285,11 +284,11 @@ await openUrl("https://telegram.org/auth");
|
||||
export async function openUrl(url: string): Promise<void> {
|
||||
try {
|
||||
// Try Tauri opener plugin first
|
||||
const { open } = await import("@tauri-apps/plugin-opener");
|
||||
const { open } = await import('@tauri-apps/plugin-opener');
|
||||
await open(url);
|
||||
} catch {
|
||||
// Fallback to browser API
|
||||
window.open(url, "_blank");
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -302,9 +301,9 @@ The `telegram` npm package requires Node.js APIs. These are polyfilled:
|
||||
|
||||
```typescript
|
||||
// polyfills.ts
|
||||
import { Buffer } from "buffer";
|
||||
import process from "process";
|
||||
import util from "util";
|
||||
import { Buffer } from 'buffer';
|
||||
import process from 'process';
|
||||
import util from 'util';
|
||||
|
||||
window.Buffer = Buffer;
|
||||
window.process = process;
|
||||
@@ -315,7 +314,8 @@ window.util = util;
|
||||
|
||||
```typescript
|
||||
// main.tsx
|
||||
import "./polyfills";
|
||||
import './polyfills';
|
||||
|
||||
// ... rest of app
|
||||
```
|
||||
|
||||
@@ -324,17 +324,8 @@ import "./polyfills";
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
buffer: "buffer",
|
||||
process: "process/browser",
|
||||
util: "util",
|
||||
},
|
||||
},
|
||||
define: {
|
||||
"process.env": {},
|
||||
global: "globalThis",
|
||||
},
|
||||
resolve: { alias: { buffer: 'buffer', process: 'process/browser', util: 'util' } },
|
||||
define: { 'process.env': {}, global: 'globalThis' },
|
||||
});
|
||||
```
|
||||
|
||||
@@ -453,14 +444,14 @@ Always include dependencies in useEffect:
|
||||
```typescript
|
||||
// Good
|
||||
useEffect(() => {
|
||||
on("event", handler);
|
||||
return () => off("event", handler);
|
||||
on('event', handler);
|
||||
return () => off('event', handler);
|
||||
}, [on, off, handler]);
|
||||
|
||||
// Bad - missing dependencies
|
||||
useEffect(() => {
|
||||
on("event", handler);
|
||||
return () => off("event", handler);
|
||||
on('event', handler);
|
||||
return () => off('event', handler);
|
||||
}, []);
|
||||
```
|
||||
|
||||
@@ -483,7 +474,7 @@ Wrap utility calls in try-catch:
|
||||
try {
|
||||
await openUrl(url);
|
||||
} catch (error) {
|
||||
console.error("Failed to open URL:", error);
|
||||
console.error('Failed to open URL:', error);
|
||||
// Fallback behavior
|
||||
}
|
||||
```
|
||||
@@ -493,7 +484,7 @@ try {
|
||||
Use TypeScript generics for API calls:
|
||||
|
||||
```typescript
|
||||
const user = await apiClient.get<User>("/users/me");
|
||||
const user = await apiClient.get<User>('/users/me');
|
||||
// user is typed as User
|
||||
```
|
||||
|
||||
|
||||
+27
-27
@@ -4,35 +4,35 @@ This documentation covers the `/src` folder structure of the Outsourced Crypto C
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [Architecture Overview](./01-architecture.md) | High-level system architecture and provider chain |
|
||||
| [State Management](./02-state-management.md) | Redux Toolkit slices, persistence, and selectors |
|
||||
| [Services Layer](./03-services.md) | Singleton services (Socket.io, MTProto, API client) |
|
||||
| [MCP System](./04-mcp-system.md) | Model Context Protocol with 81 Telegram tools |
|
||||
| [Pages & Routing](./05-pages-routing.md) | Route definitions, guards, and page components |
|
||||
| [Components](./06-components.md) | Reusable UI components and settings modal system |
|
||||
| [Providers](./07-providers.md) | React context providers and lifecycle management |
|
||||
| [Hooks & Utils](./08-hooks-utils.md) | Custom hooks and utility functions |
|
||||
| Document | Description |
|
||||
| --------------------------------------------- | --------------------------------------------------- |
|
||||
| [Architecture Overview](./01-architecture.md) | High-level system architecture and provider chain |
|
||||
| [State Management](./02-state-management.md) | Redux Toolkit slices, persistence, and selectors |
|
||||
| [Services Layer](./03-services.md) | Singleton services (Socket.io, MTProto, API client) |
|
||||
| [MCP System](./04-mcp-system.md) | Model Context Protocol with 81 Telegram tools |
|
||||
| [Pages & Routing](./05-pages-routing.md) | Route definitions, guards, and page components |
|
||||
| [Components](./06-components.md) | Reusable UI components and settings modal system |
|
||||
| [Providers](./07-providers.md) | React context providers and lifecycle management |
|
||||
| [Hooks & Utils](./08-hooks-utils.md) | Custom hooks and utility functions |
|
||||
|
||||
## File Count Summary
|
||||
|
||||
| Category | Files | Purpose |
|
||||
|----------|-------|---------|
|
||||
| Entry & Configuration | 7 | App init, routing, styles, types |
|
||||
| State Management | 13 | Redux slices, selectors, hooks |
|
||||
| Providers | 3 | Socket, Telegram, User contexts |
|
||||
| Services | 5 | Singleton API clients |
|
||||
| Pages | 9 | Full-page route components |
|
||||
| Components | 28 | Reusable UI + settings modal (16 files) |
|
||||
| MCP Core | 14 | MCP interfaces, transport, logging |
|
||||
| MCP Telegram Tools | 81 | Individual Telegram API operations |
|
||||
| Hooks | 2 | Custom React hooks |
|
||||
| Types | 2 | TypeScript interfaces |
|
||||
| Utils | 4 | Config, deep link, URL utilities |
|
||||
| Data | 1 | Static data (countries) |
|
||||
| Assets | 10+ | Icons and images |
|
||||
| **TOTAL** | **171+** | Complete frontend application |
|
||||
| Category | Files | Purpose |
|
||||
| --------------------- | -------- | --------------------------------------- |
|
||||
| Entry & Configuration | 7 | App init, routing, styles, types |
|
||||
| State Management | 13 | Redux slices, selectors, hooks |
|
||||
| Providers | 3 | Socket, Telegram, User contexts |
|
||||
| Services | 5 | Singleton API clients |
|
||||
| Pages | 9 | Full-page route components |
|
||||
| Components | 28 | Reusable UI + settings modal (16 files) |
|
||||
| MCP Core | 14 | MCP interfaces, transport, logging |
|
||||
| MCP Telegram Tools | 81 | Individual Telegram API operations |
|
||||
| Hooks | 2 | Custom React hooks |
|
||||
| Types | 2 | TypeScript interfaces |
|
||||
| Utils | 4 | Config, deep link, URL utilities |
|
||||
| Data | 1 | Static data (countries) |
|
||||
| Assets | 10+ | Icons and images |
|
||||
| **TOTAL** | **171+** | Complete frontend application |
|
||||
|
||||
## Directory Structure
|
||||
|
||||
@@ -75,4 +75,4 @@ src/
|
||||
|
||||
---
|
||||
|
||||
*Documentation maintained by stevenbaba*
|
||||
_Documentation maintained by stevenbaba_
|
||||
|
||||
@@ -33,7 +33,7 @@ The backend base URL is configured here (`src/utils/config.ts`):
|
||||
|
||||
```ts
|
||||
export const BACKEND_URL =
|
||||
import.meta.env.VITE_BACKEND_URL || "https://2937933edf8a.ngrok-free.app";
|
||||
import.meta.env.VITE_BACKEND_URL || 'https://2937933edf8a.ngrok-free.app';
|
||||
```
|
||||
|
||||
### 2) Deep link registration (Tauri config + Rust)
|
||||
@@ -41,15 +41,7 @@ export const BACKEND_URL =
|
||||
The URL scheme is declared in `src-tauri/tauri.conf.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": ["alphahuman"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "plugins": { "deep-link": { "desktop": { "schemes": ["alphahuman"] } } } }
|
||||
```
|
||||
|
||||
The deep-link plugin is initialized in `src-tauri/src/lib.rs`:
|
||||
@@ -77,9 +69,9 @@ pub fn run() {
|
||||
The listener is lazy-loaded in `src/main.tsx`:
|
||||
|
||||
```ts
|
||||
import("./utils/desktopDeepLinkListener").then((m) => {
|
||||
m.setupDesktopDeepLinkListener().catch((err) => {
|
||||
console.error("[DeepLink] setup error:", err);
|
||||
import('./utils/desktopDeepLinkListener').then(m => {
|
||||
m.setupDesktopDeepLinkListener().catch(err => {
|
||||
console.error('[DeepLink] setup error:', err);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
+27
-10
@@ -1,15 +1,20 @@
|
||||
// ESLint flat config for ESLint 9+
|
||||
// This config is compatible with Prettier and won't conflict with formatting rules
|
||||
|
||||
const js = require('@eslint/js');
|
||||
const tseslint = require('@typescript-eslint/eslint-plugin');
|
||||
const tsparser = require('@typescript-eslint/parser');
|
||||
const reactPlugin = require('eslint-plugin-react');
|
||||
const reactHooksPlugin = require('eslint-plugin-react-hooks');
|
||||
const importPlugin = require('eslint-plugin-import');
|
||||
const prettierConfig = require('eslint-config-prettier');
|
||||
import js from '@eslint/js';
|
||||
import tseslint from '@typescript-eslint/eslint-plugin';
|
||||
import tsparser from '@typescript-eslint/parser';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import prettierConfig from 'eslint-config-prettier';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
module.exports = [
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
export default [
|
||||
// Base recommended rules
|
||||
js.configs.recommended,
|
||||
|
||||
@@ -21,6 +26,9 @@ module.exports = [
|
||||
'coverage/**',
|
||||
'src-tauri/**',
|
||||
'skills/**',
|
||||
'references/**',
|
||||
'deploy/**',
|
||||
'scripts/**',
|
||||
'*.config.js',
|
||||
'*.config.ts',
|
||||
'vitest.config.ts',
|
||||
@@ -44,6 +52,12 @@ module.exports = [
|
||||
clearInterval: 'readonly',
|
||||
fetch: 'readonly',
|
||||
AbortSignal: 'readonly',
|
||||
self: 'readonly',
|
||||
crypto: 'readonly',
|
||||
atob: 'readonly',
|
||||
btoa: 'readonly',
|
||||
// React globals
|
||||
React: 'readonly',
|
||||
// Node.js globals (for Vite/node polyfills)
|
||||
require: 'readonly',
|
||||
process: 'readonly',
|
||||
@@ -59,7 +73,7 @@ module.exports = [
|
||||
|
||||
// TypeScript files configuration
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
files: ['src/**/*.ts', 'src/**/*.tsx'],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: {
|
||||
@@ -123,7 +137,7 @@ module.exports = [
|
||||
|
||||
// React files configuration
|
||||
{
|
||||
files: ['**/*.jsx', '**/*.tsx'],
|
||||
files: ['src/**/*.jsx', 'src/**/*.tsx'],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: {
|
||||
@@ -151,8 +165,11 @@ module.exports = [
|
||||
'react/react-in-jsx-scope': 'off', // Not needed in React 17+
|
||||
'react/prop-types': 'off', // TypeScript handles prop validation
|
||||
'react/display-name': 'off', // Not needed with TypeScript
|
||||
'react/no-unescaped-entities': 'off', // Prettier handles this
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'react-hooks/set-state-in-effect': 'warn', // Allow initialization in effects
|
||||
'react-hooks/refs': 'off', // Allow ref access in context providers
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
+15
-2
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "tauri-app",
|
||||
"name": "alphahuman",
|
||||
"private": true,
|
||||
"version": "0.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"compile": "tsc --noEmit",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"macos:build": "tauri build --debug --bundles app",
|
||||
@@ -18,7 +19,8 @@
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix"
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
@@ -47,18 +49,29 @@
|
||||
"zustand": "^5.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/redux-logger": "^3.0.13",
|
||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^8.54.0",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.8.1",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4",
|
||||
|
||||
+14814
-1
File diff suppressed because one or more lines are too long
+195606
-1
File diff suppressed because one or more lines are too long
+27857
-1
File diff suppressed because one or more lines are too long
+322
-1
@@ -1 +1,322 @@
|
||||
{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":77,"w":500,"h":500,"nm":"security tick","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"tick","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.094,0.094,0.094],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.067,0.067,-0.067]},"t":32,"s":[62,62,100]},{"t":56,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-52.947,0],[-17.649,35.298],[52.947,-35.298]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25.537,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.178],"y":[1]},"o":{"x":[0.21],"y":[0]},"t":32,"s":[0]},{"t":62,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"shield","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":4,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.97],"y":[1]},"o":{"x":[0.03],"y":[0]},"t":0,"s":[250]},{"i":{"x":[0.97],"y":[1]},"o":{"x":[0.03],"y":[0]},"t":20,"s":[250]},{"t":40,"s":[250]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.174],"y":[0.822]},"t":0,"s":[319]},{"i":{"x":[0.334],"y":[1]},"o":{"x":[0.308],"y":[0]},"t":20,"s":[197]},{"t":40,"s":[250]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.174,0.174,0.174],"y":[0.822,0.822,-0.822]},"t":0,"s":[46,46,100]},{"t":20,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.094,"y":1},"o":{"x":0.252,"y":0},"t":25,"s":[{"i":[[-29.271,0],[0,-29.271],[29.271,0],[0,29.271]],"o":[[29.271,0],[0,29.271],[-29.271,0],[0,-29.271]],"v":[[0,-53],[53,0],[0,53],[-53,0]],"c":true}]},{"t":56,"s":[{"i":[[-2.504,1.787],[-85.122,-7.093],[120.419,-40.532],[0,158.319]],"o":[[2.504,1.787],[0,158.319],[-120.419,-40.532],[85.122,-7.093]],"v":[[0,-169.229],[141.87,-126.668],[0,169.229],[-141.87,-126.668]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.360784322023,0.800000011921,0.474509805441,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0}],"markers":[]}
|
||||
{
|
||||
"v": "4.8.0",
|
||||
"meta": { "g": "LottieFiles AE 3.0.2", "a": "", "k": "", "d": "", "tc": "" },
|
||||
"fr": 60,
|
||||
"ip": 0,
|
||||
"op": 77,
|
||||
"w": 500,
|
||||
"h": 500,
|
||||
"nm": "security tick",
|
||||
"ddd": 0,
|
||||
"assets": [],
|
||||
"layers": [
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 1,
|
||||
"ty": 4,
|
||||
"nm": "tick",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||
"p": { "a": 0, "k": [250, 250, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0, 0], "ix": 1 },
|
||||
"s": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": [0.094, 0.094, 0.094], "y": [1, 1, 1] },
|
||||
"o": { "x": [0.167, 0.167, 0.167], "y": [0.067, 0.067, -0.067] },
|
||||
"t": 32,
|
||||
"s": [62, 62, 100]
|
||||
},
|
||||
{ "t": 56, "s": [100, 100, 100] }
|
||||
],
|
||||
"ix": 6
|
||||
}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[-52.947, 0],
|
||||
[-17.649, 35.298],
|
||||
[52.947, -35.298]
|
||||
],
|
||||
"c": false
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": { "a": 0, "k": [1, 1, 1, 1], "ix": 3 },
|
||||
"o": { "a": 0, "k": 100, "ix": 4 },
|
||||
"w": { "a": 0, "k": 25.537, "ix": 5 },
|
||||
"lc": 2,
|
||||
"lj": 2,
|
||||
"bm": 0,
|
||||
"nm": "Stroke 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tm",
|
||||
"s": { "a": 0, "k": 0, "ix": 1 },
|
||||
"e": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": [0.178], "y": [1] },
|
||||
"o": { "x": [0.21], "y": [0] },
|
||||
"t": 32,
|
||||
"s": [0]
|
||||
},
|
||||
{ "t": 62, "s": [100] }
|
||||
],
|
||||
"ix": 2
|
||||
},
|
||||
"o": { "a": 0, "k": 0, "ix": 3 },
|
||||
"m": 1,
|
||||
"ix": 2,
|
||||
"nm": "Trim Paths 1",
|
||||
"mn": "ADBE Vector Filter - Trim",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 300,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 2,
|
||||
"ty": 4,
|
||||
"nm": "shield",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": [0.833], "y": [0.833] },
|
||||
"o": { "x": [0.167], "y": [0.167] },
|
||||
"t": 0,
|
||||
"s": [0]
|
||||
},
|
||||
{ "t": 4, "s": [100] }
|
||||
],
|
||||
"ix": 11
|
||||
},
|
||||
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||
"p": {
|
||||
"s": true,
|
||||
"x": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": [0.97], "y": [1] },
|
||||
"o": { "x": [0.03], "y": [0] },
|
||||
"t": 0,
|
||||
"s": [250]
|
||||
},
|
||||
{
|
||||
"i": { "x": [0.97], "y": [1] },
|
||||
"o": { "x": [0.03], "y": [0] },
|
||||
"t": 20,
|
||||
"s": [250]
|
||||
},
|
||||
{ "t": 40, "s": [250] }
|
||||
],
|
||||
"ix": 3
|
||||
},
|
||||
"y": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": [0.667], "y": [1] },
|
||||
"o": { "x": [0.174], "y": [0.822] },
|
||||
"t": 0,
|
||||
"s": [319]
|
||||
},
|
||||
{
|
||||
"i": { "x": [0.334], "y": [1] },
|
||||
"o": { "x": [0.308], "y": [0] },
|
||||
"t": 20,
|
||||
"s": [197]
|
||||
},
|
||||
{ "t": 40, "s": [250] }
|
||||
],
|
||||
"ix": 4
|
||||
}
|
||||
},
|
||||
"a": { "a": 0, "k": [0, 0, 0], "ix": 1 },
|
||||
"s": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": [0.667, 0.667, 0.667], "y": [1, 1, 1] },
|
||||
"o": { "x": [0.174, 0.174, 0.174], "y": [0.822, 0.822, -0.822] },
|
||||
"t": 0,
|
||||
"s": [46, 46, 100]
|
||||
},
|
||||
{ "t": 20, "s": [100, 100, 100] }
|
||||
],
|
||||
"ix": 6
|
||||
}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0.094, "y": 1 },
|
||||
"o": { "x": 0.252, "y": 0 },
|
||||
"t": 25,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[-29.271, 0],
|
||||
[0, -29.271],
|
||||
[29.271, 0],
|
||||
[0, 29.271]
|
||||
],
|
||||
"o": [
|
||||
[29.271, 0],
|
||||
[0, 29.271],
|
||||
[-29.271, 0],
|
||||
[0, -29.271]
|
||||
],
|
||||
"v": [
|
||||
[0, -53],
|
||||
[53, 0],
|
||||
[0, 53],
|
||||
[-53, 0]
|
||||
],
|
||||
"c": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 56,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[-2.504, 1.787],
|
||||
[-85.122, -7.093],
|
||||
[120.419, -40.532],
|
||||
[0, 158.319]
|
||||
],
|
||||
"o": [
|
||||
[2.504, 1.787],
|
||||
[0, 158.319],
|
||||
[-120.419, -40.532],
|
||||
[85.122, -7.093]
|
||||
],
|
||||
"v": [
|
||||
[0, -169.229],
|
||||
[141.87, -126.668],
|
||||
[0, 169.229],
|
||||
[-141.87, -126.668]
|
||||
],
|
||||
"c": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": { "a": 0, "k": [0.360784322023, 0.800000011921, 0.474509805441, 1], "ix": 4 },
|
||||
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||
"r": 1,
|
||||
"bm": 0,
|
||||
"nm": "Fill 1",
|
||||
"mn": "ADBE Vector Graphic - Fill",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 300,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
}
|
||||
],
|
||||
"markers": []
|
||||
}
|
||||
|
||||
+6050
-1
File diff suppressed because one or more lines are too long
+15906
-1
File diff suppressed because one or more lines are too long
+9431
-1
File diff suppressed because one or more lines are too long
+438
-1
File diff suppressed because one or more lines are too long
@@ -1,139 +0,0 @@
|
||||
{
|
||||
"generatedAt": "2026-01-31T22:33:58.672153+00:00",
|
||||
"version": "1.0.0",
|
||||
"skills": [
|
||||
{
|
||||
"name": "email",
|
||||
"description": "Email integration via IMAP/SMTP \u2014 35 tools for reading, sending, searching, and managing email across Gmail, Outlook, Yahoo, iCloud, and custom servers.",
|
||||
"icon": null,
|
||||
"version": "1.0.0",
|
||||
"tools": [],
|
||||
"hooks": [
|
||||
"on_load",
|
||||
"on_unload",
|
||||
"on_tick",
|
||||
"on_setup_start",
|
||||
"on_setup_submit",
|
||||
"on_setup_cancel"
|
||||
],
|
||||
"tickIntervalMinutes": 5.0,
|
||||
"path": "skills/email"
|
||||
},
|
||||
{
|
||||
"name": "github",
|
||||
"description": "GitHub integration via PyGithub \u2014 72 tools for repos, issues, PRs, releases, gists, actions, search, notifications, and raw API access.",
|
||||
"icon": null,
|
||||
"version": "1.0.0",
|
||||
"tools": [],
|
||||
"hooks": [
|
||||
"on_load",
|
||||
"on_unload",
|
||||
"on_tick",
|
||||
"on_setup_start",
|
||||
"on_setup_submit",
|
||||
"on_setup_cancel"
|
||||
],
|
||||
"tickIntervalMinutes": 5.0,
|
||||
"path": "skills/github"
|
||||
},
|
||||
{
|
||||
"name": "notion",
|
||||
"description": "Notion workspace integration \u2014 22 tools for pages, databases, blocks, users, comments, and search.",
|
||||
"icon": null,
|
||||
"version": "1.0.0",
|
||||
"tools": [
|
||||
"notion_search",
|
||||
"notion_get_page",
|
||||
"notion_create_page",
|
||||
"notion_update_page",
|
||||
"notion_delete_page",
|
||||
"notion_query_database",
|
||||
"notion_get_database",
|
||||
"notion_create_database",
|
||||
"notion_update_database",
|
||||
"notion_get_block",
|
||||
"notion_get_block_children",
|
||||
"notion_append_blocks",
|
||||
"notion_update_block",
|
||||
"notion_delete_block",
|
||||
"notion_list_users",
|
||||
"notion_get_user",
|
||||
"notion_create_comment",
|
||||
"notion_list_comments",
|
||||
"notion_get_page_content",
|
||||
"notion_append_text",
|
||||
"notion_list_all_pages",
|
||||
"notion_list_all_databases"
|
||||
],
|
||||
"hooks": [
|
||||
"on_load",
|
||||
"on_unload",
|
||||
"on_tick"
|
||||
],
|
||||
"tickIntervalMinutes": 5.0,
|
||||
"path": "skills/notion"
|
||||
},
|
||||
{
|
||||
"name": "otter",
|
||||
"description": "Otter.ai meeting notes integration \u2014 fetch transcripts, summaries, and search across meetings.",
|
||||
"icon": null,
|
||||
"version": "1.0.0",
|
||||
"tools": [
|
||||
"list_meetings",
|
||||
"get_meeting",
|
||||
"get_meeting_summary",
|
||||
"search_meetings",
|
||||
"search_in_meeting",
|
||||
"download_meeting_transcript",
|
||||
"get_otter_user",
|
||||
"list_speakers"
|
||||
],
|
||||
"hooks": [
|
||||
"on_load",
|
||||
"on_unload",
|
||||
"on_tick"
|
||||
],
|
||||
"tickIntervalMinutes": 5.0,
|
||||
"path": "skills/otter"
|
||||
},
|
||||
{
|
||||
"name": "skill-generator",
|
||||
"description": "Meta-skill that creates, validates, tests, and scans new AlphaHuman skills on-the-fly.",
|
||||
"icon": null,
|
||||
"version": "1.0.0",
|
||||
"tools": [
|
||||
"list_available_skills",
|
||||
"generate_skill",
|
||||
"write_skill_file",
|
||||
"validate_skill",
|
||||
"test_skill",
|
||||
"security_scan_skill"
|
||||
],
|
||||
"hooks": [
|
||||
"on_load",
|
||||
"on_unload",
|
||||
"on_session_start",
|
||||
"on_before_message"
|
||||
],
|
||||
"tickIntervalMinutes": null,
|
||||
"path": "skills/skill-generator"
|
||||
},
|
||||
{
|
||||
"name": "telegram",
|
||||
"description": "Telegram integration via Telethon MTProto \u2014 75+ tools for chats, messages, contacts, admin, media, and settings.",
|
||||
"icon": null,
|
||||
"version": "2.0.0",
|
||||
"tools": [],
|
||||
"hooks": [
|
||||
"on_load",
|
||||
"on_unload",
|
||||
"on_tick",
|
||||
"on_setup_start",
|
||||
"on_setup_submit",
|
||||
"on_setup_cancel"
|
||||
],
|
||||
"tickIntervalMinutes": 20.0,
|
||||
"path": "skills/telegram"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,42 +3,41 @@
|
||||
* Symlink system python3 to src-tauri/runtime-skill-python-<target> for local dev.
|
||||
* Run from project root: node scripts/setup-python-sidecar.mjs
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const binariesDir = path.join(root, "src-tauri");
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const binariesDir = path.join(root, 'src-tauri');
|
||||
|
||||
function getTargetTriple() {
|
||||
try {
|
||||
return execSync("rustc --print host-tuple", { encoding: "utf8" }).trim();
|
||||
return execSync('rustc --print host-tuple', { encoding: 'utf8' }).trim();
|
||||
} catch {
|
||||
const out = execSync("rustc -Vv", { encoding: "utf8" });
|
||||
const out = execSync('rustc -Vv', { encoding: 'utf8' });
|
||||
const m = out.match(/host:\s*(.+)/);
|
||||
if (m) return m[1].trim();
|
||||
throw new Error("Could not get Rust target triple (rustc not found?)");
|
||||
throw new Error('Could not get Rust target triple (rustc not found?)');
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemPython() {
|
||||
const isWin = process.platform === "win32";
|
||||
const isWin = process.platform === 'win32';
|
||||
try {
|
||||
const cmd = isWin ? "where python" : "which python3";
|
||||
const out = execSync(cmd, { encoding: "utf8" }).trim();
|
||||
const cmd = isWin ? 'where python' : 'which python3';
|
||||
const out = execSync(cmd, { encoding: 'utf8' }).trim();
|
||||
const first = out.split(/[\r\n]/)[0].trim();
|
||||
if (first) return first;
|
||||
} catch {}
|
||||
throw new Error(
|
||||
"System Python not found. Install Python 3 and ensure python3 (or python on Windows) is on PATH."
|
||||
'System Python not found. Install Python 3 and ensure python3 (or python on Windows) is on PATH.'
|
||||
);
|
||||
}
|
||||
|
||||
const target = getTargetTriple();
|
||||
const ext = process.platform === "win32" ? ".exe" : "";
|
||||
const ext = process.platform === 'win32' ? '.exe' : '';
|
||||
const sidecarName = `runtime-skill-python-${target}${ext}`;
|
||||
const sidecarPath = path.join(binariesDir, sidecarName);
|
||||
const systemPython = getSystemPython();
|
||||
|
||||
Generated
+46
-2
@@ -1159,6 +1159,17 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.8"
|
||||
@@ -2247,6 +2258,7 @@ checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"libc",
|
||||
"redox_syscall 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2869,7 +2881,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
@@ -3383,6 +3395,15 @@ dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
@@ -3998,7 +4019,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
@@ -4212,6 +4233,17 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -4278,6 +4310,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs 5.0.1",
|
||||
"env_logger",
|
||||
"flate2",
|
||||
"keyring",
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -4287,6 +4320,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-autostart",
|
||||
@@ -5976,6 +6010,16 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
|
||||
@@ -47,6 +47,10 @@ parking_lot = "0.12"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
|
||||
# Archive extraction (for downloading skills repo without git)
|
||||
tar = "0.4"
|
||||
flate2 = "1"
|
||||
|
||||
# AI module dependencies
|
||||
base64 = "0.22"
|
||||
aes-gcm = "0.10"
|
||||
|
||||
@@ -12,7 +12,6 @@ use argon2::{self, Algorithm, Argon2, Params, Version};
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use tauri::AppHandle;
|
||||
|
||||
/// Salt length for Argon2id key derivation
|
||||
const SALT_LENGTH: usize = 16;
|
||||
|
||||
@@ -1,9 +1,111 @@
|
||||
//! Skill data I/O commands.
|
||||
//!
|
||||
//! Provides filesystem operations for skill data directories,
|
||||
//! used by the reverse RPC handler in the frontend.
|
||||
//! catalog reading, and remote repo syncing (without git).
|
||||
|
||||
use base64::Engine;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Default GitHub repo for skills (owner/repo format).
|
||||
const SKILLS_GITHUB_REPO: &str = "alphahumanxyz/skills";
|
||||
/// Default branch to sync from.
|
||||
const SKILLS_GITHUB_BRANCH: &str = "main";
|
||||
/// Minimum interval between remote update checks (24 hours in seconds).
|
||||
const UPDATE_CHECK_INTERVAL_SECS: u64 = 24 * 60 * 60;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sync metadata — persisted at ~/.alphahuman/skills-sync.json
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default, Clone)]
|
||||
struct SyncMeta {
|
||||
/// Full commit SHA of the currently-synced skills.
|
||||
commit_sha: Option<String>,
|
||||
/// Unix epoch seconds of the last remote check.
|
||||
last_checked_at: Option<u64>,
|
||||
/// The repo that was synced (owner/repo).
|
||||
repo: Option<String>,
|
||||
/// The branch that was synced.
|
||||
branch: Option<String>,
|
||||
}
|
||||
|
||||
fn sync_meta_path() -> Result<PathBuf, String> {
|
||||
let data_dir = crate::ai::encryption::get_data_dir()?;
|
||||
Ok(data_dir.join("skills-sync.json"))
|
||||
}
|
||||
|
||||
fn read_sync_meta() -> SyncMeta {
|
||||
let path = match sync_meta_path() {
|
||||
Ok(p) => p,
|
||||
Err(_) => return SyncMeta::default(),
|
||||
};
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
|
||||
Err(_) => SyncMeta::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_sync_meta(meta: &SyncMeta) -> Result<(), String> {
|
||||
let path = sync_meta_path()?;
|
||||
let json = serde_json::to_string_pretty(meta)
|
||||
.map_err(|e| format!("Failed to serialize sync meta: {}", e))?;
|
||||
std::fs::write(&path, json.as_bytes())
|
||||
.map_err(|e| format!("Failed to write sync meta: {}", e))
|
||||
}
|
||||
|
||||
fn now_epoch_secs() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
/// Fetch the latest commit SHA for a branch from the GitHub API.
|
||||
async fn fetch_remote_commit_sha(
|
||||
repo: &str,
|
||||
branch: &str,
|
||||
github_token: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
let url = format!(
|
||||
"https://api.github.com/repos/{}/commits/{}",
|
||||
repo, branch
|
||||
);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let mut request = client
|
||||
.get(&url)
|
||||
.header("User-Agent", "AlphaHuman-Desktop")
|
||||
.header("Accept", "application/vnd.github.sha");
|
||||
|
||||
if let Some(token) = github_token {
|
||||
if !token.is_empty() {
|
||||
request = request.header("Authorization", format!("Bearer {}", token));
|
||||
}
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch latest commit: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!(
|
||||
"GitHub API returned status {} when checking commits",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
// With Accept: application/vnd.github.sha the body is the raw SHA string
|
||||
let sha = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read commit SHA response: {}", e))?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
Ok(sha)
|
||||
}
|
||||
|
||||
/// Absolute path to the skills working directory (cwd for `python -m skills.xxx`).
|
||||
/// In dev: project root's `skills/` (submodule). In prod: `~/.alphahuman/skills/`.
|
||||
@@ -200,3 +302,260 @@ pub async fn skill_list_manifests() -> Result<Vec<serde_json::Value>, String> {
|
||||
|
||||
Ok(manifests)
|
||||
}
|
||||
|
||||
/// Read the skills-catalog.json from the local skills directory.
|
||||
///
|
||||
/// In dev: reads from the submodule at `./skills/skills-catalog.json`.
|
||||
/// In prod: reads from `~/.alphahuman/skills/skills-catalog.json`.
|
||||
#[tauri::command]
|
||||
pub async fn skill_read_catalog() -> Result<serde_json::Value, String> {
|
||||
let skills_cwd = skill_cwd().await?;
|
||||
let catalog_path = PathBuf::from(&skills_cwd).join("skills-catalog.json");
|
||||
|
||||
let content = tokio::fs::read_to_string(&catalog_path)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read skills catalog at {:?}: {}", catalog_path, e))?;
|
||||
|
||||
serde_json::from_str(&content)
|
||||
.map_err(|e| format!("Failed to parse skills catalog: {}", e))
|
||||
}
|
||||
|
||||
/// Download and extract the skills repository from GitHub (no git required).
|
||||
///
|
||||
/// Fetches the repo as a tarball via the GitHub API, extracts it to
|
||||
/// `~/.alphahuman/skills/`. Pass `github_token` for private repositories.
|
||||
/// Optionally override `repo` (default: `alphahumanxyz/skills`) and
|
||||
/// `branch` (default: `main`).
|
||||
///
|
||||
/// After a successful sync, saves the commit SHA and timestamp to
|
||||
/// `~/.alphahuman/skills-sync.json` for future update checks.
|
||||
#[tauri::command]
|
||||
pub async fn skill_sync_repo(
|
||||
repo: Option<String>,
|
||||
branch: Option<String>,
|
||||
github_token: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let repo = repo.unwrap_or_else(|| SKILLS_GITHUB_REPO.to_string());
|
||||
let branch = branch.unwrap_or_else(|| SKILLS_GITHUB_BRANCH.to_string());
|
||||
let token_ref = github_token.as_deref();
|
||||
|
||||
let data_dir = crate::ai::encryption::get_data_dir()?;
|
||||
let skills_dir = data_dir.join("skills");
|
||||
|
||||
// Fetch the latest commit SHA so we can record it after sync
|
||||
let commit_sha = fetch_remote_commit_sha(&repo, &branch, token_ref).await?;
|
||||
|
||||
// Download tarball from GitHub API
|
||||
let url = format!(
|
||||
"https://api.github.com/repos/{}/tarball/{}",
|
||||
repo, branch
|
||||
);
|
||||
|
||||
log::info!("Syncing skills from {} (commit {})", url, &commit_sha[..8.min(commit_sha.len())]);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let mut request = client
|
||||
.get(&url)
|
||||
.header("User-Agent", "AlphaHuman-Desktop")
|
||||
.header("Accept", "application/vnd.github+json");
|
||||
|
||||
if let Some(token) = token_ref {
|
||||
if !token.is_empty() {
|
||||
request = request.header("Authorization", format!("Bearer {}", token));
|
||||
}
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to download skills repo: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!(
|
||||
"GitHub API returned status {} for {}",
|
||||
response.status(),
|
||||
url
|
||||
));
|
||||
}
|
||||
|
||||
let bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read response body: {}", e))?;
|
||||
|
||||
// Extract tarball to a temporary directory first
|
||||
let temp_dir = data_dir.join("skills_sync_tmp");
|
||||
|
||||
// Clean up temp dir if leftover from a previous failed sync
|
||||
let _ = tokio::fs::remove_dir_all(&temp_dir).await;
|
||||
tokio::fs::create_dir_all(&temp_dir)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create temp dir: {}", e))?;
|
||||
|
||||
// Decompress gzip and extract tar archive
|
||||
let gz = flate2::read::GzDecoder::new(&bytes[..]);
|
||||
let mut archive = tar::Archive::new(gz);
|
||||
|
||||
archive
|
||||
.unpack(&temp_dir)
|
||||
.map_err(|e| format!("Failed to extract tarball: {}", e))?;
|
||||
|
||||
// GitHub tarballs contain a single top-level directory like "owner-repo-commitsha/"
|
||||
// Find that directory.
|
||||
let mut entries = tokio::fs::read_dir(&temp_dir)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read temp dir: {}", e))?;
|
||||
|
||||
let extracted_dir = if let Ok(Some(entry)) = entries.next_entry().await {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
path
|
||||
} else {
|
||||
return Err("Extracted tarball did not contain a directory".to_string());
|
||||
}
|
||||
} else {
|
||||
return Err("No directory found in extracted tarball".to_string());
|
||||
};
|
||||
|
||||
// Replace the production skills directory with the extracted content
|
||||
if skills_dir.exists() {
|
||||
tokio::fs::remove_dir_all(&skills_dir)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to remove old skills dir: {}", e))?;
|
||||
}
|
||||
|
||||
tokio::fs::rename(&extracted_dir, &skills_dir)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to move extracted skills: {}", e))?;
|
||||
|
||||
// Clean up temp dir
|
||||
let _ = tokio::fs::remove_dir_all(&temp_dir).await;
|
||||
|
||||
// Persist sync metadata
|
||||
let meta = SyncMeta {
|
||||
commit_sha: Some(commit_sha.clone()),
|
||||
last_checked_at: Some(now_epoch_secs()),
|
||||
repo: Some(repo),
|
||||
branch: Some(branch),
|
||||
};
|
||||
if let Err(e) = write_sync_meta(&meta) {
|
||||
log::warn!("Failed to write sync metadata: {}", e);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Skills synced successfully to {:?} (commit {})",
|
||||
skills_dir,
|
||||
&commit_sha[..8.min(commit_sha.len())]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check whether the local skills directory has a catalog file.
|
||||
/// Returns `true` if `skills-catalog.json` exists locally (no sync needed).
|
||||
#[tauri::command]
|
||||
pub async fn skill_catalog_exists() -> Result<bool, String> {
|
||||
let skills_cwd = skill_cwd().await?;
|
||||
let catalog_path = PathBuf::from(&skills_cwd).join("skills-catalog.json");
|
||||
Ok(catalog_path.exists())
|
||||
}
|
||||
|
||||
/// Check if a skills update is available from GitHub.
|
||||
///
|
||||
/// Respects a 24-hour cooldown — if the last check was less than 24 hours
|
||||
/// ago, returns immediately without hitting the network.
|
||||
///
|
||||
/// Returns a JSON object:
|
||||
/// ```json
|
||||
/// { "needs_update": true, "local_sha": "abc...", "remote_sha": "def..." }
|
||||
/// ```
|
||||
#[tauri::command]
|
||||
pub async fn skill_check_for_updates(
|
||||
repo: Option<String>,
|
||||
branch: Option<String>,
|
||||
github_token: Option<String>,
|
||||
force: Option<bool>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let repo = repo.unwrap_or_else(|| SKILLS_GITHUB_REPO.to_string());
|
||||
let branch = branch.unwrap_or_else(|| SKILLS_GITHUB_BRANCH.to_string());
|
||||
let force = force.unwrap_or(false);
|
||||
|
||||
let mut meta = read_sync_meta();
|
||||
let now = now_epoch_secs();
|
||||
|
||||
// Respect cooldown unless forced
|
||||
if !force {
|
||||
if let Some(last_checked) = meta.last_checked_at {
|
||||
if now.saturating_sub(last_checked) < UPDATE_CHECK_INTERVAL_SECS {
|
||||
return Ok(serde_json::json!({
|
||||
"needs_update": false,
|
||||
"skipped": true,
|
||||
"reason": "checked_recently",
|
||||
"local_sha": meta.commit_sha,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch latest commit SHA from GitHub
|
||||
let remote_sha = fetch_remote_commit_sha(
|
||||
&repo,
|
||||
&branch,
|
||||
github_token.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Update last_checked_at regardless of whether an update is needed
|
||||
meta.last_checked_at = Some(now);
|
||||
if let Err(e) = write_sync_meta(&meta) {
|
||||
log::warn!("Failed to update last_checked_at: {}", e);
|
||||
}
|
||||
|
||||
let needs_update = match &meta.commit_sha {
|
||||
Some(local_sha) => local_sha != &remote_sha,
|
||||
None => true, // No local SHA means never synced
|
||||
};
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"needs_update": needs_update,
|
||||
"local_sha": meta.commit_sha,
|
||||
"remote_sha": remote_sha,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Read the icon file for a skill, returning it as a base64 data URL.
|
||||
///
|
||||
/// Checks for `icon.svg` first, then `icon.png` in the skill's directory.
|
||||
/// Returns `null` if no icon file is found.
|
||||
#[tauri::command]
|
||||
pub async fn skill_read_icon(skill_id: String) -> Result<Option<String>, String> {
|
||||
if skill_id.contains("..") || skill_id.contains('/') || skill_id.contains('\\') {
|
||||
return Err("Invalid skill ID".to_string());
|
||||
}
|
||||
|
||||
let skills_cwd = skill_cwd().await?;
|
||||
let skill_dir = PathBuf::from(&skills_cwd).join("skills").join(&skill_id);
|
||||
|
||||
let engine = base64::engine::general_purpose::STANDARD;
|
||||
|
||||
// Check for icon.svg
|
||||
let svg_path = skill_dir.join("icon.svg");
|
||||
if svg_path.exists() {
|
||||
let content = tokio::fs::read(&svg_path)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read icon.svg: {}", e))?;
|
||||
let b64 = engine.encode(&content);
|
||||
return Ok(Some(format!("data:image/svg+xml;base64,{}", b64)));
|
||||
}
|
||||
|
||||
// Check for icon.png
|
||||
let png_path = skill_dir.join("icon.png");
|
||||
if png_path.exists() {
|
||||
let content = tokio::fs::read(&png_path)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read icon.png: {}", e))?;
|
||||
let b64 = engine.encode(&content);
|
||||
return Ok(Some(format!("data:image/png;base64,{}", b64)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -233,6 +233,11 @@ pub fn run() {
|
||||
skill_list_manifests,
|
||||
skill_cwd,
|
||||
skill_venv_site_packages,
|
||||
skill_read_catalog,
|
||||
skill_sync_repo,
|
||||
skill_catalog_exists,
|
||||
skill_check_for_updates,
|
||||
skill_read_icon,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// User session information
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Session {
|
||||
/// JWT session token
|
||||
@@ -28,6 +29,7 @@ pub struct User {
|
||||
}
|
||||
|
||||
/// Token exchange request payload
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TokenExchangeRequest {
|
||||
pub token: String,
|
||||
@@ -42,6 +44,7 @@ pub struct TokenExchangeResponse {
|
||||
}
|
||||
|
||||
/// Auth error response from backend
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AuthErrorResponse {
|
||||
pub success: bool,
|
||||
|
||||
@@ -37,6 +37,7 @@ impl Default for SocketState {
|
||||
}
|
||||
|
||||
/// Generic socket message wrapper
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SocketMessage {
|
||||
pub event: String,
|
||||
@@ -44,6 +45,7 @@ pub struct SocketMessage {
|
||||
}
|
||||
|
||||
/// MCP request structure (JSON-RPC 2.0)
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpRequest {
|
||||
pub jsonrpc: String,
|
||||
@@ -54,6 +56,7 @@ pub struct McpRequest {
|
||||
}
|
||||
|
||||
/// MCP response structure (JSON-RPC 2.0)
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpResponse {
|
||||
pub jsonrpc: String,
|
||||
@@ -65,6 +68,7 @@ pub struct McpResponse {
|
||||
}
|
||||
|
||||
/// MCP error structure
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpError {
|
||||
pub code: i32,
|
||||
|
||||
@@ -3,9 +3,3 @@ pub mod socket_service;
|
||||
|
||||
#[cfg(desktop)]
|
||||
pub mod notification_service;
|
||||
|
||||
pub use session_service::SessionService;
|
||||
pub use socket_service::SocketService;
|
||||
|
||||
#[cfg(desktop)]
|
||||
pub use notification_service::NotificationService;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
|
||||
/// Service for managing native notifications
|
||||
#[allow(dead_code)]
|
||||
pub struct NotificationService;
|
||||
|
||||
impl NotificationService {
|
||||
/// Show a simple notification
|
||||
#[allow(dead_code)]
|
||||
pub fn show(app: &AppHandle, title: &str, body: &str) -> Result<(), String> {
|
||||
app.notification()
|
||||
.builder()
|
||||
@@ -16,6 +18,7 @@ impl NotificationService {
|
||||
}
|
||||
|
||||
/// Show a notification with an icon
|
||||
#[allow(dead_code)]
|
||||
pub fn show_with_icon(
|
||||
app: &AppHandle,
|
||||
title: &str,
|
||||
@@ -32,6 +35,7 @@ impl NotificationService {
|
||||
}
|
||||
|
||||
/// Show a notification for a new message
|
||||
#[allow(dead_code)]
|
||||
pub fn show_message_notification(
|
||||
app: &AppHandle,
|
||||
sender: &str,
|
||||
@@ -41,6 +45,7 @@ impl NotificationService {
|
||||
}
|
||||
|
||||
/// Check if notifications are permitted
|
||||
#[allow(dead_code)]
|
||||
pub fn is_permission_granted(app: &AppHandle) -> Result<bool, String> {
|
||||
app.notification()
|
||||
.permission_state()
|
||||
@@ -49,6 +54,7 @@ impl NotificationService {
|
||||
}
|
||||
|
||||
/// Request notification permission
|
||||
#[allow(dead_code)]
|
||||
pub fn request_permission(app: &AppHandle) -> Result<bool, String> {
|
||||
app.notification()
|
||||
.request_permission()
|
||||
|
||||
@@ -70,6 +70,7 @@ impl SessionService {
|
||||
}
|
||||
|
||||
/// Get the current session
|
||||
#[allow(dead_code)]
|
||||
pub fn get_session(&self) -> Option<Session> {
|
||||
self.cached_session.read().ok().and_then(|cache| {
|
||||
cache.as_ref().map(|s| Session {
|
||||
|
||||
@@ -18,9 +18,12 @@ use tauri::{AppHandle, Emitter};
|
||||
|
||||
/// Events emitted to the frontend
|
||||
pub mod events {
|
||||
#[allow(dead_code)]
|
||||
pub const SOCKET_CONNECTED: &str = "socket:connected";
|
||||
#[allow(dead_code)]
|
||||
pub const SOCKET_DISCONNECTED: &str = "socket:disconnected";
|
||||
pub const SOCKET_ERROR: &str = "socket:error";
|
||||
#[allow(dead_code)]
|
||||
pub const SOCKET_MESSAGE: &str = "socket:message";
|
||||
pub const SOCKET_STATE_CHANGED: &str = "socket:state_changed";
|
||||
pub const SOCKET_SHOULD_CONNECT: &str = "socket:should_connect";
|
||||
@@ -63,6 +66,7 @@ impl SocketService {
|
||||
}
|
||||
|
||||
/// Get current connection status
|
||||
#[allow(dead_code)]
|
||||
pub fn get_status(&self) -> ConnectionStatus {
|
||||
*self.status.read()
|
||||
}
|
||||
@@ -153,6 +157,7 @@ impl SocketService {
|
||||
}
|
||||
|
||||
/// Get stored connection parameters for reconnection
|
||||
#[allow(dead_code)]
|
||||
pub fn get_connection_params(&self) -> Option<(String, String)> {
|
||||
let backend_url = self.backend_url.read().clone();
|
||||
let token = self.auth_token.read().clone();
|
||||
|
||||
+14
-11
@@ -1,13 +1,14 @@
|
||||
import { HashRouter as Router } from "react-router-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import { PersistGate } from "redux-persist/integration/react";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { store, persistor } from "./store";
|
||||
import UserProvider from "./providers/UserProvider";
|
||||
import SocketProvider from "./providers/SocketProvider";
|
||||
import AIProvider from "./providers/AIProvider";
|
||||
import SkillProvider from "./providers/SkillProvider";
|
||||
import AppRoutes from "./AppRoutes";
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { HashRouter as Router } from 'react-router-dom';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
|
||||
import AppRoutes from './AppRoutes';
|
||||
import AIProvider from './providers/AIProvider';
|
||||
import SkillProvider from './providers/SkillProvider';
|
||||
import SocketProvider from './providers/SocketProvider';
|
||||
import UserProvider from './providers/UserProvider';
|
||||
import { persistor, store } from './store';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -25,7 +26,9 @@ function App() {
|
||||
AlphaHuman is in early beta.
|
||||
</div>
|
||||
</div>
|
||||
<AppRoutes />
|
||||
<div className="pt-7">
|
||||
<AppRoutes />
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
</SkillProvider>
|
||||
|
||||
+16
-23
@@ -1,16 +1,17 @@
|
||||
import { Routes, Route, Navigate } from "react-router-dom";
|
||||
import Welcome from "./pages/Welcome";
|
||||
import Login from "./pages/Login";
|
||||
import Onboarding from "./pages/onboarding/Onboarding";
|
||||
import Home from "./pages/Home";
|
||||
import PublicRoute from "./components/PublicRoute";
|
||||
import ProtectedRoute from "./components/ProtectedRoute";
|
||||
import DefaultRedirect from "./components/DefaultRedirect";
|
||||
import SettingsModal from "./components/settings/SettingsModal";
|
||||
import { useAppSelector } from "./store/hooks";
|
||||
import { selectIsOnboarded } from "./store/authSelectors";
|
||||
import { isTauri } from "./utils/tauriCommands";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import DefaultRedirect from './components/DefaultRedirect';
|
||||
import ProtectedRoute from './components/ProtectedRoute';
|
||||
import PublicRoute from './components/PublicRoute';
|
||||
import SettingsModal from './components/settings/SettingsModal';
|
||||
import Home from './pages/Home';
|
||||
import Login from './pages/Login';
|
||||
import Onboarding from './pages/onboarding/Onboarding';
|
||||
import Welcome from './pages/Welcome';
|
||||
import { selectIsOnboarded } from './store/authSelectors';
|
||||
import { useAppSelector } from './store/hooks';
|
||||
import { isTauri } from './utils/tauriCommands';
|
||||
|
||||
const OnboardingRoute = () => {
|
||||
const isOnboarded = useAppSelector(selectIsOnboarded);
|
||||
@@ -64,11 +65,7 @@ const AppRoutes = () => {
|
||||
<Route
|
||||
path="/home"
|
||||
element={
|
||||
<ProtectedRoute
|
||||
requireAuth={true}
|
||||
requireOnboarded={true}
|
||||
redirectTo="/onboarding"
|
||||
>
|
||||
<ProtectedRoute requireAuth={true} requireOnboarded={true} redirectTo="/onboarding">
|
||||
<Home />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
@@ -78,11 +75,7 @@ const AppRoutes = () => {
|
||||
<Route
|
||||
path="/settings/*"
|
||||
element={
|
||||
<ProtectedRoute
|
||||
requireAuth={true}
|
||||
requireOnboarded={true}
|
||||
redirectTo="/onboarding"
|
||||
>
|
||||
<ProtectedRoute requireAuth={true} requireOnboarded={true} redirectTo="/onboarding">
|
||||
<Home />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
* - Silences console output during tests (unless DEBUG_TESTS=1)
|
||||
* - Resets rate limiter module-level state between tests
|
||||
*/
|
||||
|
||||
import { beforeEach, vi } from "vitest";
|
||||
import { beforeEach, vi } from 'vitest';
|
||||
|
||||
// Silence console during tests to keep output clean
|
||||
if (!process.env.DEBUG_TESTS) {
|
||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
}
|
||||
|
||||
// Reset rate limiter per-request counter before each test.
|
||||
@@ -19,8 +18,8 @@ if (!process.env.DEBUG_TESTS) {
|
||||
// that leaks between tests if not cleared.
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
const { resetRequestCallCount } = await import("../lib/mcp/rateLimiter");
|
||||
if (typeof resetRequestCallCount === "function") {
|
||||
const { resetRequestCallCount } = await import('../lib/mcp/rateLimiter');
|
||||
if (typeof resetRequestCallCount === 'function') {
|
||||
resetRequestCallCount();
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const ExchangeIcon = ({ className = "w-5 h-5" }: { className?: string }) => {
|
||||
const ExchangeIcon = ({ className = 'w-5 h-5' }: { className?: string }) => {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const GmailIcon = ({ className = "w-4 h-4" }: { className?: string }) => {
|
||||
const GmailIcon = ({ className = 'w-4 h-4' }: { className?: string }) => {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M24 5.5v13.05c0 .85-.73 1.59-1.59 1.59H1.59C.73 21.14 0 20.4 0 19.55V5.5L12 13.25 24 5.5zM24 4.5c0-.42-.2-.83-.53-1.09L12 11.25.53 3.41C.2 3.67 0 4.08 0 4.5v.75L12 13 24 5.25V4.5z" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const GoogleIcon = ({ className = "w-5 h-5" }: { className?: string }) => {
|
||||
const GoogleIcon = ({ className = 'w-5 h-5' }: { className?: string }) => {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24">
|
||||
<path
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const WalletIcon = ({ className = "w-5 h-5" }: { className?: string }) => {
|
||||
const WalletIcon = ({ className = 'w-5 h-5' }: { className?: string }) => {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
|
||||
|
||||
@@ -1,36 +1,28 @@
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectSocketStatus } from "../store/socketSelectors";
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { selectSocketStatus } from '../store/socketSelectors';
|
||||
|
||||
interface ConnectionIndicatorProps {
|
||||
status?: "connected" | "disconnected" | "connecting";
|
||||
status?: 'connected' | 'disconnected' | 'connecting';
|
||||
description?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ConnectionIndicator = ({
|
||||
status: overrideStatus,
|
||||
description = "Your browser is now connected to the AlphaHuman AI. Keep this tab open to keep the connection alive. You can message your assistant with the button below.",
|
||||
className = "",
|
||||
description = 'Your browser is now connected to the AlphaHuman AI. Keep the app running to keep the connection alive. You can message your assistant with the button below.',
|
||||
className = '',
|
||||
}: ConnectionIndicatorProps) => {
|
||||
// Use socket store status, but allow override via props
|
||||
const storeStatus = useAppSelector(selectSocketStatus);
|
||||
const status = overrideStatus || storeStatus;
|
||||
const statusConfig = {
|
||||
connected: {
|
||||
color: "bg-sage-500",
|
||||
textColor: "text-sage-500",
|
||||
text: "Connected to AlphaHuman AI 🚀",
|
||||
},
|
||||
disconnected: {
|
||||
color: "bg-coral-500",
|
||||
textColor: "text-coral-500",
|
||||
text: "Disconnected",
|
||||
},
|
||||
connecting: {
|
||||
color: "bg-amber-500",
|
||||
textColor: "text-amber-500",
|
||||
text: "Connecting",
|
||||
color: 'bg-sage-500',
|
||||
textColor: 'text-sage-500',
|
||||
text: 'Connected to AlphaHuman AI 🚀',
|
||||
},
|
||||
disconnected: { color: 'bg-coral-500', textColor: 'text-coral-500', text: 'Disconnected' },
|
||||
connecting: { color: 'bg-amber-500', textColor: 'text-amber-500', text: 'Connecting' },
|
||||
};
|
||||
|
||||
const config = statusConfig[status];
|
||||
@@ -39,14 +31,11 @@ const ConnectionIndicator = ({
|
||||
<div className={`mb-6 ${className}`}>
|
||||
<div className="flex items-center justify-center space-x-2 mb-3">
|
||||
<div
|
||||
className={`w-2 h-2 ${config.color} rounded-full ${status === "connected" ? "animate-pulse" : ""}`}
|
||||
></div>
|
||||
className={`w-2 h-2 ${config.color} rounded-full ${status === 'connected' ? 'animate-pulse' : ''}`}></div>
|
||||
<span className={`text-sm ${config.textColor}`}>{config.text}</span>
|
||||
</div>
|
||||
{description && (
|
||||
<p className="text-xs opacity-60 text-center leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
<p className="text-xs opacity-60 text-center leading-relaxed">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectIsOnboarded } from "../store/authSelectors";
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { selectIsOnboarded } from '../store/authSelectors';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
|
||||
/**
|
||||
* Default redirect component that routes users based on their auth and onboarding status
|
||||
@@ -9,7 +10,7 @@ import { selectIsOnboarded } from "../store/authSelectors";
|
||||
* - Logged in and onboarded → /home
|
||||
*/
|
||||
const DefaultRedirect = () => {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const token = useAppSelector(state => state.auth.token);
|
||||
const isOnboarded = useAppSelector(selectIsOnboarded);
|
||||
|
||||
if (token && isOnboarded) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Design System Showcase Component
|
||||
@@ -25,9 +25,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
Good afternoon,
|
||||
<span className="text-gradient"> Cyrus</span>
|
||||
</h1>
|
||||
<p className="text-xl text-stone-600 mb-8">
|
||||
Your portfolio is up 12.5% today
|
||||
</p>
|
||||
<p className="text-xl text-stone-600 mb-8">Your portfolio is up 12.5% today</p>
|
||||
|
||||
{/* Status Card */}
|
||||
<div className="glass-surface inline-flex items-center space-x-4 px-6 py-4 rounded-2xl">
|
||||
@@ -46,12 +44,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
{/* Quick Actions */}
|
||||
<div className="mt-12 flex flex-wrap gap-4">
|
||||
<button className="btn-premium">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -69,32 +62,24 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
|
||||
{/* Color Palette Section */}
|
||||
<section className="container mx-auto px-6 py-16">
|
||||
<h2 className="text-3xl font-display font-bold text-stone-900 mb-8">
|
||||
Color System
|
||||
</h2>
|
||||
<h2 className="text-3xl font-display font-bold text-stone-900 mb-8">Color System</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Primary Colors */}
|
||||
<div className="card-elevated p-6">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">
|
||||
Primary Ocean
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">Primary Ocean</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-primary-500 rounded-xl shadow-soft" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-stone-900">
|
||||
Primary 500
|
||||
</p>
|
||||
<p className="text-sm font-medium text-stone-900">Primary 500</p>
|
||||
<p className="text-xs text-stone-500">#5B9BF3</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-primary-600 rounded-xl shadow-soft" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-stone-900">
|
||||
Primary 600
|
||||
</p>
|
||||
<p className="text-sm font-medium text-stone-900">Primary 600</p>
|
||||
<p className="text-xs text-stone-500">#4A83DD</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,9 +88,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
|
||||
{/* Success Colors */}
|
||||
<div className="card-elevated p-6">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">
|
||||
Sage Success
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">Sage Success</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-sage-500 rounded-xl shadow-soft" />
|
||||
@@ -126,9 +109,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
|
||||
{/* Accent Colors */}
|
||||
<div className="card-elevated p-6">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">
|
||||
Accent Palette
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">Accent Palette</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-accent-lavender rounded-xl shadow-soft" />
|
||||
@@ -151,16 +132,12 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
|
||||
{/* Typography Section */}
|
||||
<section className="container mx-auto px-6 py-16">
|
||||
<h2 className="text-3xl font-display font-bold text-stone-900 mb-8">
|
||||
Typography Scale
|
||||
</h2>
|
||||
<h2 className="text-3xl font-display font-bold text-stone-900 mb-8">Typography Scale</h2>
|
||||
|
||||
<div className="card-elevated p-8 space-y-6">
|
||||
<div>
|
||||
<p className="text-xs text-stone-500 mb-1">Display · 4.5rem</p>
|
||||
<h1 className="text-7xl font-display font-bold text-stone-900">
|
||||
Beautiful Typography
|
||||
</h1>
|
||||
<h1 className="text-7xl font-display font-bold text-stone-900">Beautiful Typography</h1>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -180,17 +157,14 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
<div>
|
||||
<p className="text-xs text-stone-500 mb-1">Body · 1rem</p>
|
||||
<p className="text-base text-stone-700 leading-relaxed max-w-3xl">
|
||||
The typography system is designed to create clear visual hierarchy
|
||||
while maintaining excellent readability. Each text size has been
|
||||
carefully calibrated with appropriate line heights and letter
|
||||
spacing to ensure optimal legibility across all device sizes.
|
||||
The typography system is designed to create clear visual hierarchy while maintaining
|
||||
excellent readability. Each text size has been carefully calibrated with appropriate
|
||||
line heights and letter spacing to ensure optimal legibility across all device sizes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-stone-500 mb-1">
|
||||
Monospace · For prices and data
|
||||
</p>
|
||||
<p className="text-xs text-stone-500 mb-1">Monospace · For prices and data</p>
|
||||
<p className="text-crypto-price text-2xl text-stone-900">
|
||||
$48,392.50 <span className="text-market-bullish">+2.45%</span>
|
||||
</p>
|
||||
@@ -200,9 +174,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
|
||||
{/* Card Components Section */}
|
||||
<section className="container mx-auto px-6 py-16">
|
||||
<h2 className="text-3xl font-display font-bold text-stone-900 mb-8">
|
||||
Card Components
|
||||
</h2>
|
||||
<h2 className="text-3xl font-display font-bold text-stone-900 mb-8">Card Components</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Market Card */}
|
||||
@@ -224,9 +196,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
<div className="space-y-2">
|
||||
<p className="price-ticker up">$48,392.50</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-market-bullish font-medium">
|
||||
+$1,185.20
|
||||
</span>
|
||||
<span className="text-sm text-market-bullish font-medium">+$1,185.20</span>
|
||||
<span className="text-sm text-market-bullish">(+2.45%)</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,20 +205,13 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
|
||||
{/* Interactive Card */}
|
||||
<div className="card-interactive">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-2">
|
||||
Portfolio Overview
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-2">Portfolio Overview</h3>
|
||||
<p className="text-stone-600 mb-4">
|
||||
Track your investments and monitor performance in real-time.
|
||||
</p>
|
||||
<div className="flex items-center text-primary-600 font-medium">
|
||||
<span>View Details</span>
|
||||
<svg
|
||||
className="w-4 h-4 ml-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -262,16 +225,13 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
{/* Elevated Card */}
|
||||
<div className="card-elevated">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-stone-900">
|
||||
Quick Stats
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900">Quick Stats</h3>
|
||||
<div className="w-8 h-8 bg-primary-100 rounded-lg flex items-center justify-center">
|
||||
<svg
|
||||
className="w-4 h-4 text-primary-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -284,21 +244,15 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-stone-600">Total Value</span>
|
||||
<span className="font-mono font-semibold text-stone-900">
|
||||
$125,430
|
||||
</span>
|
||||
<span className="font-mono font-semibold text-stone-900">$125,430</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-stone-600">24h Change</span>
|
||||
<span className="font-mono font-semibold text-market-bullish">
|
||||
+3.2%
|
||||
</span>
|
||||
<span className="font-mono font-semibold text-market-bullish">+3.2%</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-stone-600">Holdings</span>
|
||||
<span className="font-mono font-semibold text-stone-900">
|
||||
12
|
||||
</span>
|
||||
<span className="font-mono font-semibold text-stone-900">12</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -327,12 +281,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
{/* Navigation Items */}
|
||||
<nav className="p-2">
|
||||
<a href="#" className="nav-item-premium active">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -343,12 +292,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="#" className="nav-item-premium">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -359,12 +303,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
Profile Settings
|
||||
</a>
|
||||
<a href="#" className="nav-item-premium">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -375,12 +314,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
Notifications
|
||||
</a>
|
||||
<a href="#" className="nav-item-premium">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -391,12 +325,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
Security
|
||||
</a>
|
||||
<a href="#" className="nav-item-premium">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -413,21 +342,13 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
|
||||
{/* Form Elements */}
|
||||
<section className="container mx-auto px-6 py-16">
|
||||
<h2 className="text-3xl font-display font-bold text-stone-900 mb-8">
|
||||
Form Elements
|
||||
</h2>
|
||||
<h2 className="text-3xl font-display font-bold text-stone-900 mb-8">Form Elements</h2>
|
||||
|
||||
<div className="max-w-2xl">
|
||||
<div className="card-elevated p-8 space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-stone-700 mb-2">
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
className="input-elevated"
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
<label className="block text-sm font-medium text-stone-700 mb-2">Email Address</label>
|
||||
<input type="email" className="input-elevated" placeholder="Enter your email" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -436,11 +357,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-4 top-3 text-stone-500">$</span>
|
||||
<input
|
||||
type="text"
|
||||
className="input-elevated pl-8"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
<input type="text" className="input-elevated pl-8" placeholder="0.00" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
detectPlatform,
|
||||
getDownloadLink,
|
||||
@@ -55,7 +56,7 @@ const DownloadScreen = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-2xl">
|
||||
{downloadOptions.find((opt) => opt.platform === selectedPlatform)?.icon}
|
||||
{downloadOptions.find(opt => opt.platform === selectedPlatform)?.icon}
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm opacity-70">Recommended for you</p>
|
||||
@@ -64,8 +65,7 @@ const DownloadScreen = () => {
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white font-semibold px-6 py-2 rounded-lg transition-all duration-300 hover:shadow-medium hover:scale-[1.02] active:scale-[0.98]"
|
||||
>
|
||||
className="bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white font-semibold px-6 py-2 rounded-lg transition-all duration-300 hover:shadow-medium hover:scale-[1.02] active:scale-[0.98]">
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
@@ -77,16 +77,15 @@ const DownloadScreen = () => {
|
||||
<p className="text-sm opacity-70 mb-4 text-center">Or download for other platforms:</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{downloadOptions
|
||||
.filter((opt) => opt.platform !== selectedPlatform)
|
||||
.map((option) => (
|
||||
.filter(opt => opt.platform !== selectedPlatform)
|
||||
.map(option => (
|
||||
<button
|
||||
key={option.platform}
|
||||
onClick={() => {
|
||||
setSelectedPlatform(option.platform);
|
||||
window.open(getDownloadLink(option.platform), '_blank');
|
||||
}}
|
||||
className="flex items-center justify-center space-x-2 bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/20 rounded-lg p-3 transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]"
|
||||
>
|
||||
className="flex items-center justify-center space-x-2 bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/20 rounded-lg p-3 transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]">
|
||||
<span className="text-xl">{option.icon}</span>
|
||||
<span className="text-sm font-medium">{option.label}</span>
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import GmailIcon from "../assets/icons/GmailIcon";
|
||||
import GmailIcon from '../assets/icons/GmailIcon';
|
||||
|
||||
interface GmailConnectionIndicatorProps {
|
||||
description?: string;
|
||||
@@ -7,7 +7,7 @@ interface GmailConnectionIndicatorProps {
|
||||
|
||||
const GmailConnectionIndicator = ({
|
||||
description,
|
||||
className = "",
|
||||
className = '',
|
||||
}: GmailConnectionIndicatorProps) => {
|
||||
// Gmail is always offline for now (placeholder)
|
||||
const gmailIsOnline = false;
|
||||
@@ -16,23 +16,16 @@ const GmailConnectionIndicator = ({
|
||||
<div className={`mb-6 ${className}`}>
|
||||
<div className="flex items-center justify-center space-x-2 mb-3">
|
||||
<div
|
||||
className={`w-2 h-2 ${gmailIsOnline ? "bg-red-500" : "bg-gray-500"} rounded-full ${gmailIsOnline ? "animate-pulse" : ""}`}
|
||||
></div>
|
||||
className={`w-2 h-2 ${gmailIsOnline ? 'bg-red-500' : 'bg-gray-500'} rounded-full ${gmailIsOnline ? 'animate-pulse' : ''}`}></div>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<GmailIcon
|
||||
className={`w-4 h-4 ${gmailIsOnline ? "text-red-500" : "text-gray-500"}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-sm ${gmailIsOnline ? "text-red-500" : "text-gray-500"}`}
|
||||
>
|
||||
{gmailIsOnline ? "Connected to Gmail" : "Gmail is Offline"}
|
||||
<GmailIcon className={`w-4 h-4 ${gmailIsOnline ? 'text-red-500' : 'text-gray-500'}`} />
|
||||
<span className={`text-sm ${gmailIsOnline ? 'text-red-500' : 'text-gray-500'}`}>
|
||||
{gmailIsOnline ? 'Connected to Gmail' : 'Gmail is Offline'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{description && (
|
||||
<p className="text-xs opacity-60 text-center leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
<p className="text-xs opacity-60 text-center leading-relaxed">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLottie } from "lottie-react";
|
||||
import { useLottie } from 'lottie-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface LottieAnimationProps {
|
||||
src: string;
|
||||
@@ -10,7 +10,7 @@ interface LottieAnimationProps {
|
||||
|
||||
const LottieAnimation = ({
|
||||
src,
|
||||
className = "",
|
||||
className = '',
|
||||
height = 200,
|
||||
width = 200,
|
||||
}: LottieAnimationProps) => {
|
||||
@@ -18,23 +18,14 @@ const LottieAnimation = ({
|
||||
|
||||
useEffect(() => {
|
||||
fetch(src)
|
||||
.then((response) => response.json())
|
||||
.then((data) => setAnimationData(data))
|
||||
.catch((error) =>
|
||||
console.error("Failed to load Lottie animation:", error),
|
||||
);
|
||||
.then(response => response.json())
|
||||
.then(data => setAnimationData(data))
|
||||
.catch(error => console.error('Failed to load Lottie animation:', error));
|
||||
}, [src]);
|
||||
|
||||
const options = {
|
||||
animationData,
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
};
|
||||
const options = { animationData, loop: true, autoplay: true };
|
||||
|
||||
const { View } = useLottie(options, {
|
||||
height,
|
||||
width,
|
||||
});
|
||||
const { View } = useLottie(options, { height, width });
|
||||
|
||||
if (!animationData) {
|
||||
return <div className={className} style={{ height, width }} />;
|
||||
|
||||
@@ -3,18 +3,13 @@ interface PrivacyFeatureCardProps {
|
||||
description: string;
|
||||
}
|
||||
|
||||
const PrivacyFeatureCard = ({
|
||||
title,
|
||||
description,
|
||||
}: PrivacyFeatureCardProps) => {
|
||||
const PrivacyFeatureCard = ({ title, description }: PrivacyFeatureCardProps) => {
|
||||
return (
|
||||
<div className="bg-stone-800/50 rounded-xl p-3 border border-stone-700">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm mb-2 text-center">{title}</h3>
|
||||
<p className="opacity-70 text-xs leading-relaxed text-center">
|
||||
{description}
|
||||
</p>
|
||||
<p className="opacity-70 text-xs leading-relaxed text-center">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,17 +3,14 @@ interface ProgressIndicatorProps {
|
||||
totalSteps: number;
|
||||
}
|
||||
|
||||
const ProgressIndicator = ({
|
||||
currentStep,
|
||||
totalSteps,
|
||||
}: ProgressIndicatorProps) => {
|
||||
const ProgressIndicator = ({ currentStep, totalSteps }: ProgressIndicatorProps) => {
|
||||
return (
|
||||
<div className="flex items-center justify-center space-x-1.5 mb-6">
|
||||
{Array.from({ length: totalSteps }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-8 h-0.5 rounded-full ${
|
||||
index < currentStep ? "bg-primary-500" : "bg-stone-700"
|
||||
index < currentStep ? 'bg-primary-500' : 'bg-stone-700'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectIsOnboarded } from "../store/authSelectors";
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { selectIsOnboarded } from '../store/authSelectors';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
@@ -18,17 +19,17 @@ const ProtectedRoute = ({
|
||||
requireOnboarded = false,
|
||||
redirectTo,
|
||||
}: ProtectedRouteProps) => {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const token = useAppSelector(state => state.auth.token);
|
||||
const isOnboarded = useAppSelector(selectIsOnboarded);
|
||||
|
||||
// If auth is required but user is not logged in
|
||||
if (requireAuth && !token) {
|
||||
return <Navigate to={redirectTo || "/"} replace />;
|
||||
return <Navigate to={redirectTo || '/'} replace />;
|
||||
}
|
||||
|
||||
// If onboarding is required but user is not onboarded
|
||||
if (requireOnboarded && !isOnboarded) {
|
||||
return <Navigate to={redirectTo || "/onboarding"} replace />;
|
||||
return <Navigate to={redirectTo || '/onboarding'} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectIsOnboarded } from "../store/authSelectors";
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { selectIsOnboarded } from '../store/authSelectors';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
|
||||
interface PublicRouteProps {
|
||||
children: React.ReactNode;
|
||||
@@ -13,12 +14,12 @@ interface PublicRouteProps {
|
||||
* If logged in but not onboarded -> redirect to /onboarding
|
||||
*/
|
||||
const PublicRoute = ({ children, redirectTo }: PublicRouteProps) => {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const token = useAppSelector(state => state.auth.token);
|
||||
const isOnboarded = useAppSelector(selectIsOnboarded);
|
||||
|
||||
// If user is logged in and onboarded, redirect to home
|
||||
if (token && isOnboarded) {
|
||||
return <Navigate to={redirectTo || "/home"} replace />;
|
||||
return <Navigate to={redirectTo || '/home'} replace />;
|
||||
}
|
||||
|
||||
// If user is logged in but not onboarded, redirect to onboarding
|
||||
|
||||
+316
-140
@@ -1,24 +1,25 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSkillConnectionStatus } from "../lib/skills/hooks";
|
||||
import type { SkillConnectionStatus } from "../lib/skills/types";
|
||||
import SkillSetupModal from "./skills/SkillSetupModal";
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import TelegramIcon from "../assets/icons/telegram.svg";
|
||||
import GoogleIcon from "../assets/icons/GoogleIcon";
|
||||
import NotionIcon from "../assets/icons/notion.svg";
|
||||
import GoogleIcon from '../assets/icons/GoogleIcon';
|
||||
import NotionIcon from '../assets/icons/notion.svg';
|
||||
import TelegramIcon from '../assets/icons/telegram.svg';
|
||||
import { useSkillConnectionStatus } from '../lib/skills/hooks';
|
||||
import type { SkillConnectionStatus, SkillHostConnectionState } from '../lib/skills/types';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import SkillSetupModal from './skills/SkillSetupModal';
|
||||
|
||||
// Map skill IDs to icons
|
||||
const SKILL_ICONS: Record<string, React.ReactElement> = {
|
||||
telegram: <img src={TelegramIcon} alt="Telegram" className="w-8 h-8" />,
|
||||
email: <GoogleIcon className="w-8 h-8" />,
|
||||
notion: <img src={NotionIcon} alt="Notion" className="w-8 h-8" />,
|
||||
telegram: <img src={TelegramIcon} alt="Telegram" className="w-5 h-5" />,
|
||||
email: <GoogleIcon className="w-5 h-5" />,
|
||||
notion: <img src={NotionIcon} alt="Notion" className="w-5 h-5" />,
|
||||
github: (
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
),
|
||||
otter: (
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
|
||||
</svg>
|
||||
),
|
||||
@@ -26,92 +27,65 @@ const SKILL_ICONS: Record<string, React.ReactElement> = {
|
||||
|
||||
// Default icon for unknown skills
|
||||
const DefaultIcon = () => (
|
||||
<div className="w-8 h-8 rounded-full bg-primary-500/20 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
<div className="w-5 h-5 rounded-full bg-primary-500/20 flex items-center justify-center">
|
||||
<svg className="w-3 h-3 text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Status badge colors with better contrast
|
||||
const STATUS_COLORS: Record<SkillConnectionStatus, { bg: string; text: string; border: string }> = {
|
||||
connected: {
|
||||
bg: "bg-sage-500/30",
|
||||
text: "text-sage-100",
|
||||
border: "border-sage-500/50",
|
||||
},
|
||||
connecting: {
|
||||
bg: "bg-amber-500/30",
|
||||
text: "text-amber-100",
|
||||
border: "border-amber-500/50",
|
||||
},
|
||||
not_authenticated: {
|
||||
bg: "bg-amber-500/30",
|
||||
text: "text-amber-100",
|
||||
border: "border-amber-500/50",
|
||||
},
|
||||
disconnected: {
|
||||
bg: "bg-stone-600/40",
|
||||
text: "text-stone-200",
|
||||
border: "border-stone-500/60",
|
||||
},
|
||||
error: {
|
||||
bg: "bg-coral-500/30",
|
||||
text: "text-coral-100",
|
||||
border: "border-coral-500/50",
|
||||
},
|
||||
offline: {
|
||||
bg: "bg-stone-600/40",
|
||||
text: "text-stone-200",
|
||||
border: "border-stone-500/60",
|
||||
},
|
||||
setup_required: {
|
||||
bg: "bg-primary-500/30",
|
||||
text: "text-primary-100",
|
||||
border: "border-primary-500/50",
|
||||
},
|
||||
// Status display text and colors
|
||||
const STATUS_DISPLAY: Record<SkillConnectionStatus, { text: string; color: string }> = {
|
||||
connected: { text: 'Connected', color: 'text-sage-400' },
|
||||
connecting: { text: 'Connecting', color: 'text-amber-400' },
|
||||
not_authenticated: { text: 'Not Auth', color: 'text-amber-400' },
|
||||
disconnected: { text: 'Disconnected', color: 'text-stone-400' },
|
||||
error: { text: 'Error', color: 'text-coral-400' },
|
||||
offline: { text: 'Offline', color: 'text-stone-500' },
|
||||
setup_required: { text: 'Setup', color: 'text-primary-400' },
|
||||
};
|
||||
|
||||
interface SkillCardProps {
|
||||
interface SkillRowProps {
|
||||
skillId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon?: React.ReactElement;
|
||||
onConnect: () => void;
|
||||
onConnect: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
function SkillCard({ skillId, name, description: _description, icon, onConnect }: SkillCardProps) {
|
||||
function SkillRow({ skillId, name, icon, onConnect }: SkillRowProps) {
|
||||
const connectionStatus = useSkillConnectionStatus(skillId);
|
||||
const statusConfig = STATUS_COLORS[connectionStatus] || STATUS_COLORS.offline;
|
||||
const statusDisplay = STATUS_DISPLAY[connectionStatus] || STATUS_DISPLAY.offline;
|
||||
|
||||
return (
|
||||
<button
|
||||
<tr
|
||||
onClick={onConnect}
|
||||
className="glass rounded-lg p-2.5 hover:bg-stone-800/30 transition-all duration-200 text-left focus:outline-none focus:ring-2 focus:ring-primary-500/50 group"
|
||||
>
|
||||
<div className="flex flex-col items-center space-y-1.5">
|
||||
{/* Icon */}
|
||||
<div className="w-8 h-8 flex items-center justify-center text-white opacity-80 group-hover:opacity-100 transition-opacity">
|
||||
{icon || <DefaultIcon />}
|
||||
className="skill-row group hover:bg-stone-800/20 transition-all duration-300 cursor-pointer border-b border-stone-800/30 last:border-0">
|
||||
<td className="py-2.5 px-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-5 h-5 flex items-center justify-center text-white opacity-70 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
||||
{icon || <DefaultIcon />}
|
||||
</div>
|
||||
<span className="text-sm text-white font-medium">{name}</span>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div className="text-center w-full">
|
||||
<div className="font-medium text-xs text-white">{name}</div>
|
||||
</div>
|
||||
|
||||
{/* Status badge */}
|
||||
<div className={`px-1.5 py-0.5 text-[10px] font-medium rounded-full border ${statusConfig.bg} ${statusConfig.text} ${statusConfig.border}`}>
|
||||
{connectionStatus === "connected" && "Connected"}
|
||||
{connectionStatus === "connecting" && "Connecting..."}
|
||||
{connectionStatus === "not_authenticated" && "Not Auth"}
|
||||
{connectionStatus === "disconnected" && "Disconnected"}
|
||||
{connectionStatus === "error" && "Error"}
|
||||
{connectionStatus === "offline" && "Offline"}
|
||||
{connectionStatus === "setup_required" && "Setup"}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-right">
|
||||
<span className={`text-xs ${statusDisplay.color}`}>{statusDisplay.text}</span>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 w-8">
|
||||
<svg
|
||||
className="w-4 h-4 text-stone-500 group-hover:text-stone-300 transition-colors"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,68 +106,136 @@ interface SkillsCatalog {
|
||||
skills: SkillCatalogEntry[];
|
||||
}
|
||||
|
||||
interface SkillListEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon?: React.ReactElement;
|
||||
hasSetup: boolean;
|
||||
}
|
||||
|
||||
// Helper function to derive connection status (same logic as in hooks.ts)
|
||||
function deriveConnectionStatus(
|
||||
lifecycleStatus: string | undefined,
|
||||
setupComplete: boolean | undefined,
|
||||
skillState: Record<string, unknown> | undefined
|
||||
): SkillConnectionStatus {
|
||||
if (!lifecycleStatus || lifecycleStatus === 'installed') {
|
||||
return 'offline';
|
||||
}
|
||||
if (lifecycleStatus === 'error') {
|
||||
return 'error';
|
||||
}
|
||||
if (lifecycleStatus === 'setup_required' || lifecycleStatus === 'setup_in_progress') {
|
||||
return 'setup_required';
|
||||
}
|
||||
if (lifecycleStatus === 'starting') {
|
||||
return 'connecting';
|
||||
}
|
||||
const hostState = skillState as SkillHostConnectionState | undefined;
|
||||
if (!hostState) {
|
||||
return lifecycleStatus === 'ready' ? 'connecting' : 'connecting';
|
||||
}
|
||||
const connStatus = hostState.connection_status;
|
||||
const authStatus = hostState.auth_status;
|
||||
if (connStatus === 'error' || authStatus === 'error') {
|
||||
return 'error';
|
||||
}
|
||||
if (connStatus === 'connected' && authStatus === 'authenticated') {
|
||||
return 'connected';
|
||||
}
|
||||
if (connStatus === 'connecting' || authStatus === 'authenticating') {
|
||||
return 'connecting';
|
||||
}
|
||||
if (connStatus === 'connected' && authStatus === 'not_authenticated') {
|
||||
return 'not_authenticated';
|
||||
}
|
||||
if (connStatus === 'disconnected') {
|
||||
return setupComplete ? 'disconnected' : 'setup_required';
|
||||
}
|
||||
return 'connecting';
|
||||
}
|
||||
|
||||
// Priority order for sorting (lower number = higher priority)
|
||||
const STATUS_PRIORITY: Record<SkillConnectionStatus, number> = {
|
||||
connected: 1,
|
||||
connecting: 2,
|
||||
not_authenticated: 3,
|
||||
disconnected: 4,
|
||||
setup_required: 5,
|
||||
offline: 6,
|
||||
error: 7,
|
||||
};
|
||||
|
||||
export default function SkillsGrid() {
|
||||
const [skillsList, setSkillsList] = useState<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon?: React.ReactElement;
|
||||
}>>([]);
|
||||
const [skillsList, setSkillsList] = useState<SkillListEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [setupModalOpen, setSetupModalOpen] = useState(false);
|
||||
const [managementModalOpen, setManagementModalOpen] = useState(false);
|
||||
const [activeSkillId, setActiveSkillId] = useState<string | null>(null);
|
||||
const [activeSkillName, setActiveSkillName] = useState<string>("");
|
||||
const [activeSkillName, setActiveSkillName] = useState<string>('');
|
||||
const [activeSkillDescription, setActiveSkillDescription] = useState<string>('');
|
||||
const [activeSkillHasSetup, setActiveSkillHasSetup] = useState(false);
|
||||
|
||||
// Get Redux state for sorting
|
||||
const skillsState = useAppSelector(state => state.skills.skills);
|
||||
const skillStates = useAppSelector(state => state.skills.skillStates);
|
||||
|
||||
useEffect(() => {
|
||||
// Load skills catalog from the skills repo
|
||||
// The file should be copied to public/skills-catalog.json during build
|
||||
// Load skills catalog from the local skills directory via Rust.
|
||||
// In dev: reads from the submodule. In prod: reads from ~/.alphahuman/skills/.
|
||||
const loadSkillsCatalog = async () => {
|
||||
try {
|
||||
// Try to load from public folder (served at root in Vite)
|
||||
const response = await fetch("/skills-catalog.json");
|
||||
if (!response.ok) {
|
||||
// Fallback: try skills submodule path
|
||||
const fallbackResponse = await fetch("/skills/skills-catalog.json");
|
||||
if (!fallbackResponse.ok) {
|
||||
console.warn("Could not load skills-catalog.json. Make sure it's copied to public/ folder.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const catalog: SkillsCatalog = await fallbackResponse.json();
|
||||
processCatalog(catalog);
|
||||
return;
|
||||
}
|
||||
const catalog: SkillsCatalog = await response.json();
|
||||
processCatalog(catalog);
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
const catalog: SkillsCatalog = await invoke('skill_read_catalog');
|
||||
|
||||
// Load manifests to get proper display names
|
||||
const manifests = await invoke<Array<Record<string, unknown>>>('skill_list_manifests');
|
||||
const manifestMap = new Map(
|
||||
manifests
|
||||
.filter(
|
||||
(m): m is { id: string; name: string } =>
|
||||
typeof m.id === 'string' && typeof m.name === 'string'
|
||||
)
|
||||
.map(m => [m.id, m.name])
|
||||
);
|
||||
|
||||
processCatalog(catalog, manifestMap);
|
||||
} catch (error) {
|
||||
console.error("Error loading skills catalog:", error);
|
||||
console.warn('Could not load skills catalog from filesystem:', error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const processCatalog = (catalog: SkillsCatalog) => {
|
||||
// Filter skills that have setup hooks and validate skill names
|
||||
const skillsWithSetup = catalog.skills.filter((skill) => {
|
||||
// Skip skills with underscores in name (used for tool namespacing)
|
||||
if (skill.name.includes("_")) {
|
||||
const processCatalog = (catalog: SkillsCatalog, manifestMap: Map<string, string>) => {
|
||||
// Validate skill names (underscores are reserved for tool namespacing)
|
||||
const validSkills = catalog.skills.filter(skill => {
|
||||
if (skill.name.includes('_')) {
|
||||
console.warn(
|
||||
`Skill "${skill.name}" contains underscore and will be skipped. Skill names cannot contain underscores.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
skill.hooks.includes("on_setup_start") &&
|
||||
skill.hooks.includes("on_setup_submit") &&
|
||||
skill.hooks.includes("on_setup_cancel")
|
||||
);
|
||||
return true;
|
||||
});
|
||||
|
||||
const processed = skillsWithSetup.map((skill) => ({
|
||||
id: skill.name,
|
||||
name: skill.name.charAt(0).toUpperCase() + skill.name.slice(1),
|
||||
description: skill.description,
|
||||
icon: SKILL_ICONS[skill.name],
|
||||
}));
|
||||
const processed: SkillListEntry[] = validSkills.map(skill => {
|
||||
const skillId = skill.name;
|
||||
// Use manifest name if available, otherwise capitalize the ID
|
||||
const displayName =
|
||||
manifestMap.get(skillId) || skill.name.charAt(0).toUpperCase() + skill.name.slice(1);
|
||||
|
||||
return {
|
||||
id: skillId,
|
||||
name: displayName,
|
||||
description: skill.description,
|
||||
icon: SKILL_ICONS[skillId],
|
||||
hasSetup:
|
||||
skill.hooks.includes('on_setup_start') &&
|
||||
skill.hooks.includes('on_setup_submit') &&
|
||||
skill.hooks.includes('on_setup_cancel'),
|
||||
};
|
||||
});
|
||||
|
||||
setSkillsList(processed);
|
||||
setLoading(false);
|
||||
@@ -202,32 +244,88 @@ export default function SkillsGrid() {
|
||||
loadSkillsCatalog();
|
||||
}, []);
|
||||
|
||||
// Sort skills by connection status (connected first)
|
||||
const sortedSkillsList = useMemo(() => {
|
||||
return [...skillsList].sort((a, b) => {
|
||||
const skillA = skillsState[a.id];
|
||||
const skillB = skillsState[b.id];
|
||||
const stateA = skillStates[a.id];
|
||||
const stateB = skillStates[b.id];
|
||||
|
||||
const statusA = deriveConnectionStatus(skillA?.status, skillA?.setupComplete, stateA);
|
||||
const statusB = deriveConnectionStatus(skillB?.status, skillB?.setupComplete, stateB);
|
||||
|
||||
const priorityA = STATUS_PRIORITY[statusA] ?? 999;
|
||||
const priorityB = STATUS_PRIORITY[statusB] ?? 999;
|
||||
|
||||
// If same priority, sort alphabetically by name
|
||||
if (priorityA === priorityB) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
return priorityA - priorityB;
|
||||
});
|
||||
}, [skillsList, skillsState, skillStates]);
|
||||
|
||||
// If loading or no skills, don't render
|
||||
if (loading || skillsList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleConnect = (skillId: string, skillName: string) => {
|
||||
setActiveSkillId(skillId);
|
||||
setActiveSkillName(skillName);
|
||||
const handleConnect = (skill: SkillListEntry) => {
|
||||
setActiveSkillId(skill.id);
|
||||
setActiveSkillName(skill.name);
|
||||
setActiveSkillDescription(skill.description);
|
||||
setActiveSkillHasSetup(skill.hasSetup);
|
||||
setSetupModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="animate-fade-up mt-4 mb-8">
|
||||
<h3 className="text-sm font-semibold text-white mb-4 px-1 text-center opacity-80">Available Skills</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
{skillsList.map((skill) => (
|
||||
<SkillCard
|
||||
key={skill.id}
|
||||
skillId={skill.id}
|
||||
name={skill.name}
|
||||
description={skill.description}
|
||||
icon={skill.icon}
|
||||
onConnect={() => handleConnect(skill.id, skill.name)}
|
||||
/>
|
||||
))}
|
||||
<div className="animate-fade-up mt-4 mb-8 relative">
|
||||
<h3 className="text-sm font-semibold text-white mb-3 px-1 opacity-80 text-center">
|
||||
Available Skills
|
||||
</h3>
|
||||
<div
|
||||
className="glass rounded-xl overflow-hidden skills-table-container relative cursor-pointer"
|
||||
onClick={() => setManagementModalOpen(true)}>
|
||||
<div className="skills-table-scroll">
|
||||
<table className="w-full">
|
||||
<thead className="skills-table-header">
|
||||
<tr className="border-b border-stone-800/30">
|
||||
<th className="py-2 px-3 text-left">
|
||||
<span className="text-xs font-medium text-stone-400 uppercase tracking-wider">
|
||||
Skill
|
||||
</span>
|
||||
</th>
|
||||
<th className="py-2 px-3 text-right">
|
||||
<span className="text-xs font-medium text-stone-400 uppercase tracking-wider">
|
||||
Status
|
||||
</span>
|
||||
</th>
|
||||
<th className="py-2 px-3 w-8"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="skills-table-body">
|
||||
{sortedSkillsList.map(skill => (
|
||||
<SkillRow
|
||||
key={skill.id}
|
||||
skillId={skill.id}
|
||||
name={skill.name}
|
||||
icon={skill.icon}
|
||||
onConnect={e => {
|
||||
e.stopPropagation();
|
||||
handleConnect(skill);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* Hover overlay */}
|
||||
<div className="skills-table-overlay absolute inset-0 bg-black/80 flex items-center justify-center rounded-xl opacity-0 transition-opacity duration-200 pointer-events-none">
|
||||
<span className="text-sm font-medium text-white">Click to manage skills</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -236,12 +334,90 @@ export default function SkillsGrid() {
|
||||
<SkillSetupModal
|
||||
skillId={activeSkillId}
|
||||
skillName={activeSkillName}
|
||||
skillDescription={activeSkillDescription}
|
||||
hasSetup={activeSkillHasSetup}
|
||||
onClose={() => {
|
||||
setSetupModalOpen(false);
|
||||
setActiveSkillId(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Skills Management Modal */}
|
||||
{managementModalOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 animate-fade-in"
|
||||
onClick={() => setManagementModalOpen(false)}>
|
||||
<div
|
||||
className="bg-stone-900 rounded-2xl max-w-2xl w-full max-h-[80vh] shadow-large border border-stone-700/50 flex flex-col overflow-hidden animate-slide-up"
|
||||
onClick={e => e.stopPropagation()}>
|
||||
{/* Sticky Header */}
|
||||
<div className="flex items-center justify-between p-6 pb-4 border-b border-stone-700/50 flex-shrink-0 bg-stone-900">
|
||||
<h2 className="text-xl font-semibold text-white">Manage Skills</h2>
|
||||
<button
|
||||
onClick={() => setManagementModalOpen(false)}
|
||||
className="text-stone-400 hover:text-white transition-colors">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/* Scrollable Content */}
|
||||
<div className="overflow-y-auto flex-1 p-6 pt-4">
|
||||
<div className="space-y-2">
|
||||
{sortedSkillsList.map(skill => {
|
||||
const skillState = skillsState[skill.id];
|
||||
const stateData = skillStates[skill.id];
|
||||
const connectionStatus = deriveConnectionStatus(
|
||||
skillState?.status,
|
||||
skillState?.setupComplete,
|
||||
stateData
|
||||
);
|
||||
const statusDisplay = STATUS_DISPLAY[connectionStatus] || STATUS_DISPLAY.offline;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={skill.id}
|
||||
className="flex items-center justify-between p-3 rounded-lg bg-stone-800/30 border border-stone-700/30 hover:bg-stone-800/50 transition-colors">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="w-8 h-8 flex items-center justify-center text-white opacity-70 flex-shrink-0">
|
||||
{skill.icon || <DefaultIcon />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm font-medium text-white">{skill.name}</div>
|
||||
<span className={`text-xs ${statusDisplay.color}`}>
|
||||
{statusDisplay.text}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-stone-400">{skill.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setActiveSkillId(skill.id);
|
||||
setActiveSkillName(skill.name);
|
||||
setActiveSkillDescription(skill.description);
|
||||
setActiveSkillHasSetup(skill.hasSetup);
|
||||
setSetupModalOpen(true);
|
||||
}}
|
||||
className="px-4 py-1.5 text-xs font-medium text-primary-300 bg-primary-500/10 border border-primary-500/30 rounded-lg hover:bg-primary-500/20 transition-colors flex-shrink-0 ml-3">
|
||||
Configure
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,70 +1,39 @@
|
||||
import { useSkillConnectionStatus } from "../lib/skills/hooks";
|
||||
import type { SkillConnectionStatus } from "../lib/skills/types";
|
||||
import TelegramIcon from "../assets/icons/telegram.svg";
|
||||
import TelegramIcon from '../assets/icons/telegram.svg';
|
||||
import { useSkillConnectionStatus } from '../lib/skills/hooks';
|
||||
import type { SkillConnectionStatus } from '../lib/skills/types';
|
||||
|
||||
interface TelegramConnectionIndicatorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const STATUS_CONFIG: Record<
|
||||
SkillConnectionStatus,
|
||||
{ dot: string; text: string; label: string }
|
||||
> = {
|
||||
connected: {
|
||||
dot: "bg-sage-500",
|
||||
text: "text-sage-500",
|
||||
label: "Telegram Connected",
|
||||
},
|
||||
connecting: {
|
||||
dot: "bg-amber-500",
|
||||
text: "text-amber-500",
|
||||
label: "Telegram Connecting...",
|
||||
},
|
||||
const STATUS_CONFIG: Record<SkillConnectionStatus, { dot: string; text: string; label: string }> = {
|
||||
connected: { dot: 'bg-sage-500', text: 'text-sage-500', label: 'Telegram Connected' },
|
||||
connecting: { dot: 'bg-amber-500', text: 'text-amber-500', label: 'Telegram Connecting...' },
|
||||
not_authenticated: {
|
||||
dot: "bg-amber-500",
|
||||
text: "text-amber-500",
|
||||
label: "Telegram Not Authenticated",
|
||||
},
|
||||
disconnected: {
|
||||
dot: "bg-gray-500",
|
||||
text: "text-gray-500",
|
||||
label: "Telegram Disconnected",
|
||||
},
|
||||
error: {
|
||||
dot: "bg-coral-500",
|
||||
text: "text-coral-500",
|
||||
label: "Telegram Error",
|
||||
},
|
||||
offline: {
|
||||
dot: "bg-gray-500",
|
||||
text: "text-gray-500",
|
||||
label: "Telegram Offline",
|
||||
},
|
||||
setup_required: {
|
||||
dot: "bg-gray-500",
|
||||
text: "text-gray-500",
|
||||
label: "Telegram Not Set Up",
|
||||
dot: 'bg-amber-500',
|
||||
text: 'text-amber-500',
|
||||
label: 'Telegram Not Authenticated',
|
||||
},
|
||||
disconnected: { dot: 'bg-gray-500', text: 'text-gray-500', label: 'Telegram Disconnected' },
|
||||
error: { dot: 'bg-coral-500', text: 'text-coral-500', label: 'Telegram Error' },
|
||||
offline: { dot: 'bg-gray-500', text: 'text-gray-500', label: 'Telegram Offline' },
|
||||
setup_required: { dot: 'bg-gray-500', text: 'text-gray-500', label: 'Telegram Not Set Up' },
|
||||
};
|
||||
|
||||
const TelegramConnectionIndicator = ({
|
||||
className = "",
|
||||
}: TelegramConnectionIndicatorProps) => {
|
||||
const status = useSkillConnectionStatus("telegram");
|
||||
const TelegramConnectionIndicator = ({ className = '' }: TelegramConnectionIndicatorProps) => {
|
||||
const status = useSkillConnectionStatus('telegram');
|
||||
const config = STATUS_CONFIG[status];
|
||||
const isActive = status === "connected";
|
||||
const isActive = status === 'connected';
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<div
|
||||
className={`w-2 h-2 ${config.dot} rounded-full ${isActive ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
<div className={`w-2 h-2 ${config.dot} rounded-full ${isActive ? 'animate-pulse' : ''}`} />
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<img
|
||||
src={TelegramIcon}
|
||||
alt="Telegram"
|
||||
className={`w-4 h-4 ${isActive ? "opacity-100" : "opacity-50"}`}
|
||||
className={`w-4 h-4 ${isActive ? 'opacity-100' : 'opacity-50'}`}
|
||||
/>
|
||||
<span className={`text-sm ${config.text}`}>{config.label}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { TELEGRAM_BOT_USERNAME } from "../utils/config";
|
||||
import { openUrl } from "../utils/openUrl";
|
||||
import { isTauri } from "../utils/tauriCommands";
|
||||
|
||||
import { TELEGRAM_BOT_USERNAME } from '../utils/config';
|
||||
import { openUrl } from '../utils/openUrl';
|
||||
import { isTauri } from '../utils/tauriCommands';
|
||||
|
||||
interface TelegramLoginButtonProps {
|
||||
className?: string;
|
||||
@@ -10,14 +9,14 @@ interface TelegramLoginButtonProps {
|
||||
}
|
||||
|
||||
const TelegramLoginButton = ({
|
||||
className = "",
|
||||
text = "Yes, Login with Telegram",
|
||||
className = '',
|
||||
text = 'Yes, Login with Telegram',
|
||||
disabled: externalDisabled = false,
|
||||
}: TelegramLoginButtonProps) => {
|
||||
const handleTelegramLogin = async () => {
|
||||
if (externalDisabled) return;
|
||||
|
||||
console.log("Starting Telegram login", isTauri());
|
||||
console.log('Starting Telegram login', isTauri());
|
||||
|
||||
// Desktop (Tauri): use system browser → backend Telegram widget → deep link back to app.
|
||||
if (isTauri()) await openUrl(`https://t.me/${TELEGRAM_BOT_USERNAME}?start=login_desktop`);
|
||||
@@ -31,8 +30,7 @@ const TelegramLoginButton = ({
|
||||
<button
|
||||
onClick={handleTelegramLogin}
|
||||
disabled={isDisabled}
|
||||
className={`w-full flex items-center justify-center space-x-3 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed text-white font-semibold py-4 rounded-xl transition-all duration-300 hover:shadow-medium hover:scale-[1.02] active:scale-[0.98] disabled:hover:scale-100 ${className}`}
|
||||
>
|
||||
className={`w-full flex items-center justify-center space-x-3 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed text-white font-semibold py-4 rounded-xl transition-all duration-300 hover:shadow-medium hover:scale-[1.02] active:scale-[0.98] disabled:hover:scale-100 ${className}`}>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
</svg>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface TypewriterGreetingProps {
|
||||
greetings: string[];
|
||||
@@ -13,10 +13,10 @@ const TypewriterGreeting = ({
|
||||
typingSpeed = 100,
|
||||
deletingSpeed = 50,
|
||||
pauseTime = 2000,
|
||||
className = "",
|
||||
className = '',
|
||||
}: TypewriterGreetingProps) => {
|
||||
const [currentGreetingIndex, setCurrentGreetingIndex] = useState(0);
|
||||
const [displayedText, setDisplayedText] = useState("");
|
||||
const [displayedText, setDisplayedText] = useState('');
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
|
||||
@@ -50,7 +50,7 @@ const TypewriterGreeting = ({
|
||||
} else {
|
||||
// Finished deleting, move to next greeting
|
||||
setIsDeleting(false);
|
||||
setCurrentGreetingIndex((prev) => (prev + 1) % greetings.length);
|
||||
setCurrentGreetingIndex(prev => (prev + 1) % greetings.length);
|
||||
}
|
||||
}
|
||||
}, speed);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useAppDispatch } from "../../store/hooks";
|
||||
import { clearToken } from "../../store/authSlice";
|
||||
import { useSettingsNavigation } from "./hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "./components/SettingsHeader";
|
||||
import SettingsMenuItem from "./components/SettingsMenuItem";
|
||||
import { clearToken } from '../../store/authSlice';
|
||||
import { useAppDispatch } from '../../store/hooks';
|
||||
import SettingsHeader from './components/SettingsHeader';
|
||||
import SettingsMenuItem from './components/SettingsMenuItem';
|
||||
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
|
||||
|
||||
const SettingsHome = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -15,12 +15,12 @@ const SettingsHome = () => {
|
||||
|
||||
const handleViewEncryptionKey = () => {
|
||||
// TODO: Show encryption key in a secure modal
|
||||
console.log("View encryption key");
|
||||
console.log('View encryption key');
|
||||
};
|
||||
|
||||
const handleDeleteAllData = () => {
|
||||
// TODO: Show confirmation dialog and delete all data
|
||||
console.log("Delete all data");
|
||||
console.log('Delete all data');
|
||||
};
|
||||
|
||||
// Main settings menu items
|
||||
@@ -48,16 +48,11 @@ const SettingsHome = () => {
|
||||
// dangerous: false,
|
||||
// },
|
||||
{
|
||||
id: "privacy",
|
||||
title: "Privacy & Security",
|
||||
description: "Control your privacy and security settings",
|
||||
id: 'privacy',
|
||||
title: 'Privacy & Security',
|
||||
description: 'Control your privacy and security settings',
|
||||
icon: (
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -66,20 +61,15 @@ const SettingsHome = () => {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings("privacy"),
|
||||
onClick: () => navigateToSettings('privacy'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: "profile",
|
||||
title: "Profile",
|
||||
description: "Update your profile information and preferences",
|
||||
id: 'profile',
|
||||
title: 'Profile',
|
||||
description: 'Update your profile information and preferences',
|
||||
icon: (
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -88,20 +78,15 @@ const SettingsHome = () => {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings("profile"),
|
||||
onClick: () => navigateToSettings('profile'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: "advanced",
|
||||
title: "Advanced",
|
||||
description: "Advanced configuration and developer options",
|
||||
id: 'advanced',
|
||||
title: 'Advanced',
|
||||
description: 'Advanced configuration and developer options',
|
||||
icon: (
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -116,20 +101,15 @@ const SettingsHome = () => {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings("advanced"),
|
||||
onClick: () => navigateToSettings('advanced'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: "encryption",
|
||||
title: "View Encryption Key",
|
||||
description: "Access your encryption key for backup purposes",
|
||||
id: 'encryption',
|
||||
title: 'View Encryption Key',
|
||||
description: 'Access your encryption key for backup purposes',
|
||||
icon: (
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -142,16 +122,28 @@ const SettingsHome = () => {
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: "billing",
|
||||
title: "Billing",
|
||||
description: "Manage your subscription and payment methods",
|
||||
id: 'team',
|
||||
title: 'Team',
|
||||
description: 'Manage your team and invite members',
|
||||
icon: (
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('team'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'billing',
|
||||
title: 'Billing',
|
||||
description: 'Manage your subscription and payment methods',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -160,7 +152,7 @@ const SettingsHome = () => {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings("billing"),
|
||||
onClick: () => navigateToSettings('billing'),
|
||||
dangerous: false,
|
||||
},
|
||||
];
|
||||
@@ -168,16 +160,11 @@ const SettingsHome = () => {
|
||||
// Destructive actions menu items
|
||||
const destructiveMenuItems = [
|
||||
{
|
||||
id: "delete",
|
||||
title: "Delete All Data",
|
||||
description: "Permanently delete all your data and reset your account",
|
||||
id: 'delete',
|
||||
title: 'Delete All Data',
|
||||
description: 'Permanently delete all your data and reset your account',
|
||||
icon: (
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -190,16 +177,11 @@ const SettingsHome = () => {
|
||||
dangerous: true,
|
||||
},
|
||||
{
|
||||
id: "logout",
|
||||
title: "Log out",
|
||||
description: "Sign out of your account",
|
||||
id: 'logout',
|
||||
title: 'Log out',
|
||||
description: 'Sign out of your account',
|
||||
icon: (
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode, useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { ReactNode, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: ReactNode;
|
||||
@@ -12,13 +12,13 @@ const SettingsLayout = ({ children, onClose }: SettingsLayoutProps) => {
|
||||
// Handle escape key
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => document.removeEventListener('keydown', handleEscape);
|
||||
}, [onClose]);
|
||||
|
||||
// Handle backdrop click
|
||||
@@ -47,23 +47,21 @@ const SettingsLayout = ({ children, onClose }: SettingsLayoutProps) => {
|
||||
|
||||
const modalContent = (
|
||||
<div
|
||||
className="fixed inset-0 z-[9999] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
className="fixed inset-0 z-[9999] bg-black/50 backdrop-blur-lg flex items-center justify-center p-4"
|
||||
onClick={handleBackdropClick}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="settings-modal-title"
|
||||
>
|
||||
aria-labelledby="settings-modal-title">
|
||||
<div
|
||||
ref={modalRef}
|
||||
className="glass rounded-3xl shadow-large w-full max-w-[520px] h-[800px] overflow-hidden animate-slide-right focus:outline-none focus:ring-0"
|
||||
className="bg-stone-900 rounded-3xl shadow-large w-full max-w-[520px] max-h-[90vh] overflow-hidden animate-slide-right focus:outline-none focus:ring-0"
|
||||
style={{
|
||||
animationDuration: "300ms",
|
||||
animationTimingFunction: "cubic-bezier(0.25, 0.46, 0.45, 0.94)",
|
||||
animationFillMode: "both",
|
||||
animationDuration: '300ms',
|
||||
animationTimingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
animationFillMode: 'both',
|
||||
}}
|
||||
tabIndex={-1}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
onClick={e => e.stopPropagation()}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import SettingsLayout from "./SettingsLayout";
|
||||
import SettingsHome from "./SettingsHome";
|
||||
import ConnectionsPanel from "./panels/ConnectionsPanel";
|
||||
import MessagingPanel from "./panels/MessagingPanel";
|
||||
import PrivacyPanel from "./panels/PrivacyPanel";
|
||||
import ProfilePanel from "./panels/ProfilePanel";
|
||||
import AdvancedPanel from "./panels/AdvancedPanel";
|
||||
import BillingPanel from "./panels/BillingPanel";
|
||||
import { useSettingsNavigation } from "./hooks/useSettingsNavigation";
|
||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
|
||||
import AdvancedPanel from './panels/AdvancedPanel';
|
||||
import BillingPanel from './panels/BillingPanel';
|
||||
import ConnectionsPanel from './panels/ConnectionsPanel';
|
||||
import MessagingPanel from './panels/MessagingPanel';
|
||||
import PrivacyPanel from './panels/PrivacyPanel';
|
||||
import ProfilePanel from './panels/ProfilePanel';
|
||||
import TeamInvitesPanel from './panels/TeamInvitesPanel';
|
||||
import TeamMembersPanel from './panels/TeamMembersPanel';
|
||||
import TeamPanel from './panels/TeamPanel';
|
||||
import SettingsHome from './SettingsHome';
|
||||
import SettingsLayout from './SettingsLayout';
|
||||
|
||||
const SettingsModal = () => {
|
||||
const location = useLocation();
|
||||
const { closeSettings } = useSettingsNavigation();
|
||||
|
||||
// Only render modal when on settings routes
|
||||
const isSettingsRoute = location.pathname.startsWith("/settings");
|
||||
const isSettingsRoute = location.pathname.startsWith('/settings');
|
||||
|
||||
if (!isSettingsRoute) {
|
||||
return null;
|
||||
@@ -31,6 +34,9 @@ const SettingsModal = () => {
|
||||
<Route path="/settings/profile" element={<ProfilePanel />} />
|
||||
<Route path="/settings/advanced" element={<AdvancedPanel />} />
|
||||
<Route path="/settings/billing" element={<BillingPanel />} />
|
||||
<Route path="/settings/team" element={<TeamPanel />} />
|
||||
<Route path="/settings/team/members" element={<TeamMembersPanel />} />
|
||||
<Route path="/settings/team/invites" element={<TeamInvitesPanel />} />
|
||||
</Routes>
|
||||
</SettingsLayout>
|
||||
);
|
||||
|
||||
@@ -6,28 +6,17 @@ interface SettingsBackButtonProps {
|
||||
|
||||
const SettingsBackButton = ({
|
||||
onClick,
|
||||
title = "Settings",
|
||||
className = "",
|
||||
title = 'Settings',
|
||||
className = '',
|
||||
}: SettingsBackButtonProps) => {
|
||||
return (
|
||||
<div className={`bg-black/30 border-b border-stone-700 p-6 ${className}`}>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="flex items-center space-x-3 text-white hover:text-stone-300 transition-colors duration-150"
|
||||
aria-label="Go back"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
aria-label="Go back">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span className="text-lg font-semibold">{title}</span>
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
interface SettingsHeaderProps {
|
||||
className?: string;
|
||||
@@ -8,17 +8,15 @@ interface SettingsHeaderProps {
|
||||
}
|
||||
|
||||
const SettingsHeader = ({
|
||||
className = "",
|
||||
title = "Settings",
|
||||
className = '',
|
||||
title = 'Settings',
|
||||
showBackButton = false,
|
||||
onBack,
|
||||
}: SettingsHeaderProps) => {
|
||||
const { closeSettings } = useSettingsNavigation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-black/30 border-b border-stone-700 p-6 relative ${className}`}
|
||||
>
|
||||
<div className={`bg-black/30 border-b border-stone-700 py-3 px-4 relative ${className}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{/* Back button */}
|
||||
@@ -26,14 +24,12 @@ const SettingsHeader = ({
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-800/50 transition-colors mr-3"
|
||||
aria-label="Go back"
|
||||
>
|
||||
aria-label="Go back">
|
||||
<svg
|
||||
className="w-5 h-5 opacity-70"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -45,10 +41,7 @@ const SettingsHeader = ({
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<h2
|
||||
className="text-lg font-semibold text-white"
|
||||
id="settings-modal-title"
|
||||
>
|
||||
<h2 className="text-lg font-semibold text-white" id="settings-modal-title">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -57,14 +50,8 @@ const SettingsHeader = ({
|
||||
<button
|
||||
onClick={closeSettings}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-800/50 transition-colors"
|
||||
aria-label="Close settings"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 opacity-70"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
aria-label="Close settings">
|
||||
<svg className="w-5 h-5 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode } from "react";
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface SettingsMenuItemProps {
|
||||
icon: ReactNode;
|
||||
@@ -20,26 +20,19 @@ const SettingsMenuItem = ({
|
||||
isLast = false,
|
||||
}: SettingsMenuItemProps) => {
|
||||
// Color variations for dangerous items (like logout/delete)
|
||||
const titleColor = dangerous ? "text-amber-400" : "text-white";
|
||||
const iconColor = dangerous ? "text-amber-400" : "text-white";
|
||||
const borderColor = "border-stone-700"; // Use consistent border color for all items
|
||||
const titleColor = dangerous ? 'text-amber-400' : 'text-white';
|
||||
const iconColor = dangerous ? 'text-amber-400' : 'text-white';
|
||||
const borderColor = 'border-stone-700'; // Use consistent border color for all items
|
||||
|
||||
// Border classes for first/last items
|
||||
const borderClasses = isLast ? "" : `border-b ${borderColor}`;
|
||||
const roundedClasses = isFirst
|
||||
? "first:rounded-t-3xl"
|
||||
: isLast
|
||||
? "last:rounded-b-3xl"
|
||||
: "";
|
||||
const borderClasses = isLast ? '' : `border-b ${borderColor}`;
|
||||
const roundedClasses = isFirst ? 'first:rounded-t-3xl' : isLast ? 'last:rounded-b-3xl' : '';
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`w-full flex items-center justify-between p-3 bg-black/50 ${borderClasses} hover:bg-stone-800/30 transition-all duration-200 text-left ${roundedClasses} focus:outline-none focus:ring-0 focus:border-inherit`}
|
||||
>
|
||||
<div className={`w-5 h-5 opacity-60 flex-shrink-0 mr-3 ${iconColor}`}>
|
||||
{icon}
|
||||
</div>
|
||||
className={`w-full flex items-center justify-between p-3 bg-black/50 ${borderClasses} hover:bg-stone-800/30 transition-all duration-200 text-left ${roundedClasses} focus:outline-none focus:ring-0 focus:border-inherit`}>
|
||||
<div className={`w-5 h-5 opacity-60 flex-shrink-0 mr-3 ${iconColor}`}>{icon}</div>
|
||||
<div className="flex-1">
|
||||
<div className={`font-medium text-sm mb-1 ${titleColor}`}>{title}</div>
|
||||
{description && <p className="opacity-70 text-xs">{description}</p>}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
import SettingsBackButton from "./SettingsBackButton";
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import SettingsBackButton from './SettingsBackButton';
|
||||
|
||||
interface SettingsPanelLayoutProps {
|
||||
title: string;
|
||||
@@ -12,12 +13,10 @@ const SettingsPanelLayout = ({
|
||||
title,
|
||||
onBack,
|
||||
children,
|
||||
className = "",
|
||||
className = '',
|
||||
}: SettingsPanelLayoutProps) => {
|
||||
return (
|
||||
<div
|
||||
className={`glass rounded-3xl overflow-hidden h-[600px] flex flex-col ${className}`}
|
||||
>
|
||||
<div className={`glass rounded-3xl overflow-hidden h-[600px] flex flex-col ${className}`}>
|
||||
<SettingsBackButton onClick={onBack} title={title} />
|
||||
<div className="flex-1 overflow-y-auto">{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export type AnimationState = "entering" | "entered" | "exiting" | "exited";
|
||||
export type AnimationState = 'entering' | 'entered' | 'exiting' | 'exited';
|
||||
|
||||
interface SettingsAnimationHook {
|
||||
isVisible: boolean;
|
||||
@@ -10,32 +10,25 @@ interface SettingsAnimationHook {
|
||||
}
|
||||
|
||||
export const useSettingsAnimation = (duration = 300): SettingsAnimationHook => {
|
||||
const [animationState, setAnimationState] =
|
||||
useState<AnimationState>("exited");
|
||||
const [animationState, setAnimationState] = useState<AnimationState>('exited');
|
||||
|
||||
const isVisible =
|
||||
animationState === "entering" || animationState === "entered";
|
||||
const isVisible = animationState === 'entering' || animationState === 'entered';
|
||||
|
||||
const startEntry = () => {
|
||||
setAnimationState("entering");
|
||||
setAnimationState('entering');
|
||||
setTimeout(() => {
|
||||
setAnimationState("entered");
|
||||
setAnimationState('entered');
|
||||
}, duration);
|
||||
};
|
||||
|
||||
const startExit = () => {
|
||||
setAnimationState("exiting");
|
||||
setAnimationState('exiting');
|
||||
setTimeout(() => {
|
||||
setAnimationState("exited");
|
||||
setAnimationState('exited');
|
||||
}, duration);
|
||||
};
|
||||
|
||||
return {
|
||||
isVisible,
|
||||
animationState,
|
||||
startEntry,
|
||||
startExit,
|
||||
};
|
||||
return { isVisible, animationState, startEntry, startExit };
|
||||
};
|
||||
|
||||
// Hook for panel slide animations (slide from right)
|
||||
@@ -52,8 +45,7 @@ export const usePanelAnimation = (isActive: boolean, duration = 300) => {
|
||||
}, [isActive, duration]);
|
||||
|
||||
const getPanelClasses = () => {
|
||||
const baseClasses =
|
||||
"transition-all duration-300 ease-[cubic-bezier(0.25,0.46,0.45,0.94)]";
|
||||
const baseClasses = 'transition-all duration-300 ease-[cubic-bezier(0.25,0.46,0.45,0.94)]';
|
||||
if (!mounted) return `${baseClasses} opacity-0`;
|
||||
|
||||
return isActive
|
||||
@@ -61,8 +53,5 @@ export const usePanelAnimation = (isActive: boolean, duration = 300) => {
|
||||
: `${baseClasses} opacity-0 translate-x-full`;
|
||||
};
|
||||
|
||||
return {
|
||||
mounted,
|
||||
panelClasses: getPanelClasses(),
|
||||
};
|
||||
return { mounted, panelClasses: getPanelClasses() };
|
||||
};
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export type SettingsRoute =
|
||||
| "home"
|
||||
| "connections"
|
||||
| "messaging"
|
||||
| "privacy"
|
||||
| "profile"
|
||||
| "advanced"
|
||||
| "billing";
|
||||
| 'home'
|
||||
| 'connections'
|
||||
| 'messaging'
|
||||
| 'privacy'
|
||||
| 'profile'
|
||||
| 'advanced'
|
||||
| 'billing'
|
||||
| 'team'
|
||||
| 'team-members'
|
||||
| 'team-invites';
|
||||
|
||||
interface SettingsNavigationHook {
|
||||
currentRoute: SettingsRoute;
|
||||
@@ -24,44 +27,44 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
|
||||
// Determine current settings route from URL
|
||||
const getCurrentRoute = (): SettingsRoute => {
|
||||
const path = location.pathname;
|
||||
if (path.includes("/settings/connections")) return "connections";
|
||||
if (path.includes("/settings/messaging")) return "messaging";
|
||||
if (path.includes("/settings/privacy")) return "privacy";
|
||||
if (path.includes("/settings/profile")) return "profile";
|
||||
if (path.includes("/settings/advanced")) return "advanced";
|
||||
if (path.includes("/settings/billing")) return "billing";
|
||||
return "home";
|
||||
if (path.includes('/settings/team/members')) return 'team-members';
|
||||
if (path.includes('/settings/team/invites')) return 'team-invites';
|
||||
if (path.includes('/settings/team')) return 'team';
|
||||
if (path.includes('/settings/connections')) return 'connections';
|
||||
if (path.includes('/settings/messaging')) return 'messaging';
|
||||
if (path.includes('/settings/privacy')) return 'privacy';
|
||||
if (path.includes('/settings/profile')) return 'profile';
|
||||
if (path.includes('/settings/advanced')) return 'advanced';
|
||||
if (path.includes('/settings/billing')) return 'billing';
|
||||
return 'home';
|
||||
};
|
||||
|
||||
const currentRoute = getCurrentRoute();
|
||||
|
||||
const navigateToSettings = useCallback(
|
||||
(route: SettingsRoute = "home") => {
|
||||
if (route === "home") {
|
||||
navigate("/settings");
|
||||
(route: SettingsRoute = 'home') => {
|
||||
if (route === 'home') {
|
||||
navigate('/settings');
|
||||
} else {
|
||||
navigate(`/settings/${route}`);
|
||||
}
|
||||
},
|
||||
[navigate],
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const navigateBack = useCallback(() => {
|
||||
if (currentRoute === "home") {
|
||||
navigate("/home");
|
||||
if (currentRoute === 'home') {
|
||||
navigate('/home');
|
||||
} else if (currentRoute === 'team-members' || currentRoute === 'team-invites') {
|
||||
navigate('/settings/team');
|
||||
} else {
|
||||
navigate("/settings");
|
||||
navigate('/settings');
|
||||
}
|
||||
}, [navigate, currentRoute]);
|
||||
|
||||
const closeSettings = useCallback(() => {
|
||||
navigate("/home");
|
||||
navigate('/home');
|
||||
}, [navigate]);
|
||||
|
||||
return {
|
||||
currentRoute,
|
||||
navigateToSettings,
|
||||
navigateBack,
|
||||
closeSettings,
|
||||
};
|
||||
return { currentRoute, navigateToSettings, navigateBack, closeSettings };
|
||||
};
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
const AdvancedPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden h-full flex flex-col">
|
||||
<SettingsHeader
|
||||
title="Advanced"
|
||||
showBackButton={true}
|
||||
onBack={navigateBack}
|
||||
/>
|
||||
<SettingsHeader title="Advanced" showBackButton={true} onBack={navigateBack} />
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="p-4 h-full flex items-center justify-center">
|
||||
@@ -20,8 +16,7 @@ const AdvancedPanel = () => {
|
||||
className="w-8 h-8 text-stone-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -36,12 +31,9 @@ const AdvancedPanel = () => {
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Advanced Settings
|
||||
</h3>
|
||||
<h3 className="text-lg font-medium text-white mb-2">Advanced Settings</h3>
|
||||
<p className="text-stone-400 text-sm max-w-sm mx-auto">
|
||||
Configure advanced features, developer options, and system-level
|
||||
settings.
|
||||
Configure advanced features, developer options, and system-level settings.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-700/30 text-stone-300 border-stone-600/50">
|
||||
|
||||
@@ -1,60 +1,59 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useAppSelector, useAppDispatch } from "../../../store/hooks";
|
||||
import { fetchCurrentUser } from "../../../store/userSlice";
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
import { billingApi } from "../../../services/api/billingApi";
|
||||
import { openUrl } from "../../../utils/openUrl";
|
||||
import type { CurrentPlanData, PlanTier } from "../../../types/api";
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { billingApi } from '../../../services/api/billingApi';
|
||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||
import { fetchCurrentUser } from '../../../store/userSlice';
|
||||
import type { PlanTier } from '../../../types/api';
|
||||
import { openUrl } from '../../../utils/openUrl';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import {
|
||||
PLANS,
|
||||
buildPlanId,
|
||||
displayPrice,
|
||||
annualSavings,
|
||||
buildPlanId,
|
||||
isUpgrade as checkIsUpgrade,
|
||||
} from "./billingHelpers";
|
||||
displayPrice,
|
||||
PLANS,
|
||||
} from './billingHelpers';
|
||||
|
||||
// ── Component ───────────────────────────────────────────────────────────
|
||||
const BillingPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
const dispatch = useAppDispatch();
|
||||
const user = useAppSelector((state) => state.user.user);
|
||||
const user = useAppSelector(state => state.user.user);
|
||||
const { teams } = useAppSelector(state => state.team);
|
||||
|
||||
// Derived from Redux user state
|
||||
const currentTier: PlanTier = user?.subscription?.plan ?? "FREE";
|
||||
const hasActive = user?.subscription?.hasActiveSubscription ?? false;
|
||||
const planExpiry = user?.subscription?.planExpiry;
|
||||
const usage = user?.usage;
|
||||
// Active team context
|
||||
const activeTeamId = user?.activeTeamId;
|
||||
const activeTeam = teams.find(t => t.team._id === activeTeamId);
|
||||
const teamName = activeTeam?.team.name;
|
||||
|
||||
// Derive plan from active team when available, fall back to user
|
||||
const currentTier: PlanTier =
|
||||
activeTeam?.team.subscription?.plan ?? user?.subscription?.plan ?? 'FREE';
|
||||
const hasActive =
|
||||
activeTeam?.team.subscription?.hasActiveSubscription ??
|
||||
user?.subscription?.hasActiveSubscription ??
|
||||
false;
|
||||
const planExpiry = activeTeam?.team.subscription?.planExpiry ?? user?.subscription?.planExpiry;
|
||||
const usage = activeTeam?.team.usage ?? user?.usage;
|
||||
|
||||
// Local state
|
||||
const [billingInterval, setBillingInterval] = useState<"monthly" | "annual">(
|
||||
"monthly",
|
||||
);
|
||||
const [paymentMethod, setPaymentMethod] = useState<"card" | "crypto">(
|
||||
"card",
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [billingInterval, setBillingInterval] = useState<'monthly' | 'annual'>('monthly');
|
||||
const [paymentMethod, setPaymentMethod] = useState<'card' | 'crypto'>('card');
|
||||
const [isPurchasing, setIsPurchasing] = useState(false);
|
||||
const [purchasingTier, setPurchasingTier] = useState<PlanTier | null>(null);
|
||||
const [currentPlanData, setCurrentPlanData] =
|
||||
useState<CurrentPlanData | null>(null);
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const pollStartRef = useRef<number>(0);
|
||||
|
||||
// Fetch current plan on mount
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
billingApi
|
||||
.getCurrentPlan()
|
||||
.then(setCurrentPlanData)
|
||||
.catch(console.error)
|
||||
.finally(() => setIsLoading(false));
|
||||
billingApi.getCurrentPlan().catch(console.error);
|
||||
}, []);
|
||||
|
||||
// When crypto is selected, force annual
|
||||
useEffect(() => {
|
||||
if (paymentMethod === "crypto") {
|
||||
setBillingInterval("annual");
|
||||
if (paymentMethod === 'crypto') {
|
||||
setBillingInterval('annual');
|
||||
}
|
||||
}, [paymentMethod]);
|
||||
|
||||
@@ -66,6 +65,12 @@ const BillingPanel = () => {
|
||||
}, []);
|
||||
|
||||
// ── Poll for plan change after checkout ─────────────────────────────
|
||||
const currentTierRef = useRef(currentTier);
|
||||
useEffect(() => {
|
||||
currentTierRef.current = currentTier;
|
||||
}, [currentTier]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
||||
const startPolling = useCallback(() => {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
pollStartRef.current = Date.now();
|
||||
@@ -81,11 +86,7 @@ const BillingPanel = () => {
|
||||
|
||||
try {
|
||||
const plan = await billingApi.getCurrentPlan();
|
||||
if (
|
||||
plan.hasActiveSubscription &&
|
||||
plan.plan !== currentTier
|
||||
) {
|
||||
setCurrentPlanData(plan);
|
||||
if (plan.hasActiveSubscription && plan.plan !== currentTierRef.current) {
|
||||
dispatch(fetchCurrentUser());
|
||||
setIsPurchasing(false);
|
||||
setPurchasingTier(null);
|
||||
@@ -95,20 +96,17 @@ const BillingPanel = () => {
|
||||
// Ignore polling errors
|
||||
}
|
||||
}, 5_000);
|
||||
}, [currentTier, dispatch]);
|
||||
}, [dispatch]);
|
||||
|
||||
// ── Purchase handlers ───────────────────────────────────────────────
|
||||
const handleUpgrade = async (tier: PlanTier) => {
|
||||
if (tier === "FREE" || tier === currentTier) return;
|
||||
if (tier === 'FREE' || tier === currentTier) return;
|
||||
setIsPurchasing(true);
|
||||
setPurchasingTier(tier);
|
||||
|
||||
try {
|
||||
if (paymentMethod === "crypto") {
|
||||
const { hostedUrl } = await billingApi.createCoinbaseCharge(
|
||||
tier,
|
||||
"annual",
|
||||
);
|
||||
if (paymentMethod === 'crypto') {
|
||||
const { hostedUrl } = await billingApi.createCoinbaseCharge(tier, 'annual');
|
||||
await openUrl(hostedUrl);
|
||||
} else {
|
||||
const planId = buildPlanId(tier, billingInterval);
|
||||
@@ -117,7 +115,7 @@ const BillingPanel = () => {
|
||||
}
|
||||
startPolling();
|
||||
} catch (err) {
|
||||
console.error("Purchase failed:", err);
|
||||
console.error('Purchase failed:', err);
|
||||
setIsPurchasing(false);
|
||||
setPurchasingTier(null);
|
||||
}
|
||||
@@ -128,140 +126,136 @@ const BillingPanel = () => {
|
||||
const { portalUrl } = await billingApi.createPortalSession();
|
||||
await openUrl(portalUrl);
|
||||
} catch (err) {
|
||||
console.error("Portal session failed:", err);
|
||||
console.error('Portal session failed:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// ── JSX ─────────────────────────────────────────────────────────────
|
||||
return (
|
||||
<div className="overflow-hidden h-full flex flex-col">
|
||||
<div className="overflow-hidden flex flex-col">
|
||||
<SettingsHeader
|
||||
title="Billing & Subscription"
|
||||
title={teamName ? `Billing — ${teamName}` : 'Billing & Subscription'}
|
||||
showBackButton={true}
|
||||
onBack={navigateBack}
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="p-4 space-y-5">
|
||||
<div className="overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
{/* ── Current plan banner ──────────────────────────────── */}
|
||||
<div className="rounded-2xl bg-stone-800/60 border border-stone-700/50 p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-white">
|
||||
{isLoading
|
||||
? "Loading..."
|
||||
: (currentPlanData?.plan ?? currentTier)}{" "}
|
||||
Plan
|
||||
</h3>
|
||||
<span
|
||||
className={`px-2 py-0.5 text-xs font-medium rounded-full ${
|
||||
hasActive
|
||||
? "bg-sage-500/20 text-sage-400 border border-sage-500/30"
|
||||
: "bg-stone-600/30 text-stone-400 border border-stone-600/40"
|
||||
}`}
|
||||
>
|
||||
{hasActive ? "Active" : "Free"}
|
||||
<div className="bg-stone-800/60 p-2.5">
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<h3 className="text-sm font-semibold text-white">Your Current Plan {currentTier}</h3>
|
||||
{usage && (
|
||||
<span className="text-xs text-stone-400">
|
||||
{Math.round(
|
||||
((usage.dailyTokenLimit - usage.remainingTokens) / usage.dailyTokenLimit) * 100
|
||||
)}
|
||||
% used
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{hasActive && (
|
||||
<button
|
||||
onClick={handleManageSubscription}
|
||||
className="text-xs text-primary-400 hover:text-primary-300 font-medium transition-colors"
|
||||
>
|
||||
Manage Subscription
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Renewal date */}
|
||||
{hasActive && planExpiry && (
|
||||
<p className="text-xs text-stone-400 mb-3">
|
||||
Renews{" "}
|
||||
{new Date(planExpiry).toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
{hasActive && (
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
{planExpiry && (
|
||||
<p className="text-xs text-stone-400">
|
||||
Renews{' '}
|
||||
{new Date(planExpiry).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
onClick={handleManageSubscription}
|
||||
className="text-xs text-primary-400 hover:text-primary-300 font-medium transition-colors">
|
||||
Manage Subscription
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Renewal date (for non-active subscriptions) */}
|
||||
{!hasActive && planExpiry && (
|
||||
<p className="text-xs text-stone-400 mb-1.5">
|
||||
Renews{' '}
|
||||
{new Date(planExpiry).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Token usage */}
|
||||
{/* Token usage progress bar */}
|
||||
{usage && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs text-stone-400 mb-1.5">
|
||||
<span>Daily token usage</span>
|
||||
<span>
|
||||
{usage.remainingTokens.toLocaleString()} /{" "}
|
||||
{usage.dailyTokenLimit.toLocaleString()} remaining
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-stone-700/60 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300 bg-primary-500"
|
||||
style={{
|
||||
width: `${Math.min(
|
||||
100,
|
||||
(usage.remainingTokens / usage.dailyTokenLimit) * 100,
|
||||
)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-1.5 bg-stone-700/60 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300 bg-primary-500"
|
||||
style={{
|
||||
width: `${Math.min(
|
||||
100,
|
||||
((usage.dailyTokenLimit - usage.remainingTokens) / usage.dailyTokenLimit) *
|
||||
100
|
||||
)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Interval toggle ──────────────────────────────────── */}
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="flex items-center justify-center gap-2 px-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (paymentMethod !== "crypto") setBillingInterval("monthly");
|
||||
if (paymentMethod !== 'crypto') setBillingInterval('monthly');
|
||||
}}
|
||||
disabled={paymentMethod === "crypto"}
|
||||
disabled={paymentMethod === 'crypto'}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors ${
|
||||
billingInterval === "monthly"
|
||||
? "bg-primary-500/20 text-primary-400 border border-primary-500/30"
|
||||
: "text-stone-400 hover:text-stone-300"
|
||||
} ${paymentMethod === "crypto" ? "opacity-40 cursor-not-allowed" : ""}`}
|
||||
>
|
||||
billingInterval === 'monthly'
|
||||
? 'bg-primary-500/20 text-primary-400 border border-primary-500/30'
|
||||
: 'text-stone-400 hover:text-stone-300'
|
||||
} ${paymentMethod === 'crypto' ? 'opacity-40 cursor-not-allowed' : ''}`}>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setBillingInterval("annual")}
|
||||
onClick={() => setBillingInterval('annual')}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors ${
|
||||
billingInterval === "annual"
|
||||
? "bg-primary-500/20 text-primary-400 border border-primary-500/30"
|
||||
: "text-stone-400 hover:text-stone-300"
|
||||
}`}
|
||||
>
|
||||
billingInterval === 'annual'
|
||||
? 'bg-primary-500/20 text-primary-400 border border-primary-500/30'
|
||||
: 'text-stone-400 hover:text-stone-300'
|
||||
}`}>
|
||||
Annual
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Plan tier cards ───────────────────────────────────── */}
|
||||
<div className="space-y-3">
|
||||
{PLANS.map((plan) => {
|
||||
<div className="space-y-2 px-4">
|
||||
{PLANS.map(plan => {
|
||||
const isCurrent = plan.tier === currentTier;
|
||||
const isUpgrade = checkIsUpgrade(plan.tier, currentTier);
|
||||
const savings = annualSavings(plan, billingInterval);
|
||||
const isThisPurchasing =
|
||||
isPurchasing && purchasingTier === plan.tier;
|
||||
const isThisPurchasing = isPurchasing && purchasingTier === plan.tier;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={plan.tier}
|
||||
className={`rounded-2xl border p-4 transition-all ${
|
||||
className={`rounded-2xl border p-3 transition-all ${
|
||||
isCurrent
|
||||
? "border-primary-500/40 bg-primary-500/5"
|
||||
: "border-stone-700/50 bg-stone-800/40"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-semibold text-white">
|
||||
{plan.name}
|
||||
</h4>
|
||||
? 'border-primary-500/40 bg-primary-500/5'
|
||||
: 'border-stone-700/50 bg-stone-800/40'
|
||||
}`}>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h4 className="text-sm font-semibold text-white">{plan.name}</h4>
|
||||
{/* Features inline with title */}
|
||||
{plan.features.map(f => (
|
||||
<span key={f.text} className="text-xs text-stone-300">
|
||||
<span className="text-stone-500 mx-1">•</span>
|
||||
{f.text}
|
||||
</span>
|
||||
))}
|
||||
{isCurrent && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded-full bg-primary-500/20 text-primary-400 border border-primary-500/30">
|
||||
Current
|
||||
@@ -273,19 +267,18 @@ const BillingPanel = () => {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 flex items-baseline gap-1">
|
||||
<div className="mt-0.5 flex items-baseline gap-1">
|
||||
<span className="text-xl font-bold text-white">
|
||||
{displayPrice(plan, billingInterval)}
|
||||
</span>
|
||||
{plan.tier !== "FREE" && (
|
||||
{plan.tier !== 'FREE' && (
|
||||
<span className="text-xs text-stone-400">/mo</span>
|
||||
)}
|
||||
{plan.tier !== "FREE" &&
|
||||
billingInterval === "annual" && (
|
||||
<span className="text-xs text-stone-500 ml-1">
|
||||
(billed ${plan.annualPrice}/yr)
|
||||
</span>
|
||||
)}
|
||||
{plan.tier !== 'FREE' && billingInterval === 'annual' && (
|
||||
<span className="text-xs text-stone-500 ml-1">
|
||||
(billed ${plan.annualPrice}/yr)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -294,41 +287,15 @@ const BillingPanel = () => {
|
||||
<button
|
||||
onClick={() => handleUpgrade(plan.tier)}
|
||||
disabled={isPurchasing}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors ${
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors flex-shrink-0 ${
|
||||
isPurchasing
|
||||
? "bg-stone-700/40 text-stone-500 cursor-not-allowed"
|
||||
: "bg-primary-500 hover:bg-primary-600 text-white"
|
||||
}`}
|
||||
>
|
||||
{isThisPurchasing ? "Waiting..." : "Upgrade"}
|
||||
? 'bg-stone-700/40 text-stone-500 cursor-not-allowed'
|
||||
: 'bg-primary-500 hover:bg-primary-600 text-white'
|
||||
}`}>
|
||||
{isThisPurchasing ? 'Waiting...' : 'Upgrade'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<ul className="space-y-1.5">
|
||||
{plan.features.map((f) => (
|
||||
<li
|
||||
key={f.text}
|
||||
className="flex items-center gap-2 text-xs text-stone-300"
|
||||
>
|
||||
<svg
|
||||
className="w-3.5 h-3.5 text-sage-500 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{f.text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -336,13 +303,12 @@ const BillingPanel = () => {
|
||||
|
||||
{/* ── Purchasing overlay message ────────────────────────── */}
|
||||
{isPurchasing && (
|
||||
<div className="rounded-xl bg-amber-500/10 border border-amber-500/20 p-3">
|
||||
<div className="rounded-xl bg-amber-500/10 border border-amber-500/20 p-3 mx-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-4 h-4 text-amber-400 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
@@ -358,65 +324,76 @@ const BillingPanel = () => {
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-xs text-amber-300">
|
||||
Waiting for payment confirmation... Complete checkout in the
|
||||
browser window that opened.
|
||||
Waiting for payment confirmation... Complete checkout in the browser window that
|
||||
opened.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Pay with crypto toggle ────────────────────────────── */}
|
||||
<div className="flex items-center justify-between rounded-xl bg-stone-800/40 border border-stone-700/40 p-3">
|
||||
<div className="flex items-center justify-between rounded-xl bg-stone-800/40 border border-stone-700/40 p-3 mx-4">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-white">
|
||||
Pay with Crypto
|
||||
</p>
|
||||
<p className="text-xs font-medium text-white">Pay with Crypto</p>
|
||||
<p className="text-[11px] text-stone-400 mt-0.5">
|
||||
Annual plans only via Coinbase Commerce
|
||||
You can choose to pay annually using crypto
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
setPaymentMethod((m) => (m === "card" ? "crypto" : "card"))
|
||||
}
|
||||
onClick={() => setPaymentMethod(m => (m === 'card' ? 'crypto' : 'card'))}
|
||||
className={`relative w-10 h-5 rounded-full transition-colors ${
|
||||
paymentMethod === "crypto"
|
||||
? "bg-primary-500"
|
||||
: "bg-stone-600"
|
||||
paymentMethod === 'crypto' ? 'bg-primary-500' : 'bg-stone-600'
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={paymentMethod === "crypto"}
|
||||
>
|
||||
aria-checked={paymentMethod === 'crypto'}>
|
||||
<span
|
||||
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
|
||||
paymentMethod === "crypto"
|
||||
? "translate-x-5"
|
||||
: "translate-x-0"
|
||||
paymentMethod === 'crypto' ? 'translate-x-5' : 'translate-x-0'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Info notice ───────────────────────────────────────── */}
|
||||
<div className="p-3 bg-blue-500/10 border border-blue-500/20 rounded-xl">
|
||||
<div className="flex items-start gap-2">
|
||||
<svg
|
||||
className="w-4 h-4 text-blue-400 flex-shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-[11px] text-blue-200">
|
||||
Payments processed securely through Stripe. Crypto payments
|
||||
available for annual plans via Coinbase.
|
||||
</p>
|
||||
{/* ── Upgrade benefits ───────────────────────────────────── */}
|
||||
<div className="px-4 pb-4 pt-2">
|
||||
<div className="rounded-xl bg-gradient-to-br from-primary-500/10 to-sage-500/10 border border-primary-500/20 p-4">
|
||||
<h3 className="text-sm font-semibold text-white mb-2">Why upgrade?</h3>
|
||||
<ul className="space-y-1.5 text-xs text-stone-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<svg
|
||||
className="w-4 h-4 text-sage-400 flex-shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span>Unlock higher daily limits for more AI interactions</span>
|
||||
</li>
|
||||
{currentTier === 'FREE' && (
|
||||
<li className="flex items-start gap-2">
|
||||
<svg
|
||||
className="w-4 h-4 text-sage-400 flex-shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Save up to 20% with annual plans and never worry about hitting limits
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user