Merge pull request #14 from vezuresdotxyz/develop

Develop
This commit is contained in:
Steven Enamakel
2026-02-02 06:27:27 +05:30
committed by GitHub
232 changed files with 279207 additions and 5902 deletions
+7 -7
View File
@@ -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
+9 -4
View File
@@ -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();
}
```
+9 -6
View File
@@ -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' });
```
+16 -1
View File
@@ -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
+9 -15
View File
@@ -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
}
```
+11
View File
@@ -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
+24 -1
View File
@@ -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
```
```
+21 -1
View File
@@ -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
```
```
+13 -1
View File
@@ -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
+22 -26
View File
@@ -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]
+2 -1
View File
@@ -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_
+24 -23
View File
@@ -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/
+4 -7
View File
@@ -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
+2 -7
View File
@@ -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"
```
+4 -4
View File
@@ -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();
+23 -40
View File
@@ -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;
}
+30 -47
View File
@@ -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
+15 -6
View File
@@ -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
+13 -1
View File
@@ -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_
+31 -1
View File
@@ -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._
+10 -25
View File
@@ -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.
+4 -14
View File
@@ -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"] }
```
---
+25 -9
View File
@@ -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 -2
View File
@@ -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
+21 -15
View File
@@ -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: |
+3 -3
View File
@@ -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;
}
+10
View File
@@ -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
+2
View File
@@ -32,3 +32,5 @@ dist-ssr
*.sw?
references/
src-tauri/runtime-skill-*
.mypy_cache
.ruff_cache
View File
+37
View File
@@ -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
+1 -3
View File
@@ -1,3 +1 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}
{ "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] }
+122 -119
View File
@@ -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
---
+2 -8
View File
@@ -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
View File
@@ -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.
+46 -33
View File
@@ -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
View File
@@ -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
View File
@@ -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)_
+7 -7
View File
@@ -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();
+6 -4
View File
@@ -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)_
+13 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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_
+5 -13
View File
@@ -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
View File
@@ -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
View File
@@ -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",
File diff suppressed because one or more lines are too long
+195606 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+322 -1
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+15906 -1
View File
File diff suppressed because one or more lines are too long
+9431 -1
View File
File diff suppressed because one or more lines are too long
+438 -1
View File
File diff suppressed because one or more lines are too long
-139
View File
@@ -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"
}
]
}
+14 -15
View File
@@ -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();
+46 -2
View File
@@ -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"
+4
View File
@@ -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"
-1
View File
@@ -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;
+360 -1
View File
@@ -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)
}
+5
View File
@@ -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")
+3
View File
@@ -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,
+4
View File
@@ -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,
-6
View File
@@ -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 {
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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>
}
+6 -7
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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" />
+12 -23
View File
@@ -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>
);
+5 -4
View File
@@ -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) {
+33 -116
View File
@@ -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>
+6 -7
View File
@@ -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>
+7 -14
View File
@@ -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>
);
+8 -17
View File
@@ -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 }} />;
+2 -7
View File
@@ -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>
+2 -5
View File
@@ -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'
}`}
/>
))}
+7 -6
View File
@@ -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}</>;
+6 -5
View File
@@ -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
View File
@@ -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>
)}
</>
);
}
+18 -49
View File
@@ -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>
+7 -9
View File
@@ -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>
+4 -4
View File
@@ -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);
+56 -74
View File
@@ -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"
+12 -14
View File
@@ -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>
+18 -12
View File
@@ -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">
+193 -216
View File
@@ -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