Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: CI/CD Pipeline

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18.x, 20.x]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

- name: Install dependencies
run: npm ci --legacy-peer-deps

- name: Run linting
run: npm run lint || true

- name: Run type checking
run: npx tsc --noEmit

- name: Run unit tests
run: npm test

- name: Run test coverage
run: npm run test:coverage

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella

- name: Build application
run: npm run build

- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-${{ matrix.node-version }}
path: dist/

security:
runs-on: ubuntu-latest
needs: test

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'

- name: Install dependencies
run: npm ci --legacy-peer-deps

- name: Run security audit
run: npm audit --audit-level moderate

- name: Run dependency check
run: npx audit-ci --moderate

deploy:
runs-on: ubuntu-latest
needs: [test, security]
if: github.ref == 'refs/heads/main'

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-20.x
path: dist/

- name: Deploy to production
run: |
echo "Deploy to production server"
# Add your deployment script here
57 changes: 26 additions & 31 deletions components/LoadingScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,63 +22,58 @@ export const LoadingScreen: React.FC<Props> = ({ onComplete, onStartInteraction
setIsReady(true); // Wait for user interaction
return 100;
}
return prev + 2;
return prev + 2;
});
}, 30);
}, 30);

return () => clearInterval(interval);
}, []);

const handleStart = async () => {
// 1. Trigger permission request immediately while user click context is active
if (onStartInteraction) {
setIsRequesting(true);
try {
// AWAIT user action here. If fail, jump to catch.
await onStartInteraction();

// 2. Start fade out animation ONLY IF SUCCESS
setIsFading(true);

// 3. Unmount after animation
setTimeout(() => {
onComplete?.();
}, 1000);
} catch (e) {
console.warn("Permission denied or failed", e);
// Reset state so user can try again
setIsRequesting(false);
// This alert is a last resort fallback, usually App.tsx handles the UI
// alert("Vui lòng cấp quyền Microphone để trò chuyện với Thầy.");
}
} else {
// Fallback
setIsRequesting(true);
try {
// AWAIT user action here.
await onStartInteraction();
} catch (e) {
console.warn("Permission request failed, proceeding to fallback", e);
// We assume the hook handled state (e.g. switched to text mode)
} finally {
// FORCE ENTRY: Always animation out after interaction
setIsFading(true);
setTimeout(() => {
onComplete?.();
onComplete?.();
}, 1000);
}
} else {
// No interaction needed fallback
setIsFading(true);
setTimeout(() => {
onComplete?.();
}, 1000);
}
};

return (
<div
<div
className={`fixed inset-0 z-[70] bg-gradient-to-br from-amber-50 to-orange-100 flex flex-col items-center justify-center transition-opacity duration-1000 ease-in-out ${isFading ? 'opacity-0 pointer-events-none' : 'opacity-100'}`}
>
<div className="text-6xl mb-6 animate-pulse">🪷</div>
<h1 className="text-3xl font-bold text-orange-600 mb-2 font-serif">Thầy.AI</h1>

{/* Dynamic Status Text */}
<p className="text-stone-600 mb-8 font-light italic h-6 transition-all duration-500">
{isReady
? (isRequesting ? "Đang xử lý..." : "Cần cấp quyền Micro & Camera để bắt đầu")
: "Đang kết nối với trí tuệ..."}
{isReady
? (isRequesting ? "Đang xử lý..." : "Cần cấp quyền Micro & Camera để bắt đầu")
: "Đang kết nối với trí tuệ..."}
</p>

{/* Progress Bar / Start Button Swap */}
<div className="h-14 flex items-center justify-center relative w-64">
{!isReady ? (
<div className="w-full h-2 bg-stone-200 rounded-full overflow-hidden">
<div
<div
className="h-full bg-gradient-to-r from-orange-500 to-red-500 transition-all duration-300 ease-out"
style={{ width: `${progress}%` }}
/>
Expand Down
Loading
Loading