update feat
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
After Width: | Height: | Size: 157 KiB |
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
After Width: | Height: | Size: 123 KiB |
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
|
@ -0,0 +1,114 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../ui/card';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
||||||
|
import Loader from "../ui/loader";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
|
||||||
|
const PRICE_CONFIG = [
|
||||||
|
{purchaseType: 'loose', price: 2000, minute: 10000},
|
||||||
|
{purchaseType: 'bulk', price: 1000, minute: 10000}
|
||||||
|
];
|
||||||
|
export default function STTStreaming(){
|
||||||
|
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||||
|
const [purchaseType, setPurchaseType] = useState();
|
||||||
|
const [amount, setAmount] = useState();
|
||||||
|
const [dataError, setDataError] = useState();
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const handlePurchaseType = (type) => {
|
||||||
|
const selected = PRICE_CONFIG.find(item => item.purchaseType === type);
|
||||||
|
setAmount(type === 'bulk' ? selected.price : type === 'loose' ? selected.price : '')
|
||||||
|
setPurchaseType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePurchase = async () => {
|
||||||
|
// Validate inputs
|
||||||
|
if (!purchaseType) {
|
||||||
|
setDataError('Please select a Plan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
setDataError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('service', 'STT Streaming API');
|
||||||
|
formData.append('serviceId', 'sttstreamingapi');
|
||||||
|
formData.append('cycle', 'monthly');
|
||||||
|
formData.append('amount', amount.toString());
|
||||||
|
formData.append('service_type', 'streaming_api');
|
||||||
|
const response = await fetch(`${USER_API_URL}?query=initiate_payment`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData, // Using FormData instead of JSON
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Redirect to payment success page with order ID
|
||||||
|
window.location.href = `/success?service=streaming_api&orderId=${data.order_id}`;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || 'Payment initialization failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Purchase error:', error);
|
||||||
|
setDataError(error.message || 'An error occurred during purchase. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if(!purchaseType){
|
||||||
|
<div>
|
||||||
|
<p>{dataError}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
<div className='container mx-auto'>
|
||||||
|
<Card className='max-w-2xl mx-auto px-4 mt-8'>
|
||||||
|
<CardContent>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>STT Streaming API</CardTitle>
|
||||||
|
<CardDescription>Real-time Speech-to-Text (STT) streaming that converts spoken words into accurate, readable text</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<div className='flex flex-row justify-between items-center gap-x-6'>
|
||||||
|
<label htmlFor="purchase-type-loose" className={`border ${purchaseType === 'loose' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}>
|
||||||
|
<input onChange={() => handlePurchaseType('loose')} type="radio" name="purchase-type" id="purchase-type-loose" className='hidden' />
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<span className='text-2xl font-bold'>Loose Plan</span>
|
||||||
|
<span className=''>🕐10,000 Min ₹2000</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label htmlFor="purchase-type-bulk" className={`border ${purchaseType === 'bulk' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}>
|
||||||
|
<input onChange={() => handlePurchaseType('bulk')} type="radio" name="purchase-type" id="purchase-type-bulk" className='hidden' />
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<span className='text-2xl font-bold'>Bulk Plan</span>
|
||||||
|
<span className=''>🕐10,000 Min ₹1000</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<ul className="grid grid-cols-2 justify-between mt-2 gap-x-2 text-xs">
|
||||||
|
{['Setup Charge 5000', '10000 Min INR 2000 in Loose Plan', '10000 Min INR 1000 in Bulk Plan', 'Real-time transcription with high accuracy', 'Supports multiple languages', 'Custom vocabulary for domain-specific terms', 'Integrates easily with video/audio streams', 'Secure and scalable API access', 'Live subtitle overlay options'].map((feature, index) => (
|
||||||
|
<li key={index} className="flex items-start">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
<span className='text-zinc-400'>{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className='flex my-8 w-full'>
|
||||||
|
<Button onClick={handlePurchase} disabled={!purchaseType} className={`w-full ${!purchaseType ? 'cursor-not-allowed' : ''}`}>{isProcessing ? (<><Loader2 className="mr-2 h-4 w-4 animate-spin" />Processing...</>) : 'Proceed'}</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -4,53 +4,57 @@ import { Textarea } from './ui/textarea';
|
||||||
import { Label } from './ui/label';
|
import { Label } from './ui/label';
|
||||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "./ui/select";
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "./ui/select";
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
|
|
||||||
export function ContactForm() {
|
export function ContactForm() {
|
||||||
const [formState, setFormState] = useState({
|
const [formState, setFormState] = useState({ name: '', email: '', company: '', service: '', message: '' });
|
||||||
name: '',
|
const [formStatus, setFormStatus] = useState('idle');
|
||||||
email: '',
|
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||||
company: '',
|
|
||||||
service: '',
|
|
||||||
message: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const [formStatus, setFormStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
|
const handleInputChange = (e) => {
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormState(prev => ({ ...prev, [name]: value }));
|
setFormState(prev => ({ ...prev, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectChange = (value: string) => {
|
const handleSelectChange = (value) => {
|
||||||
setFormState(prev => ({ ...prev, service: value }));
|
setFormState(prev => ({ ...prev, service: value }));
|
||||||
console.log(formState)
|
// console.log(formState)
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setFormStatus('submitting');
|
setFormStatus('submitting');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Simulate API call
|
const payload = {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
name: formState.name,
|
||||||
|
email: formState.email,
|
||||||
console.log('Form submitted:', formState);
|
company: formState.company,
|
||||||
setFormStatus('success');
|
service_intrest: formState.service,
|
||||||
|
message: formState.message
|
||||||
|
};
|
||||||
|
|
||||||
// Reset form
|
const response = await fetch(`${USER_API_URL}?query=contact-form`, {
|
||||||
setFormState({
|
method: "POST",
|
||||||
name: '',
|
credentials: "include",
|
||||||
email: '',
|
headers: {
|
||||||
company: '',
|
'Content-Type': 'application/json',
|
||||||
service: '',
|
},
|
||||||
message: '',
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => setFormStatus('idle'), 3000);
|
const data = await response.json();
|
||||||
|
// console.log('Form Response', data);
|
||||||
|
|
||||||
|
if(data.success) {
|
||||||
|
setFormStatus('success');
|
||||||
|
// Reset form only after successful submission
|
||||||
|
setFormState({ name: '', email: '', company: '', service: '', message: '' });
|
||||||
|
} else {
|
||||||
|
setFormStatus('error');
|
||||||
|
console.error('Backend error:', data.message);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Submission error:', error);
|
console.error('Submission error:', error);
|
||||||
setFormStatus('error');
|
setFormStatus('error');
|
||||||
setTimeout(() => setFormStatus('idle'), 3000);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -123,6 +123,13 @@ const LoginPage = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set Coockie Function
|
||||||
|
function setCookie(name : string, value : string, daysToExpire : number) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setTime(date.getTime() + (daysToExpire * 24 * 60 * 60 * 1000));
|
||||||
|
const expires = "expires=" + date.toUTCString();
|
||||||
|
document.cookie = `${name}=${value}; ${expires}; path=/`;
|
||||||
|
}
|
||||||
const handleCreateUserInMongo = async (siliconId: string) => {
|
const handleCreateUserInMongo = async (siliconId: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://hostapi2.cs1.hz.siliconpin.com/api/users`, {
|
const response = await fetch(`https://hostapi2.cs1.hz.siliconpin.com/api/users`, {
|
||||||
|
@ -184,7 +191,7 @@ const LoginPage = () => {
|
||||||
|
|
||||||
// Step 3: Redirect to profile if all are ok
|
// Step 3: Redirect to profile if all are ok
|
||||||
window.location.href = '/profile';
|
window.location.href = '/profile';
|
||||||
|
setCookie('token', data.userData.accessToken, 7);
|
||||||
return data;
|
return data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Sync Error:', error);
|
console.error('Sync Error:', error);
|
||||||
|
|
|
@ -0,0 +1,485 @@
|
||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import MDEditor, { commands } from '@uiw/react-md-editor';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Label } from './ui/label';
|
||||||
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from './ui/select';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
||||||
|
import { CustomTabs } from './ui/tabs';
|
||||||
|
|
||||||
|
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
||||||
|
const MINIO_UPLOAD_URL = 'https://hostapi2.cs1.hz.siliconpin.com/api/storage/upload';
|
||||||
|
|
||||||
|
const NewTopic = () => {
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formData, setFormData] = useState({ status: 'draft', category: '', title: '', content: '', imageUrl: '' });
|
||||||
|
// UI state
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [imageFile, setImageFile] = useState(null);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
|
const [imageDialogOpen, setImageDialogOpen] = useState(false);
|
||||||
|
const [imageUrlInput, setImageUrlInput] = useState('');
|
||||||
|
const [imageUploadFile, setImageUploadFile] = useState(null);
|
||||||
|
const [imageUploadPreview, setImageUploadPreview] = useState('');
|
||||||
|
const [editorMode, setEditorMode] = useState('edit');
|
||||||
|
|
||||||
|
// Upload file to MinIO
|
||||||
|
const uploadToMinIO = async (file, onProgress) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(MINIO_UPLOAD_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Upload failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data)
|
||||||
|
return data.url;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// // Generate slug from title
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (formData.title) {
|
||||||
|
// const slug = formData.title
|
||||||
|
// .toLowerCase()
|
||||||
|
// .replace(/[^\w\s]/g, '')
|
||||||
|
// .replace(/\s+/g, '-');
|
||||||
|
// setFormData(prev => ({ ...prev, slug }));
|
||||||
|
// }
|
||||||
|
// }, [formData.title]);
|
||||||
|
|
||||||
|
const customImageCommand = {
|
||||||
|
name: 'image',
|
||||||
|
keyCommand: 'image',
|
||||||
|
buttonProps: { 'aria-label': 'Insert image' },
|
||||||
|
icon: (
|
||||||
|
<svg width="12" height="12" viewBox="0 0 20 20">
|
||||||
|
<path fill="currentColor" d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
execute: () => {
|
||||||
|
setImageDialogOpen(true);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all default commands and replace the image command
|
||||||
|
const allCommands = commands.getCommands().map(cmd => {
|
||||||
|
if (cmd.name === 'image') {
|
||||||
|
return customImageCommand;
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Handle image URL insertion
|
||||||
|
const handleInsertImageUrl = () => {
|
||||||
|
if (imageUrlInput) {
|
||||||
|
const imgMarkdown = ``;
|
||||||
|
const textarea = document.querySelector('.w-md-editor-text-input');
|
||||||
|
if (textarea) {
|
||||||
|
const startPos = textarea.selectionStart;
|
||||||
|
const endPos = textarea.selectionEnd;
|
||||||
|
const currentValue = formData.content;
|
||||||
|
|
||||||
|
const newValue =
|
||||||
|
currentValue.substring(0, startPos) +
|
||||||
|
imgMarkdown +
|
||||||
|
currentValue.substring(endPos);
|
||||||
|
|
||||||
|
setFormData(prev => ({ ...prev, content: newValue }));
|
||||||
|
}
|
||||||
|
setImageDialogOpen(false);
|
||||||
|
setImageUrlInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle image file selection
|
||||||
|
const handleImageFileSelect = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
setImageUploadFile(file);
|
||||||
|
|
||||||
|
// Create preview
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
setImageUploadPreview(reader.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Upload image file to MinIO and insert into editor
|
||||||
|
const handleImageUpload = async () => {
|
||||||
|
if (!imageUploadFile) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setUploadProgress(0);
|
||||||
|
|
||||||
|
const uploadedUrl = await uploadToMinIO(imageUploadFile, (progress) => {
|
||||||
|
setUploadProgress(progress);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert markdown for the uploaded image
|
||||||
|
const imgMarkdown = ``;
|
||||||
|
const textarea = document.querySelector('.w-md-editor-text-input');
|
||||||
|
if (textarea) {
|
||||||
|
const startPos = textarea.selectionStart;
|
||||||
|
const endPos = textarea.selectionEnd;
|
||||||
|
const currentValue = formData.content;
|
||||||
|
|
||||||
|
const newValue =
|
||||||
|
currentValue.substring(0, startPos) +
|
||||||
|
imgMarkdown +
|
||||||
|
currentValue.substring(endPos);
|
||||||
|
|
||||||
|
setFormData(prev => ({ ...prev, content: newValue }));
|
||||||
|
}
|
||||||
|
|
||||||
|
setImageDialogOpen(false);
|
||||||
|
setImageUploadFile(null);
|
||||||
|
setImageUploadPreview('');
|
||||||
|
} catch (error) {
|
||||||
|
setError('Failed to upload image: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Form submission
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setError(null);
|
||||||
|
setUploadProgress(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Upload featured image if selected
|
||||||
|
let imageUrl = formData.imageUrl;
|
||||||
|
if (imageFile) {
|
||||||
|
imageUrl = await uploadToMinIO(imageFile, (progress) => {
|
||||||
|
setUploadProgress(progress);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare payload
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
imageUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
// Submit to API
|
||||||
|
const response = await fetch(`${TOPIC_API_URL}?query=create-new-topic`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.message || 'Failed to create topic');
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccess(true);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setUploadProgress(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle form field changes
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle featured image file selection
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
setImageFile(file);
|
||||||
|
// Preview image
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
setFormData(prev => ({ ...prev, imageUrl: reader.result }));
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
status: 'draft',
|
||||||
|
category: '',
|
||||||
|
title: '',
|
||||||
|
slug: '',
|
||||||
|
content: '',
|
||||||
|
imageUrl: ''
|
||||||
|
});
|
||||||
|
setImageFile(null);
|
||||||
|
setSuccess(false);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Success state
|
||||||
|
if (success) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 text-center py-8">
|
||||||
|
<h3 className="text-xl font-semibold text-green-600 mb-4">Topic created successfully!</h3>
|
||||||
|
<div className="flex justify-center gap-4">
|
||||||
|
<Button onClick={resetForm}>Create Another</Button>
|
||||||
|
<Button variant="outline" onClick={() => window.location.href = '/'}>Return Home</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="max-w-3xl mx-auto bg-neutral-800 rounded-lg shadow-md p-6">
|
||||||
|
<h2 className="text-2xl font-bold text-[#6d9e37] mb-2">Create New Topic</h2>
|
||||||
|
<p className="text-gray-600 mb-6">Start a new discussion in the SiliconPin community</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="status">Status</Label>
|
||||||
|
<Select
|
||||||
|
name="status"
|
||||||
|
required
|
||||||
|
value={formData.status}
|
||||||
|
onValueChange={(value) => setFormData(prev => ({ ...prev, status: value }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="status" className="text-sm sm:text-base w-full">
|
||||||
|
<SelectValue placeholder="Select status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="draft">Draft</SelectItem>
|
||||||
|
<SelectItem value="published">Published</SelectItem>
|
||||||
|
<SelectItem value="archived">Archived</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="category">Category *</Label>
|
||||||
|
<Select
|
||||||
|
name="category"
|
||||||
|
required
|
||||||
|
value={formData.category}
|
||||||
|
onValueChange={(value) => setFormData(prev => ({ ...prev, category: value }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="category" className="text-sm sm:text-base w-full">
|
||||||
|
<SelectValue placeholder="Select a category" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent position="popper" className="z-50">
|
||||||
|
<SelectItem value="php">PHP Hosting</SelectItem>
|
||||||
|
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title and Slug */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="title">Title *</Label>
|
||||||
|
<Input type="text" name="title" value={formData.title} onChange={handleChange} placeholder="Enter a descriptive title" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className="space-y-2">
|
||||||
|
<Label htmlFor="slug">URL Slug</Label>
|
||||||
|
<Input type="text" name="slug" value={formData.slug} onChange={handleChange} readOnly className="bg-gray-50" />
|
||||||
|
<p className="text-xs text-gray-500">This will be used in the topic URL</p>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Content Editor with Preview Toggle */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Label htmlFor="content">Content *</Label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
|
||||||
|
className={`ml-2 ${editorMode !== 'edit' ? 'bg-[#6d9e37]' : ''} text-white border border-[#6d9e37] text-[#6d9e37] px-2 py-1 rounded-md`}
|
||||||
|
>
|
||||||
|
{editorMode === 'edit' ? 'Preview' : 'Edit'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div data-color-mode="light">
|
||||||
|
<MDEditor
|
||||||
|
placeholder="Write your content"
|
||||||
|
value={formData.content}
|
||||||
|
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
|
||||||
|
height={400}
|
||||||
|
preview={editorMode}
|
||||||
|
commands={allCommands}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image Upload Dialog */}
|
||||||
|
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Insert Image</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<CustomTabs
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
label: "From URL",
|
||||||
|
value: "url",
|
||||||
|
content: (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter image URL"
|
||||||
|
value={imageUrlInput}
|
||||||
|
onChange={(e) => setImageUrlInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleInsertImageUrl}
|
||||||
|
disabled={!imageUrlInput}
|
||||||
|
>
|
||||||
|
Insert Image
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Upload",
|
||||||
|
value: "upload",
|
||||||
|
content: (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageFileSelect}
|
||||||
|
/>
|
||||||
|
{imageUploadPreview && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<img
|
||||||
|
src={imageUploadPreview}
|
||||||
|
alt="Preview"
|
||||||
|
className="max-h-40 rounded-md border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2.5 rounded-full"
|
||||||
|
style={{ width: `${uploadProgress}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleImageUpload}
|
||||||
|
disabled={!imageUploadFile || isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Uploading...' : 'Upload & Insert'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Featured Image Upload */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="image">Featured Image</Label>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
id="image"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept="image/*"
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2.5 rounded-full"
|
||||||
|
style={{ width: `${uploadProgress}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formData.imageUrl && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<img
|
||||||
|
src={formData.imageUrl}
|
||||||
|
alt="Preview"
|
||||||
|
className="max-h-40 rounded-md border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Form Actions */}
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={resetForm}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="min-w-32"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span className="animate-spin">↻</span>
|
||||||
|
Creating...
|
||||||
|
</span>
|
||||||
|
) : 'Create Topic'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 bg-red-50 text-red-600 rounded-md">
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewTopic;
|
|
@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
|
||||||
import { CustomTabs } from './ui/tabs';
|
import { CustomTabs } from './ui/tabs';
|
||||||
|
|
||||||
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
||||||
const MINIO_UPLOAD_URL = 'https://your-minio-api-endpoint/upload';
|
const MINIO_UPLOAD_URL = 'https://hostapi2.cs1.hz.siliconpin.com/api/storage/upload';
|
||||||
|
|
||||||
const NewTopic = () => {
|
const NewTopic = () => {
|
||||||
|
|
||||||
|
@ -31,14 +31,13 @@ const NewTopic = () => {
|
||||||
const uploadToMinIO = async (file, onProgress) => {
|
const uploadToMinIO = async (file, onProgress) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('bucket', 'siliconpin-uploads');
|
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
|
||||||
formData.append('folder', 'topic-images');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(MINIO_UPLOAD_URL, {
|
const response = await fetch(MINIO_UPLOAD_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: 'include',
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
@ -46,7 +45,8 @@ const NewTopic = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.url;
|
console.log(data.publicUrl)
|
||||||
|
return data.publicUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -127,7 +127,7 @@ const NewTopic = () => {
|
||||||
// Upload image file to MinIO and insert into editor
|
// Upload image file to MinIO and insert into editor
|
||||||
const handleImageUpload = async () => {
|
const handleImageUpload = async () => {
|
||||||
if (!imageUploadFile) return;
|
if (!imageUploadFile) return;
|
||||||
|
console.log(imageUploadFile)
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
|
@ -174,10 +174,11 @@ const NewTopic = () => {
|
||||||
let imageUrl = formData.imageUrl;
|
let imageUrl = formData.imageUrl;
|
||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
imageUrl = await uploadToMinIO(imageFile, (progress) => {
|
imageUrl = await uploadToMinIO(imageFile, (progress) => {
|
||||||
|
console.log('imageUrl', imageUrl)
|
||||||
setUploadProgress(progress);
|
setUploadProgress(progress);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// let finalImageUrl = imageUrl.publicUrl;
|
||||||
// Prepare payload
|
// Prepare payload
|
||||||
const payload = {
|
const payload = {
|
||||||
...formData,
|
...formData,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { FileDown } from "lucide-react";
|
import { FileDown } from "lucide-react";
|
||||||
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||||
|
@ -32,7 +33,7 @@ export default function HestiaCredentialsFetcher() {
|
||||||
}, [sessionData]);
|
}, [sessionData]);
|
||||||
|
|
||||||
const handleSaveDataInMongoDB = async (servicesData, currentBillingId, serviceToEndpoint, siliconId) => {
|
const handleSaveDataInMongoDB = async (servicesData, currentBillingId, serviceToEndpoint, siliconId) => {
|
||||||
const query = serviceToEndpoint === 'vpn' ? 'vpns' : serviceToEndpoint === 'hosting' ? hostings : '';
|
const query = serviceToEndpoint === 'vpn' ? 'vpns' : serviceToEndpoint === 'hosting' ? 'hostings' : '';
|
||||||
try {
|
try {
|
||||||
const serviceDataPayload = {
|
const serviceDataPayload = {
|
||||||
success: servicesData.success,
|
success: servicesData.success,
|
||||||
|
@ -46,7 +47,7 @@ export default function HestiaCredentialsFetcher() {
|
||||||
billingId: currentBillingId,
|
billingId: currentBillingId,
|
||||||
status: "active"
|
status: "active"
|
||||||
}
|
}
|
||||||
console.log('serviceDataPayload', serviceDataPayload)
|
// console.log('serviceDataPayload', serviceDataPayload)
|
||||||
const response = await fetch(`https://hostapi2.cs1.hz.siliconpin.com/api/users/${siliconId}/${query}`, {
|
const response = await fetch(`https://hostapi2.cs1.hz.siliconpin.com/api/users/${siliconId}/${query}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -108,7 +109,7 @@ export default function HestiaCredentialsFetcher() {
|
||||||
element.click();
|
element.click();
|
||||||
document.body.removeChild(element);
|
document.body.removeChild(element);
|
||||||
};
|
};
|
||||||
|
console.log('serviceName', serviceName)
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
@ -168,7 +169,7 @@ export default function HestiaCredentialsFetcher() {
|
||||||
|
|
||||||
{error && <p className="text-red-600 mt-2">{error}</p>}
|
{error && <p className="text-red-600 mt-2">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
) : creds && creds.service === 'hestia' && (
|
) : creds && creds.service === 'hestia' ? (
|
||||||
<div className="p-4 bg-gray-100 rounded shadow max-w-md mx-auto mt-10">
|
<div className="p-4 bg-gray-100 rounded shadow max-w-md mx-auto mt-10">
|
||||||
<h2 className="text-xl font-bold mb-4">Hestia Credentials</h2>
|
<h2 className="text-xl font-bold mb-4">Hestia Credentials</h2>
|
||||||
|
|
||||||
|
@ -186,7 +187,16 @@ export default function HestiaCredentialsFetcher() {
|
||||||
|
|
||||||
{error && <p className="text-red-600 mt-2">{error}</p>}
|
{error && <p className="text-red-600 mt-2">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : serviceName === 'streaming_api' ? (
|
||||||
|
<Card className='max-w-2xl mx-auto px-4 mt-8'>
|
||||||
|
<CardContent>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>STT Streaming API Purchase Succesfully</CardTitle>
|
||||||
|
<CardDescription>You can See Info on my Billing Section in Profile</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : ''}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||||
import Loader from "./ui/loader";
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
||||||
|
|
||||||
export interface ServiceCardProps {
|
export interface ServiceCardProps {
|
||||||
|
@ -25,6 +24,7 @@ export function ServiceCard({ title, description, imageUrl, features, learnMoreU
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.location.href = buyButtonUrl;
|
window.location.href = buyButtonUrl;
|
||||||
|
console.log(buyButtonUrl)
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -39,7 +39,7 @@ export function ServiceCard({ title, description, imageUrl, features, learnMoreU
|
||||||
</div>
|
</div>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-xl text-[#6d9e37]">{title}</CardTitle>
|
<CardTitle className="text-xl text-[#6d9e37]">{title}</CardTitle>
|
||||||
<CardDescription className="text-neutral-400">{description}</CardDescription>
|
<CardDescription className="text-neutral-400 text-justify">{description}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-grow">
|
<CardContent className="flex-grow">
|
||||||
<ul className="space-y-2 text-sm">
|
<ul className="space-y-2 text-sm">
|
||||||
|
@ -60,7 +60,7 @@ export function ServiceCard({ title, description, imageUrl, features, learnMoreU
|
||||||
href={learnMoreUrl}
|
href={learnMoreUrl}
|
||||||
className="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors w-full"
|
className="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors w-full"
|
||||||
>
|
>
|
||||||
Learn More
|
Details
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||||
import { Pencil, Trash2, Search } from "lucide-react";
|
import { Pencil, Trash2, Search } from "lucide-react";
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
export default function TopicItems(props) {
|
export default function TopicItems(props) {
|
||||||
console.table(props.topics)
|
// console.table(props.topics)
|
||||||
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
const [localSearchTerm, setLocalSearchTerm] = React.useState(props.searchTerm || '');
|
const [localSearchTerm, setLocalSearchTerm] = React.useState(props.searchTerm || '');
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "./ui/select";
|
|
||||||
import { Separator } from "./ui/separator";
|
import { Separator } from "./ui/separator";
|
||||||
import { Textarea } from "./ui/textarea";
|
import { Textarea } from "./ui/textarea";
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
@ -266,7 +265,42 @@ export default function ProfilePage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 space-y-6 lg:max-w-xl">
|
<div className="flex-1 space-y-6 lg:max-w-xl">
|
||||||
<PasswordUpdateCard userId={userData?.session_data?.id} onLogout={sessionLogOut}/>
|
<Card className="">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex flex-row justify-between">
|
||||||
|
<CardTitle>My Services</CardTitle>
|
||||||
|
{/* <a href="/profile/billing-info" className="hover:bg-[#6d9e37] hover:text-white transtion duration-500 py-1 px-2 rounded">View All</a> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardDescription>
|
||||||
|
View your all Services.
|
||||||
|
</CardDescription>
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-left">Order ID</th>
|
||||||
|
<th className="text-center">Date</th>
|
||||||
|
<th className="text-left">Description</th>
|
||||||
|
<th className="text-center">Status</th>
|
||||||
|
{/* <th className="text-center">Action</th> */}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
invoiceList.slice(0, 5).map((invoice, index) => (
|
||||||
|
<tr key={index} className="">
|
||||||
|
<td>{invoice.billing_id}</td>
|
||||||
|
<td className="text-center">{invoice?.created_at.split(' ')[0]}</td>
|
||||||
|
<td className=""><p className="line-clamp-1">{invoice.service}</p></td>
|
||||||
|
<td className={`text-center text-sm rounded-full h-fit ${invoice.service_status === '0' ? 'text-yellow-500' : invoice.service_status === '1' ? 'text-green-500' : 'text-red-500'}`}>{invoice.service_status === '1' ? 'Active' : invoice.service_status === '0' ? 'Unactive' : ''}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
<PasswordUpdateCard userId={userData?.session_data?.id} onLogout={sessionLogOut} />
|
||||||
<Card className="mt-6">
|
<Card className="mt-6">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Danger Zone</CardTitle>
|
<CardTitle>Danger Zone</CardTitle>
|
||||||
|
|
|
@ -62,7 +62,7 @@ const contactSchema = {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Full width contact form section -->
|
<!-- Full width contact form section -->
|
||||||
<section aria-labelledby="contact-form-heading" class="mb-8 sm:mb-12 bg-neutral-800 rounded-lg p-4 sm:p-6 lg:p-8 border border-neutral-700">
|
<section aria-labelledby="contact-form-heading" class="mb-8 sm:mb-12 mx-auto bg-neutral-800 rounded-lg p-4 sm:p-6 lg:p-8 border border-neutral-700">
|
||||||
<h2 id="contact-form-heading" class="text-xl sm:text-2xl font-bold text-white mb-4 sm:mb-6">Send us a message</h2>
|
<h2 id="contact-form-heading" class="text-xl sm:text-2xl font-bold text-white mb-4 sm:mb-6">Send us a message</h2>
|
||||||
<ContactForm client:load />
|
<ContactForm client:load />
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import Layout from '../../layouts/Layout.astro';
|
import Layout from '../../layouts/Layout.astro';
|
||||||
import { ServiceCard } from '../../components/ServiceCard';
|
import {ServiceCard} from '../../components/ServiceCard';
|
||||||
|
|
||||||
// Page-specific SEO metadata
|
// Page-specific SEO metadata
|
||||||
const pageTitle = "Hosting Services | SiliconPin";
|
const pageTitle = "Hosting Services | SiliconPin";
|
||||||
|
@ -12,7 +12,7 @@ const services = [
|
||||||
{
|
{
|
||||||
title: 'VPN (WireGuard)',
|
title: 'VPN (WireGuard)',
|
||||||
description: 'WireGuard is much better than other solutions like open VPN Free VPN. as WireGuard is the best we provide WireGuard VPN',
|
description: 'WireGuard is much better than other solutions like open VPN Free VPN. as WireGuard is the best we provide WireGuard VPN',
|
||||||
imageUrl: '/assets/images/wiregurd-thumb.jpg',
|
imageUrl: '/assets/images/services/wiregurd-thumb.jpg',
|
||||||
features: ["600 GB bandwidth", "WireGuard® protocol", "Global server locations", "No activity logs"],
|
features: ["600 GB bandwidth", "WireGuard® protocol", "Global server locations", "No activity logs"],
|
||||||
learnMoreUrl: '/services/vpn',
|
learnMoreUrl: '/services/vpn',
|
||||||
buyButtonText: 'Buy VPN',
|
buyButtonText: 'Buy VPN',
|
||||||
|
@ -20,7 +20,7 @@ const services = [
|
||||||
},{
|
},{
|
||||||
title: 'Deploy an App',
|
title: 'Deploy an App',
|
||||||
description: 'WordPress, Joomla, Drupal, PrestaShop, Wiki, Moodle, Directus, PocketBase, StarAPI and more.',
|
description: 'WordPress, Joomla, Drupal, PrestaShop, Wiki, Moodle, Directus, PocketBase, StarAPI and more.',
|
||||||
imageUrl: '/assets/images/deployapp-thumb.jpg',
|
imageUrl: '/assets/images/services/deployapp-thumb.jpg',
|
||||||
features: [ 'WordPress', 'Joomla', 'Drupal', 'PrestaShop', 'Wiki', 'Moodle', 'Directus', 'PocketBase', 'StarAPI' ],
|
features: [ 'WordPress', 'Joomla', 'Drupal', 'PrestaShop', 'Wiki', 'Moodle', 'Directus', 'PocketBase', 'StarAPI' ],
|
||||||
learnMoreUrl: '/services/deploy-an-app',
|
learnMoreUrl: '/services/deploy-an-app',
|
||||||
buyButtonText: '',
|
buyButtonText: '',
|
||||||
|
@ -56,16 +56,25 @@ const services = [
|
||||||
{
|
{
|
||||||
title: "Node.js Hosting",
|
title: "Node.js Hosting",
|
||||||
description: "Optimized cloud hosting for Node.js applications with automatic scaling.",
|
description: "Optimized cloud hosting for Node.js applications with automatic scaling.",
|
||||||
imageUrl: "/assets/node-js.jpg",
|
imageUrl: "/assets/images/services/node-js.jpg",
|
||||||
features: ["Express", "NestJS", "Koa", "Socket.io", "PM2", "WebSockets", "Serverless", "TypeScript", "GraphQL"],
|
features: ["Express", "NestJS", "Koa", "Socket.io", "PM2", "WebSockets", "Serverless", "TypeScript", "GraphQL"],
|
||||||
learnMoreUrl: "/services/nodejs-hosting",
|
learnMoreUrl: "/services/nodejs-hosting",
|
||||||
buyButtonText: "",
|
buyButtonText: "",
|
||||||
buyButtonUrl: ""
|
buyButtonUrl: ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'STT Streaming (API Services)',
|
||||||
|
description: 'Real-time Speech-to-Text (STT) streaming that converts spoken words into accurate, readable text. Ideal for live events, webinars, customer support, and accessibility solutions.',
|
||||||
|
imageUrl: '/assets/images/services/stt-streaming.png',
|
||||||
|
features: ['Setup Charge 5000', '10000 Min INR 2000 in Loose Plan', '10000 Min INR 1000 in Bulk Plan', 'Real-time transcription with high accuracy', 'Supports multiple languages', 'Custom vocabulary for domain-specific terms', 'Integrates easily with video/audio streams', 'Secure and scalable API access', 'Live subtitle overlay options'],
|
||||||
|
learnMoreUrl: '',
|
||||||
|
buyButtonText: 'Purchase',
|
||||||
|
buyButtonUrl: '/services/stt-streaming'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Kubernetes (K8s)',
|
title: 'Kubernetes (K8s)',
|
||||||
description: 'Enterprise-grade Kubernetes clusters for container orchestration at scale.',
|
description: 'Enterprise-grade Kubernetes clusters for container orchestration at scale.',
|
||||||
imageUrl: 'https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop',
|
imageUrl: '/assets/images/services/kubernetes-edge.png',
|
||||||
features: [ 'Fully managed K8s clusters', 'Auto-scaling and load balancing', 'Advanced networking options', 'Persistent storage solutions', 'Enterprise support available'],
|
features: [ 'Fully managed K8s clusters', 'Auto-scaling and load balancing', 'Advanced networking options', 'Persistent storage solutions', 'Enterprise support available'],
|
||||||
learnMoreUrl: '/services/kubernetes',
|
learnMoreUrl: '/services/kubernetes',
|
||||||
buyButtonText: '',
|
buyButtonText: '',
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
import STTStreaming from "../../components/BuyServices/STTStreaming"
|
||||||
|
---
|
||||||
|
<Layout title="STT Streaming | SiliconPin">
|
||||||
|
<STTStreaming client:load />
|
||||||
|
</Layout>
|
|
@ -0,0 +1,100 @@
|
||||||
|
---
|
||||||
|
import Layout from "../layouts/Layout.astro";
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div>
|
||||||
|
<button id="startBtn">Start Speaking</button>
|
||||||
|
<button id="stopBtn">Stop</button>
|
||||||
|
<div id="outputText" style="min-height: 100px; border: 1px solid #ccc; padding: 10px;"></div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
/**
|
||||||
|
* Speech-to-Text function that listens to microphone input and displays transcribed text
|
||||||
|
* @param {HTMLElement} outputElement - Element where the transcribed text will be displayed
|
||||||
|
* @param {function} onResult - Optional callback function that receives the transcribed text
|
||||||
|
* @returns {object} An object with start and stop methods to control the recognition
|
||||||
|
*/
|
||||||
|
function speechToText(outputElement, onResult) {
|
||||||
|
// Check if browser supports the Web Speech API
|
||||||
|
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
||||||
|
alert('Your browser does not support speech recognition. Try Chrome or Edge.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create speech recognition object
|
||||||
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
|
const recognition = new SpeechRecognition();
|
||||||
|
|
||||||
|
// Configure recognition settings
|
||||||
|
recognition.continuous = true;
|
||||||
|
recognition.interimResults = true;
|
||||||
|
recognition.lang = 'en-US'; // Set language - change as needed
|
||||||
|
|
||||||
|
let finalTranscript = '';
|
||||||
|
|
||||||
|
// Event handler for recognition results
|
||||||
|
recognition.onresult = (event) => {
|
||||||
|
let interimTranscript = '';
|
||||||
|
|
||||||
|
// Clear final transcript at the start of new session
|
||||||
|
if (event.resultIndex === 0) {
|
||||||
|
finalTranscript = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||||
|
const transcript = event.results[i][0].transcript;
|
||||||
|
|
||||||
|
if (event.results[i].isFinal) {
|
||||||
|
// Replace rather than append to avoid duplicates
|
||||||
|
finalTranscript = transcript;
|
||||||
|
if (onResult) onResult(finalTranscript);
|
||||||
|
} else {
|
||||||
|
interimTranscript = transcript;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the output element
|
||||||
|
if (outputElement) {
|
||||||
|
outputElement.innerHTML = finalTranscript + (interimTranscript ? '<span style="color:#999">' + interimTranscript + '</span>' : '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onerror = (event) => {
|
||||||
|
console.error('Speech recognition error', event.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onend = () => {
|
||||||
|
console.log('Speech recognition ended');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return control methods
|
||||||
|
return {
|
||||||
|
start: () => recognition.start(),
|
||||||
|
stop: () => recognition.stop(),
|
||||||
|
abort: () => recognition.abort(),
|
||||||
|
getFinalTranscript: () => finalTranscript
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get DOM elements
|
||||||
|
const outputElement = document.getElementById('outputText');
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
|
||||||
|
// Initialize speech recognition
|
||||||
|
const recognizer = speechToText(outputElement, (finalText) => {
|
||||||
|
console.log('Final text:', finalText);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add button event listeners
|
||||||
|
startBtn.addEventListener('click', () => {
|
||||||
|
outputElement.textContent = 'Listening...';
|
||||||
|
recognizer.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
recognizer.stop();
|
||||||
|
console.log('Final transcript:', recognizer.getFinalTranscript());
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,200 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div class=" min-h-screen flex items-center justify-center p-4">
|
||||||
|
<div class="w-full max-w-2xl bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<div class="bg-blue-600 p-4 text-white">
|
||||||
|
<h1 class="text-2xl font-bold">Real-Time Speech Transcription</h1>
|
||||||
|
<p class="text-blue-100">Using Annyang.js for speech recognition</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800">Transcription</h2>
|
||||||
|
<div id="status" class="px-3 py-1 rounded-full text-sm font-medium bg-gray-200 text-gray-800">
|
||||||
|
Not Listening
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="transcript" class="h-64 p-4 border border-gray-300 rounded-lg overflow-y-auto bg-gray-50">
|
||||||
|
<p class="text-gray-500 italic">Your transcribed text will appear here...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4">
|
||||||
|
<button id="startBtn" class="flex-1 bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-4 rounded-lg transition">
|
||||||
|
Start Listening
|
||||||
|
</button>
|
||||||
|
<button id="stopBtn" class="flex-1 bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-4 rounded-lg transition" disabled>
|
||||||
|
Stop Listening
|
||||||
|
</button>
|
||||||
|
<button id="clearBtn" class="flex-1 bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-4 rounded-lg transition">
|
||||||
|
Clear Text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Available Commands</label>
|
||||||
|
<div class="bg-gray-50 p-3 rounded-lg border border-gray-200">
|
||||||
|
<ul class="list-disc pl-5 text-sm text-gray-600">
|
||||||
|
<li>"clear transcript" - Clears the transcription</li>
|
||||||
|
<li>"new paragraph" - Inserts a new paragraph</li>
|
||||||
|
<li>"period", "comma", "question mark" - Adds punctuation</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline src="https://cdnjs.cloudflare.com/ajax/libs/annyang/2.6.1/annyang.min.js"></script>
|
||||||
|
<script is:inline>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const clearBtn = document.getElementById('clearBtn');
|
||||||
|
const transcript = document.getElementById('transcript');
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
|
||||||
|
// Check if browser supports speech recognition
|
||||||
|
if (!annyang) {
|
||||||
|
status.textContent = "Speech recognition not supported";
|
||||||
|
startBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add commands
|
||||||
|
const commands = {
|
||||||
|
'*text': function(phrase) {
|
||||||
|
appendToTranscript(phrase);
|
||||||
|
},
|
||||||
|
'clear transcript': function() {
|
||||||
|
clearTranscript();
|
||||||
|
},
|
||||||
|
'new paragraph': function() {
|
||||||
|
appendToTranscript("\n\n");
|
||||||
|
},
|
||||||
|
'period': function() {
|
||||||
|
appendToTranscript(". ");
|
||||||
|
},
|
||||||
|
'comma': function() {
|
||||||
|
appendToTranscript(", ");
|
||||||
|
},
|
||||||
|
'question mark': function() {
|
||||||
|
appendToTranscript("? ");
|
||||||
|
},
|
||||||
|
'exclamation mark': function() {
|
||||||
|
appendToTranscript("! ");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
annyang.addCommands(commands);
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
startBtn.addEventListener('click', function() {
|
||||||
|
annyang.start({
|
||||||
|
autoRestart: true,
|
||||||
|
continuous: true
|
||||||
|
});
|
||||||
|
status.textContent = "Listening...";
|
||||||
|
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-green-200 text-green-800";
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', function() {
|
||||||
|
try {
|
||||||
|
annyang.abort();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Stopping recognition", e);
|
||||||
|
}
|
||||||
|
status.textContent = "Not Listening";
|
||||||
|
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-gray-200 text-gray-800";
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
startBtn.disabled = false;
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
clearBtn.addEventListener('click', clearTranscript);
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
annyang.addCallback('start', function() {
|
||||||
|
console.log("Speech recognition started");
|
||||||
|
});
|
||||||
|
|
||||||
|
annyang.addCallback('soundstart', function() {
|
||||||
|
console.log("User is speaking");
|
||||||
|
});
|
||||||
|
|
||||||
|
annyang.addCallback('end', function() {
|
||||||
|
console.log("Speech recognition ended");
|
||||||
|
});
|
||||||
|
|
||||||
|
annyang.addCallback('error', function(err) {
|
||||||
|
if (err.error === 'aborted') {
|
||||||
|
console.log("Recognition stopped manually");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Error:", err);
|
||||||
|
status.textContent = "Error occurred";
|
||||||
|
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-red-200 text-red-800";
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
annyang.addCallback('errorNetwork', function(err) {
|
||||||
|
console.error("Network error:", err);
|
||||||
|
status.textContent = "Network error";
|
||||||
|
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-red-200 text-red-800";
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
annyang.addCallback('errorPermissionBlocked', function() {
|
||||||
|
console.error("Permission blocked");
|
||||||
|
status.textContent = "Permission blocked";
|
||||||
|
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-red-200 text-red-800";
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
annyang.addCallback('errorPermissionDenied', function() {
|
||||||
|
console.error("Permission denied");
|
||||||
|
status.textContent = "Permission denied";
|
||||||
|
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-red-200 text-red-800";
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function appendToTranscript(text) {
|
||||||
|
if (transcript.innerHTML.includes("Your transcribed text will appear here...")) {
|
||||||
|
transcript.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text === "\n\n") {
|
||||||
|
transcript.innerHTML += '<p class="mt-4"></p>';
|
||||||
|
} else {
|
||||||
|
let paragraphs = transcript.querySelectorAll('p');
|
||||||
|
let lastParagraph = paragraphs.length > 0 ? paragraphs[paragraphs.length - 1] : null;
|
||||||
|
|
||||||
|
if (!lastParagraph) {
|
||||||
|
lastParagraph = document.createElement('p');
|
||||||
|
transcript.appendChild(lastParagraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastParagraph.textContent += text;
|
||||||
|
}
|
||||||
|
|
||||||
|
transcript.scrollTop = transcript.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTranscript() {
|
||||||
|
transcript.innerHTML = '<p class="text-gray-500 italic">Your transcribed text will appear here...</p>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,168 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="">
|
||||||
|
<div class="min-h-screen flex items-center justify-center p-4">
|
||||||
|
<div class="w-full max-w-2xl bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="bg-blue-600 text-white p-4">
|
||||||
|
<h1 class="text-2xl font-bold">Real-Time Transcription</h1>
|
||||||
|
<p class="text-blue-100">Using Articulate.js</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Status Indicator -->
|
||||||
|
<div id="status" class="mb-4 flex items-center">
|
||||||
|
<div id="statusDot" class="w-3 h-3 rounded-full bg-gray-400 mr-2"></div>
|
||||||
|
<span id="statusText" class="text-sm">Ready to start</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transcription Output -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block text-gray-700 font-medium mb-2">Transcription</label>
|
||||||
|
<div id="transcriptOutput" class="min-h-32 border border-gray-300 rounded-lg p-4 bg-gray-50 overflow-y-auto max-h-64">
|
||||||
|
<p id="interimText" class="text-gray-500 italic">Your transcription will appear here...</p>
|
||||||
|
<p id="finalText" class="text-gray-800"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<button id="startBtn" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M7 4a3 3 0 016 0v4a3 3 0 11-6 0V4zm4 10.93A7.001 7.001 0 0017 8a1 1 0 10-2 0A5 5 0 015 8a1 1 0 00-2 0 7.001 7.001 0 006 6.93V17H6a1 1 0 100 2h8a1 1 0 100-2h-3v-2.07z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
<button id="stopBtn" disabled class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg flex items-center opacity-50 cursor-not-allowed">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
<button id="clearBtn" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg flex items-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Language Selector -->
|
||||||
|
<select id="languageSelect" class="border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
<option value="en-US">English (US)</option>
|
||||||
|
<option value="en-GB">English (UK)</option>
|
||||||
|
<option value="es-ES">Spanish</option>
|
||||||
|
<option value="fr-FR">French</option>
|
||||||
|
<option value="de-DE">German</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="bg-gray-100 p-3 text-center text-sm text-gray-600">
|
||||||
|
<p>Click "Start" and begin speaking. Transcription will appear in real-time.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline src="https://cdn.jsdelivr.net/npm/@articulate/chromeless@1.3.0-beta5/dist/src/index.js"></script>
|
||||||
|
<script is:inline>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// DOM Elements
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const clearBtn = document.getElementById('clearBtn');
|
||||||
|
const languageSelect = document.getElementById('languageSelect');
|
||||||
|
const statusDot = document.getElementById('statusDot');
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
const interimText = document.getElementById('interimText');
|
||||||
|
const finalText = document.getElementById('finalText');
|
||||||
|
|
||||||
|
// Initialize Articulate.js
|
||||||
|
const articulate = new Articulate();
|
||||||
|
let isListening = false;
|
||||||
|
|
||||||
|
// Configure Articulate
|
||||||
|
function configureArticulate() {
|
||||||
|
articulate.configure({
|
||||||
|
continuous: true,
|
||||||
|
interimResults: true,
|
||||||
|
language: languageSelect.value,
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Recognition error:', error);
|
||||||
|
updateStatus('Error: ' + error.message, 'red');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status UI
|
||||||
|
function updateStatus(text, color = 'gray') {
|
||||||
|
statusText.textContent = text;
|
||||||
|
statusDot.className = `w-3 h-3 rounded-full mr-2 bg-${color}-500`;
|
||||||
|
|
||||||
|
// Handle special colors
|
||||||
|
if (color === 'green') {
|
||||||
|
statusDot.classList.add('animate-pulse');
|
||||||
|
} else {
|
||||||
|
statusDot.classList.remove('animate-pulse');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transcription
|
||||||
|
startBtn.addEventListener('click', () => {
|
||||||
|
configureArticulate();
|
||||||
|
|
||||||
|
articulate.start()
|
||||||
|
.on('transcript', (text, isFinal) => {
|
||||||
|
if (isFinal) {
|
||||||
|
finalText.textContent += text + ' ';
|
||||||
|
interimText.textContent = '';
|
||||||
|
} else {
|
||||||
|
interimText.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-scroll to bottom
|
||||||
|
document.getElementById('transcriptOutput').scrollTop =
|
||||||
|
document.getElementById('transcriptOutput').scrollHeight;
|
||||||
|
})
|
||||||
|
.on('start', () => {
|
||||||
|
isListening = true;
|
||||||
|
updateStatus('Listening...', 'green');
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
stopBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop transcription
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
articulate.stop();
|
||||||
|
isListening = false;
|
||||||
|
updateStatus('Ready to continue', 'blue');
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
stopBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear transcription
|
||||||
|
clearBtn.addEventListener('click', () => {
|
||||||
|
finalText.textContent = '';
|
||||||
|
interimText.textContent = 'Your transcription will appear here...';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change language
|
||||||
|
languageSelect.addEventListener('change', () => {
|
||||||
|
if (isListening) {
|
||||||
|
articulate.stop();
|
||||||
|
configureArticulate();
|
||||||
|
articulate.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial configuration
|
||||||
|
configureArticulate();
|
||||||
|
updateStatus('Ready to start', 'blue');
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,73 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
---
|
||||||
|
<Layout title="DeepSpeech Streaming STT">
|
||||||
|
<h1>DeepSpeech Real-Time Transcription</h1>
|
||||||
|
<button id="startBtn">Start Listening</button>
|
||||||
|
<button id="stopBtn" disabled>Stop</button>
|
||||||
|
<div id="transcript"></div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const transcriptDiv = document.getElementById('transcript');
|
||||||
|
|
||||||
|
let mediaStream;
|
||||||
|
let audioContext;
|
||||||
|
let processor;
|
||||||
|
let websocket;
|
||||||
|
|
||||||
|
startBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
// Initialize WebSocket
|
||||||
|
websocket = new WebSocket('ws://localhost:3000');
|
||||||
|
|
||||||
|
websocket.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
transcriptDiv.innerHTML += data.transcript + (data.isFinal ? '<br>' : ' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get microphone
|
||||||
|
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
|
// Set up audio processing (16kHz, mono for DeepSpeech)
|
||||||
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||||
|
|
||||||
|
// Resample to 16kHz (DeepSpeech requirement)
|
||||||
|
const downsampler = audioContext.createScriptProcessor(4096, 1, 1);
|
||||||
|
downsampler.onaudioprocess = (e) => {
|
||||||
|
const inputData = e.inputBuffer.getChannelData(0);
|
||||||
|
const pcm16 = new Int16Array(inputData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < inputData.length; i++) {
|
||||||
|
pcm16[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32768));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (websocket.readyState === WebSocket.OPEN) {
|
||||||
|
websocket.send(pcm16.buffer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
source.connect(downsampler);
|
||||||
|
downsampler.connect(audioContext.destination);
|
||||||
|
|
||||||
|
// Notify server to start processing
|
||||||
|
websocket.onopen = () => {
|
||||||
|
websocket.send('start');
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
if (mediaStream) mediaStream.getTracks().forEach(track => track.stop());
|
||||||
|
if (websocket) websocket.send('end');
|
||||||
|
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,70 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div>
|
||||||
|
<h1>Voice Recognition Demo</h1>
|
||||||
|
<button id="startBtn">Start Recording</button>
|
||||||
|
<button id="stopBtn" disabled>Stop Recording</button>
|
||||||
|
<div id="result"></div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const resultDiv = document.getElementById('result');
|
||||||
|
|
||||||
|
let mediaRecorder;
|
||||||
|
let audioChunks = [];
|
||||||
|
const serverUrl = 'https://transcribe.teachertrainingkolkata.in/transcribe';
|
||||||
|
|
||||||
|
startBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
mediaRecorder = new MediaRecorder(stream, {
|
||||||
|
mimeType: 'audio/webm;codecs=opus',
|
||||||
|
audioBitsPerSecond: 16000
|
||||||
|
});
|
||||||
|
|
||||||
|
mediaRecorder.ondataavailable = (e) => {
|
||||||
|
audioChunks.push(e.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.onstop = async () => {
|
||||||
|
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
||||||
|
audioChunks = [];
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('audio', audioBlob, 'recording.webm');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(serverUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
resultDiv.textContent = data.text || "No speech detected";
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
resultDiv.textContent = 'Error sending audio to server';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.start(1000); // Collect data every 1 second
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
resultDiv.textContent = "Recording...";
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
resultDiv.textContent = 'Error accessing microphone';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
mediaRecorder.stop();
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
resultDiv.textContent = "Processing...";
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,248 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div class="min-h-screen">
|
||||||
|
<div class="container mx-auto px-4 py-8 max-w-3xl">
|
||||||
|
<h1 class="text-3xl font-bold mb-6 text-center">🎙️ Speech to Text</h1>
|
||||||
|
|
||||||
|
<div class="bg-gray-200 rounded-lg shadow-md p-6">
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="flex flex-wrap gap-3 mb-6">
|
||||||
|
<button id="startBtn" class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed">
|
||||||
|
Start Recording
|
||||||
|
</button>
|
||||||
|
<button id="stopBtn" disabled class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed hidden">
|
||||||
|
Stop Recording
|
||||||
|
</button>
|
||||||
|
<button id="clearBtn" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||||
|
Clear Text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<div id="status" class="px-4 py-3 rounded-md mb-6 bg-gray-100 text-gray-700">
|
||||||
|
Ready to start
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transcription -->
|
||||||
|
<div id="transcription" class="p-4 border border-gray-200 rounded-md bg-gray-50 min-h-32 max-h-96 overflow-y-auto text-gray-700">
|
||||||
|
Transcribed text will appear here...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
class SpeechToTextApp {
|
||||||
|
constructor() {
|
||||||
|
this.ws = null;
|
||||||
|
this.audioContext = null;
|
||||||
|
this.processor = null;
|
||||||
|
this.stream = null;
|
||||||
|
this.isRecording = false;
|
||||||
|
|
||||||
|
this.startBtn = document.getElementById('startBtn');
|
||||||
|
this.stopBtn = document.getElementById('stopBtn');
|
||||||
|
this.clearBtn = document.getElementById('clearBtn');
|
||||||
|
this.status = document.getElementById('status');
|
||||||
|
this.transcription = document.getElementById('transcription');
|
||||||
|
|
||||||
|
this.initializeEventListeners();
|
||||||
|
this.connectWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeEventListeners() {
|
||||||
|
this.startBtn.addEventListener('click', () => this.startRecording());
|
||||||
|
this.stopBtn.addEventListener('click', () => this.stopRecording());
|
||||||
|
this.clearBtn.addEventListener('click', () => this.clearTranscription());
|
||||||
|
}
|
||||||
|
|
||||||
|
connectWebSocket() {
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
// const wsUrl = `${wsProtocol}//${window.location.host}`;
|
||||||
|
const wsUrl = `${wsProtocol}//${`localhost:3000`}`;
|
||||||
|
console.log(wsUrl)
|
||||||
|
this.ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
this.updateStatus('Connected to server', 'bg-green-100 text-green-700');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'transcription' && data.text) {
|
||||||
|
this.appendTranscription(data.text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
this.updateStatus('Disconnected from server', 'bg-red-100 text-red-700');
|
||||||
|
setTimeout(() => this.connectWebSocket(), 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = (error) => {
|
||||||
|
this.updateStatus('WebSocket error', 'bg-red-100 text-red-700');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async startRecording() {
|
||||||
|
try {
|
||||||
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: {
|
||||||
|
sampleRate: 16000,
|
||||||
|
channelCount: 1,
|
||||||
|
echoCancellation: true,
|
||||||
|
noiseSuppression: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||||||
|
sampleRate: 16000
|
||||||
|
});
|
||||||
|
|
||||||
|
const source = this.audioContext.createMediaStreamSource(this.stream);
|
||||||
|
|
||||||
|
await this.audioContext.audioWorklet.addModule('data:text/javascript,' + encodeURIComponent(`
|
||||||
|
class AudioProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.bufferSize = 4096;
|
||||||
|
this.buffer = new Float32Array(this.bufferSize);
|
||||||
|
this.bufferIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs) {
|
||||||
|
const input = inputs[0];
|
||||||
|
if (input.length > 0) {
|
||||||
|
const audioData = input[0];
|
||||||
|
|
||||||
|
for (let i = 0; i < audioData.length; i++) {
|
||||||
|
this.buffer[this.bufferIndex] = audioData[i];
|
||||||
|
this.bufferIndex++;
|
||||||
|
|
||||||
|
if (this.bufferIndex >= this.bufferSize) {
|
||||||
|
// Convert to WAV format
|
||||||
|
const int16Array = new Int16Array(this.bufferSize);
|
||||||
|
for (let j = 0; j < this.bufferSize; j++) {
|
||||||
|
int16Array[j] = Math.max(-32768, Math.min(32767, this.buffer[j] * 32768));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create WAV header
|
||||||
|
const wavBuffer = this.createWAVBuffer(int16Array);
|
||||||
|
this.port.postMessage(wavBuffer);
|
||||||
|
|
||||||
|
this.bufferIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
createWAVBuffer(samples) {
|
||||||
|
const length = samples.length;
|
||||||
|
const buffer = new ArrayBuffer(44 + length * 2);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
// WAV header
|
||||||
|
const writeString = (offset, string) => {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
writeString(0, 'RIFF');
|
||||||
|
view.setUint32(4, 36 + length * 2, true);
|
||||||
|
writeString(8, 'WAVE');
|
||||||
|
writeString(12, 'fmt ');
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
view.setUint16(20, 1, true);
|
||||||
|
view.setUint16(22, 1, true);
|
||||||
|
view.setUint32(24, 16000, true);
|
||||||
|
view.setUint32(28, 16000 * 2, true);
|
||||||
|
view.setUint16(32, 2, true);
|
||||||
|
view.setUint16(34, 16, true);
|
||||||
|
writeString(36, 'data');
|
||||||
|
view.setUint32(40, length * 2, true);
|
||||||
|
|
||||||
|
// Convert samples to bytes
|
||||||
|
let offset = 44;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
view.setInt16(offset, samples[i], true);
|
||||||
|
offset += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerProcessor('audio-processor', AudioProcessor);
|
||||||
|
`));
|
||||||
|
|
||||||
|
this.processor = new AudioWorkletNode(this.audioContext, 'audio-processor');
|
||||||
|
|
||||||
|
this.processor.port.onmessage = (event) => {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
source.connect(this.processor);
|
||||||
|
|
||||||
|
this.isRecording = true;
|
||||||
|
this.startBtn.disabled = true;
|
||||||
|
this.stopBtn.disabled = false;
|
||||||
|
this.stopBtn.classList.remove('hidden');
|
||||||
|
this.startBtn.textContent = 'Streaming...';
|
||||||
|
this.startBtn.classList.remove('bg-green-600', 'hover:bg-green-700');
|
||||||
|
this.startBtn.classList.add('bg-red-600', 'hover:bg-red-700');
|
||||||
|
this.updateStatus('🔴 Streaming...', 'bg-green-100 text-green-700');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.updateStatus('Error accessing microphone: ' + error.message, 'bg-red-100 text-red-700');
|
||||||
|
console.error('Error starting recording:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRecording() {
|
||||||
|
if (this.stream) {
|
||||||
|
this.stream.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.audioContext) {
|
||||||
|
this.audioContext.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRecording = false;
|
||||||
|
this.startBtn.disabled = false;
|
||||||
|
this.stopBtn.disabled = true;
|
||||||
|
this.stopBtn.classList.add('hidden');
|
||||||
|
this.startBtn.textContent = 'Start Recording';
|
||||||
|
this.startBtn.classList.remove('bg-red-600', 'hover:bg-red-700');
|
||||||
|
this.startBtn.classList.add('bg-green-600', 'hover:bg-green-700');
|
||||||
|
this.updateStatus('Recording stopped', 'bg-green-100 text-green-700');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTranscription() {
|
||||||
|
this.transcription.textContent = 'Transcribed text will appear here...';
|
||||||
|
this.transcription.classList.add('text-gray-500');
|
||||||
|
}
|
||||||
|
|
||||||
|
appendTranscription(text) {
|
||||||
|
if (this.transcription.textContent === 'Transcribed text will appear here...') {
|
||||||
|
this.transcription.textContent = '';
|
||||||
|
this.transcription.classList.remove('text-gray-500');
|
||||||
|
}
|
||||||
|
this.transcription.textContent += text + ' ';
|
||||||
|
this.transcription.scrollTop = this.transcription.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(message, classes = 'bg-gray-100 text-gray-700') {
|
||||||
|
this.status.textContent = message;
|
||||||
|
this.status.className = `px-4 py-3 rounded-md mb-6 ${classes}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new SpeechToTextApp();
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,100 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div>
|
||||||
|
<button id="startBtn">Start Speaking</button>
|
||||||
|
<button id="stopBtn">Stop</button>
|
||||||
|
<div id="outputText" style="min-height: 100px; border: 1px solid #ccc; padding: 10px;"></div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
/**
|
||||||
|
* Speech-to-Text function that listens to microphone input and displays transcribed text
|
||||||
|
* @param {HTMLElement} outputElement - Element where the transcribed text will be displayed
|
||||||
|
* @param {function} onResult - Optional callback function that receives the transcribed text
|
||||||
|
* @returns {object} An object with start and stop methods to control the recognition
|
||||||
|
*/
|
||||||
|
function speechToText(outputElement, onResult) {
|
||||||
|
// Check if browser supports the Web Speech API
|
||||||
|
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
||||||
|
alert('Your browser does not support speech recognition. Try Chrome or Edge.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create speech recognition object
|
||||||
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
|
const recognition = new SpeechRecognition();
|
||||||
|
|
||||||
|
// Configure recognition settings
|
||||||
|
recognition.continuous = true;
|
||||||
|
recognition.interimResults = true;
|
||||||
|
recognition.lang = 'en-US'; // Set language - change as needed
|
||||||
|
|
||||||
|
let finalTranscript = '';
|
||||||
|
|
||||||
|
// Event handler for recognition results
|
||||||
|
recognition.onresult = (event) => {
|
||||||
|
let interimTranscript = '';
|
||||||
|
|
||||||
|
// Clear final transcript at the start of new session
|
||||||
|
if (event.resultIndex === 0) {
|
||||||
|
finalTranscript = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||||
|
const transcript = event.results[i][0].transcript;
|
||||||
|
|
||||||
|
if (event.results[i].isFinal) {
|
||||||
|
// Replace rather than append to avoid duplicates
|
||||||
|
finalTranscript = transcript;
|
||||||
|
if (onResult) onResult(finalTranscript);
|
||||||
|
} else {
|
||||||
|
interimTranscript = transcript;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the output element
|
||||||
|
if (outputElement) {
|
||||||
|
outputElement.innerHTML = finalTranscript + (interimTranscript ? '<span style="color:#999">' + interimTranscript + '</span>' : '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onerror = (event) => {
|
||||||
|
console.error('Speech recognition error', event.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onend = () => {
|
||||||
|
console.log('Speech recognition ended');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return control methods
|
||||||
|
return {
|
||||||
|
start: () => recognition.start(),
|
||||||
|
stop: () => recognition.stop(),
|
||||||
|
abort: () => recognition.abort(),
|
||||||
|
getFinalTranscript: () => finalTranscript
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get DOM elements
|
||||||
|
const outputElement = document.getElementById('outputText');
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
|
||||||
|
// Initialize speech recognition
|
||||||
|
const recognizer = speechToText(outputElement, (finalText) => {
|
||||||
|
console.log('Final text:', finalText);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add button event listeners
|
||||||
|
startBtn.addEventListener('click', () => {
|
||||||
|
outputElement.textContent = 'Listening...';
|
||||||
|
recognizer.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
recognizer.stop();
|
||||||
|
console.log('Final transcript:', recognizer.getFinalTranscript());
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,276 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div class="min-h-screen">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="max-w-3xl mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6">
|
||||||
|
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Whisper.cpp STT Streaming</h1>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex justify-center space-x-4 mb-4">
|
||||||
|
<button id="startBtn" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">
|
||||||
|
Start Recording
|
||||||
|
</button>
|
||||||
|
<button id="stopBtn" disabled class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
|
||||||
|
Stop Recording
|
||||||
|
</button>
|
||||||
|
<button id="clearBtn" class="bg-gray-500 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded">
|
||||||
|
Clear Text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="language">
|
||||||
|
Language
|
||||||
|
</label>
|
||||||
|
<select id="language" class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||||
|
<option value="auto">Auto-detect</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="es">Spanish</option>
|
||||||
|
<option value="fr">French</option>
|
||||||
|
<option value="de">German</option>
|
||||||
|
<option value="it">Italian</option>
|
||||||
|
<option value="ja">Japanese</option>
|
||||||
|
<option value="zh">Chinese</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="model">
|
||||||
|
Model
|
||||||
|
</label>
|
||||||
|
<select id="model" class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||||
|
<option value="tiny">Tiny</option>
|
||||||
|
<option value="base">Base</option>
|
||||||
|
<option value="small">Small</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="large" selected>Large</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="status">
|
||||||
|
Status
|
||||||
|
</label>
|
||||||
|
<div id="status" class="bg-gray-100 p-3 rounded text-sm text-gray-700">
|
||||||
|
Ready to start recording...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="transcript">
|
||||||
|
Transcript
|
||||||
|
</label>
|
||||||
|
<div id="transcript" class="bg-gray-50 p-4 rounded min-h-32 border border-gray-200">
|
||||||
|
<!-- Transcript will appear here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 mt-6">
|
||||||
|
<p>Note: This interface connects to a whisper.cpp server for processing. Audio is streamed in real-time.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
// DOM Elements
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const clearBtn = document.getElementById('clearBtn');
|
||||||
|
const statusDiv = document.getElementById('status');
|
||||||
|
const transcriptDiv = document.getElementById('transcript');
|
||||||
|
const languageSelect = document.getElementById('language');
|
||||||
|
const modelSelect = document.getElementById('model');
|
||||||
|
|
||||||
|
// Audio context and variables
|
||||||
|
let audioContext;
|
||||||
|
let mediaStream;
|
||||||
|
let processor;
|
||||||
|
let audioSocket;
|
||||||
|
let silenceTimeout;
|
||||||
|
const SILENCE_THRESHOLD = 0.02; // Adjust based on testing
|
||||||
|
const SILENCE_TIMEOUT_MS = 2000; // 2 seconds of silence before stopping
|
||||||
|
|
||||||
|
// WebSocket URL - adjust to your whisper.cpp server
|
||||||
|
const WS_URL = 'ws://localhost:8765';
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Check for WebAudio API support
|
||||||
|
if (!window.AudioContext && !window.webkitAudioContext) {
|
||||||
|
statusDiv.textContent = 'Web Audio API not supported in this browser';
|
||||||
|
startBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for WebSocket support
|
||||||
|
if (!window.WebSocket) {
|
||||||
|
statusDiv.textContent = 'WebSocket not supported in this browser';
|
||||||
|
startBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
startBtn.addEventListener('click', startRecording);
|
||||||
|
stopBtn.addEventListener('click', stopRecording);
|
||||||
|
clearBtn.addEventListener('click', clearTranscript);
|
||||||
|
|
||||||
|
async function startRecording() {
|
||||||
|
try {
|
||||||
|
statusDiv.textContent = 'Requesting microphone...';
|
||||||
|
|
||||||
|
// Get microphone access
|
||||||
|
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
|
// Initialize audio context
|
||||||
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||||
|
|
||||||
|
// Create script processor for audio processing
|
||||||
|
processor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||||
|
|
||||||
|
// Connect audio nodes
|
||||||
|
source.connect(processor);
|
||||||
|
processor.connect(audioContext.destination);
|
||||||
|
|
||||||
|
// Initialize WebSocket connection
|
||||||
|
audioSocket = new WebSocket(WS_URL);
|
||||||
|
|
||||||
|
audioSocket.onopen = () => {
|
||||||
|
statusDiv.textContent = 'Connected to server. Recording...';
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
|
||||||
|
// Send configuration
|
||||||
|
audioSocket.send(JSON.stringify({
|
||||||
|
type: 'config',
|
||||||
|
language: languageSelect.value,
|
||||||
|
model: modelSelect.value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
audioSocket.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type === 'transcript') {
|
||||||
|
// Append to transcript
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.className = 'mb-2';
|
||||||
|
p.textContent = data.text;
|
||||||
|
transcriptDiv.appendChild(p);
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
transcriptDiv.scrollTop = transcriptDiv.scrollHeight;
|
||||||
|
} else if (data.type === 'status') {
|
||||||
|
statusDiv.textContent = data.message;
|
||||||
|
} else if (data.type === 'error') {
|
||||||
|
statusDiv.textContent = `Error: ${data.message}`;
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audioSocket.onclose = () => {
|
||||||
|
if (statusDiv.textContent !== 'Recording stopped.') {
|
||||||
|
statusDiv.textContent = 'Connection closed unexpectedly.';
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
audioSocket.onerror = (error) => {
|
||||||
|
statusDiv.textContent = `WebSocket error: ${error.message}`;
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process audio data
|
||||||
|
processor.onaudioprocess = (event) => {
|
||||||
|
if (!audioSocket || audioSocket.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
|
const audioData = event.inputBuffer.getChannelData(0);
|
||||||
|
|
||||||
|
// Check for silence
|
||||||
|
const isSilent = isAudioSilent(audioData);
|
||||||
|
|
||||||
|
if (!isSilent) {
|
||||||
|
// Reset silence timeout
|
||||||
|
clearTimeout(silenceTimeout);
|
||||||
|
silenceTimeout = setTimeout(() => {
|
||||||
|
statusDiv.textContent = 'Silence detected, stopping recording...';
|
||||||
|
stopRecording();
|
||||||
|
}, SILENCE_TIMEOUT_MS);
|
||||||
|
|
||||||
|
// Convert Float32Array to Int16Array for WebSocket
|
||||||
|
const int16Data = convertFloat32ToInt16(audioData);
|
||||||
|
|
||||||
|
// Send audio data
|
||||||
|
audioSocket.send(int16Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
statusDiv.textContent = `Error: ${error.message}`;
|
||||||
|
console.error(error);
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopRecording() {
|
||||||
|
statusDiv.textContent = 'Recording stopped.';
|
||||||
|
if (audioSocket && audioSocket.readyState === WebSocket.OPEN) {
|
||||||
|
audioSocket.send(JSON.stringify({ type: 'eof' }));
|
||||||
|
audioSocket.close();
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTranscript() {
|
||||||
|
transcriptDiv.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if (processor) {
|
||||||
|
processor.disconnect();
|
||||||
|
processor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaStream) {
|
||||||
|
mediaStream.getTracks().forEach(track => track.stop());
|
||||||
|
mediaStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioContext) {
|
||||||
|
audioContext.close().catch(console.error);
|
||||||
|
audioContext = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(silenceTimeout);
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function isAudioSilent(audioData) {
|
||||||
|
// Calculate RMS (root mean square) of the audio buffer
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < audioData.length; i++) {
|
||||||
|
sum += audioData[i] * audioData[i];
|
||||||
|
}
|
||||||
|
const rms = Math.sqrt(sum / audioData.length);
|
||||||
|
return rms < SILENCE_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertFloat32ToInt16(buffer) {
|
||||||
|
const length = buffer.length;
|
||||||
|
const int16Array = new Int16Array(length);
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const s = Math.max(-1, Math.min(1, buffer[i]));
|
||||||
|
int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
return int16Array.buffer;
|
||||||
|
}
|
||||||
|
</script>
|