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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env
128 changes: 85 additions & 43 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
background-color: #111827;
}
</style>

<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🪷</text></svg>">
<link rel="apple-touch-icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🪷</text></svg>">

<!-- Optimization Hints -->
<link rel="preconnect" href="https://aistudiocdn.com" crossorigin>
Expand All @@ -45,6 +46,20 @@
<meta property="og:description" content="Đồng hành tâm linh với AI inspired by Thích Nhất Hạnh">
<meta property="og:type" content="website">

<!-- Security Headers -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com data:;
img-src 'self' data: blob:;
media-src 'self' blob:;
connect-src 'self' https://generativelanguage.googleapis.com wss://generativelanguage.googleapis.com;
worker-src 'self' blob:;
base-uri 'self';
form-action 'self';
">

<link rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🪷</text></svg>">

Expand Down Expand Up @@ -132,18 +147,24 @@

/* Enhanced animations */
@keyframes glow {
0%, 100% {

0%,
100% {
box-shadow: 0 0 20px rgba(0, 243, 255, 0.3);
}

50% {
box-shadow: 0 0 40px rgba(0, 243, 255, 0.6);
}
}

@keyframes float {
0%, 100% {

0%,
100% {
transform: translateY(0px);
}

50% {
transform: translateY(-10px);
}
Expand All @@ -153,24 +174,31 @@
0% {
background-position: -200% center;
}

100% {
background-position: 200% center;
}
}

@keyframes pulse-slow {
0%, 100% {

0%,
100% {
opacity: 0.8;
}

50% {
opacity: 1;
}
}

@keyframes bounce-subtle {
0%, 100% {

0%,
100% {
transform: translateY(0);
}

50% {
transform: translateY(-2px);
}
Expand All @@ -180,6 +208,7 @@
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
Expand All @@ -189,6 +218,7 @@
0% {
transform: translateX(-100%);
}

100% {
transform: translateX(100%);
}
Expand Down Expand Up @@ -231,22 +261,24 @@

.no-scrollbar {
-ms-overflow-style: none;
/* scrollbar-width: none; */ /* Commented out for broader compatibility */
/* scrollbar-width: none; */
/* Commented out for broader compatibility */
/* Fallback for browsers that don't support scrollbar-width */
}

/* Modern browsers that support scrollbar-width */
@supports (scrollbar-width: none) {
.no-scrollbar {
scrollbar-width: none;
}
}

/* Fallback for older browsers */
@supports not (scrollbar-width: none) {
.no-scrollbar::-webkit-scrollbar {
display: none;
}

.no-scrollbar {
overflow: -moz-scrollbars-none;
}
Expand Down Expand Up @@ -326,41 +358,51 @@
}

/* Width utilities for progress bars */
.w-0 { width: 0%; }
.w-10 { width: 10%; }
.w-20 { width: 20%; }
.w-30 { width: 30%; }
.w-40 { width: 40%; }
.w-50 { width: 50%; }
.w-60 { width: 60%; }
.w-70 { width: 70%; }
.w-80 { width: 80%; }
.w-90 { width: 90%; }
.w-100 { width: 100%; }
.w-0 {
width: 0%;
}

.w-10 {
width: 10%;
}

.w-20 {
width: 20%;
}

.w-30 {
width: 30%;
}

.w-40 {
width: 40%;
}

.w-50 {
width: 50%;
}

.w-60 {
width: 60%;
}

.w-70 {
width: 70%;
}

.w-80 {
width: 80%;
}

.w-90 {
width: 90%;
}

.w-100 {
width: 100%;
}
</style>
<script type="importmap">
{
"imports": {
"react/": "https://aistudiocdn.com/react@^19.2.1/",
"react": "https://aistudiocdn.com/react@^19.2.1",
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.1/",
"react-dom": "https://aistudiocdn.com/react-dom@^19.2.1",
"react-dom/client": "https://aistudiocdn.com/react-dom@^19.2.1/client",
"scheduler": "https://aistudiocdn.com/scheduler@^0.23.2",
"scheduler/": "https://aistudiocdn.com/scheduler@^0.23.2/",
"@google/genai": "https://aistudiocdn.com/@google/genai@^1.32.0",
"lucide-react": "https://aistudiocdn.com/lucide-react@^0.559.0",
"three": "https://aistudiocdn.com/three@^0.170.0",
"@react-three/fiber": "https://aistudiocdn.com/@react-three/fiber@^9.0.0-rc.3",
"@react-three/drei": "https://aistudiocdn.com/@react-three/drei@^9.120.0",
"tone": "https://aistudiocdn.com/tone@^14.7.77",
"zustand": "https://esm.sh/zustand@^5.0.3?external=react",
"vite": "https://esm.sh/vite@^7.2.7",
"@vitejs/plugin-react": "https://esm.sh/@vitejs/plugin-react@^5.1.2",
"onnxruntime-web": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.17.1/dist/ort.es6.min.js"
}
}
</script>

</head>

<body class="bg-gray-900 text-gray-100 h-screen w-screen overflow-hidden">
Expand Down
28 changes: 28 additions & 0 deletions services/airlockClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { GoogleGenAI } from "@google/genai";

const DEFAULT_API_VERSION = "v1beta";
const AIRLOCK_CLIENT_KEY = "AIRLOCK_CLIENT";

const normalizeBaseUrl = (raw: string): string => raw.replace(/\/+$/, "");

export const getAirlockBaseUrl = (): string => {
const envUrl = import.meta.env.VITE_AIRLOCK_URL as string | undefined;
if (envUrl && envUrl.trim().length > 0) {
return normalizeBaseUrl(envUrl.trim());
}
return normalizeBaseUrl(window.location.origin);
};

export const createAirlockClient = (): GoogleGenAI => {
return new GoogleGenAI({
apiKey: AIRLOCK_CLIENT_KEY,
apiVersion: DEFAULT_API_VERSION,
httpOptions: {
baseUrl: getAirlockBaseUrl(),
apiVersion: DEFAULT_API_VERSION,
headers: {
"x-airlock-client": "thay-ai-web",
},
},
});
};
11 changes: 9 additions & 2 deletions services/audioContextManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ class AudioContextManager {
private async initializeContext(): Promise<AudioContext> {
try {
// Create new audio context with optimal settings
const context = new (window.AudioContext || (window as any).webkitAudioContext)();
// Attempt to force 24kHz for speech optimization (supported in modern browsers)
const contextOptions: AudioContextOptions = {
sampleRate: 24000,
latencyHint: 'interactive'
};

const context = new (window.AudioContext || (window as any).webkitAudioContext)(contextOptions);

// Resume context if suspended (common in mobile browsers)
if (context.state === 'suspended') {
Expand All @@ -56,7 +62,8 @@ class AudioContextManager {

// Set optimal audio parameters for voice processing
if (context.sampleRate !== 24000) {
console.warn(`[AudioContext] Sample rate is ${context.sampleRate}, expected 24000`);
console.warn(`[AudioContext] System enforced sample rate: ${context.sampleRate}Hz. Resampling may be required.`);
// Note: Downstream processors (AudioWorklet) must handle resampling if necessary.
}

// Add error handling
Expand Down
Loading
Loading