suvodip ghosh 2025-07-08 11:40:48 +00:00
parent 340a1c3e61
commit 00d2bdd384
35 changed files with 5882 additions and 6044 deletions

View File

@ -1,42 +0,0 @@
{
"meta": {
"expiry": "2025-06-29 11:36:07.506Z",
"rawUser": {
"email": "suvodipghosh35@gmail.com",
"id": "3913660032212288",
"name": "Suvodip Ghosh",
"picture": {
"data": {
"height": 201,
"is_silhouette": false,
"url": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=3913660032212288&height=200&width=200&ext=1748604969&hash=AbaH-X9tWREaBN46FPCtAG6g",
"width": 200
}
}
},
"id": "3913660032212288",
"name": "Suvodip Ghosh",
"username": "",
"email": "suvodipghosh35@gmail.com",
"avatarURL": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=3913660032212288&height=200&width=200&ext=1748604969&hash=AbaH-X9tWREaBN46FPCtAG6g",
"accessToken": "EAATKn4EGuGABO5BQDFZBCNX6QrVyfsUZBCfJKov5OzHHrQIAEu5wOePaLcvIZBklT9MPkoiZCrZCraJNIwtBzGrKxZCeUMwKJRAbicZAPuK1gesmDl27iL3J7vp0sAjMoue4yACIZAIRDZBzsDU0Yi91ztFJ6FOJ2qWVfLkTgjkKwEudTe95vUYcN1E9oS6bo6smA75KdVB6RgGiK217C4ez9aRDrQcvH3ZBSGZBY2QZBW1KynycojdqhKAQS68xT1Bpezv80kEP5DQfONMMNbaUOwBU9r0WxwS0viCbW0qZAk92mXYZAZBQhny5Ili4aP1jmAZD",
"refreshToken": "",
"avatarUrl": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=3913660032212288&height=200&width=200&ext=1748604969&hash=AbaH-X9tWREaBN46FPCtAG6g"
},
"record": {
"avatar": "colorful_bird_sits_branch_forest_uiu589q5aa.jpg",
"collectionId": "_pb_users_auth_",
"collectionName": "users",
"created": "2025-03-10 13:17:51.909Z",
"email": "neha@siliconpin.com",
"emailVisibility": true,
"id": "7fp08mzhs7qgmg9",
"name": "Neha Ghosh",
"phone": "9090935312",
"provider": "",
"type": "admin",
"updated": "2025-04-30 09:34:24.970Z",
"verified": true
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NDY2MTc3NzAsImlkIjoiN2ZwMDhtemhzN3FnbWc5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.5Xz_0hoAod9YLgeOTHbWTEKdRxqusZPUDyb2D0SfVKQ"
}

View File

@ -1,77 +0,0 @@
{
"meta": {
"expiry": "",
"rawUser": {
"avatar_url": "https://avatars.githubusercontent.com/u/128118374?v=4",
"bio": null,
"blog": "",
"collaborators": 0,
"company": null,
"created_at": "2023-03-17T04:49:12Z",
"disk_usage": 55453,
"email": null,
"events_url": "https://api.github.com/users/suvodip35/events{/privacy}",
"followers": 0,
"followers_url": "https://api.github.com/users/suvodip35/followers",
"following": 1,
"following_url": "https://api.github.com/users/suvodip35/following{/other_user}",
"gists_url": "https://api.github.com/users/suvodip35/gists{/gist_id}",
"gravatar_id": "",
"hireable": null,
"html_url": "https://github.com/suvodip35",
"id": 128118374,
"location": null,
"login": "suvodip35",
"name": "Suvodip",
"node_id": "U_kgDOB6LuZg",
"notification_email": null,
"organizations_url": "https://api.github.com/users/suvodip35/orgs",
"owned_private_repos": 4,
"plan": {
"collaborators": 0,
"name": "free",
"private_repos": 10000,
"space": 976562499
},
"private_gists": 0,
"public_gists": 0,
"public_repos": 4,
"received_events_url": "https://api.github.com/users/suvodip35/received_events",
"repos_url": "https://api.github.com/users/suvodip35/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/suvodip35/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/suvodip35/subscriptions",
"total_private_repos": 4,
"twitter_username": null,
"two_factor_authentication": true,
"type": "User",
"updated_at": "2025-04-30T08:46:03Z",
"url": "https://api.github.com/users/suvodip35",
"user_view_type": "private"
},
"id": "128118374",
"name": "Suvodip",
"username": "suvodip35",
"email": "suvodipghosh35@gmail.com",
"avatarURL": "https://avatars.githubusercontent.com/u/128118374?v=4",
"accessToken": "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",
"refreshToken": "",
"avatarUrl": "https://avatars.githubusercontent.com/u/128118374?v=4"
},
"record": {
"avatar": "colorful_bird_sits_branch_forest_uiu589q5aa.jpg",
"collectionId": "_pb_users_auth_",
"collectionName": "users",
"created": "2025-03-10 13:17:51.909Z",
"email": "neha@siliconpin.com",
"emailVisibility": true,
"id": "7fp08mzhs7qgmg9",
"name": "Neha Ghosh",
"phone": "9090935312",
"provider": "",
"type": "admin",
"updated": "2025-04-30 09:34:24.970Z",
"verified": true
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NDY2MTc2NDQsImlkIjoiN2ZwMDhtemhzN3FnbWc5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.XLlZwZ3TrHWlIoxOhkLoKW7AbTPr6VIB49fRpNCTVbY"
}

View File

@ -1,38 +0,0 @@
{
"meta": {
"expiry": "2025-04-30 12:35:11.096Z",
"rawUser": {
"email": "suvoairtel@gmail.com",
"family_name": "GHOSH",
"given_name": "SUBHODIP",
"id": "112026834578335832361",
"name": "SUBHODIP GHOSH",
"picture": "https://lh3.googleusercontent.com/a/ACg8ocLvTh2_Injp6f23coUiAfTQB_WmLki0vmQinLak3pnY_xnznQ=s96-c",
"verified_email": true
},
"id": "112026834578335832361",
"name": "SUBHODIP GHOSH",
"username": "",
"email": "suvoairtel@gmail.com",
"avatarURL": "https://lh3.googleusercontent.com/a/ACg8ocLvTh2_Injp6f23coUiAfTQB_WmLki0vmQinLak3pnY_xnznQ=s96-c",
"accessToken": "ya29.a0AZYkNZiV6bkpVlDOQy2ayy6WCULOE5uWYi_QvRSN9vUqJgUxuxcn3xlUzbe6rOHQV7UCjPb_aCmiOojHCMWsCvtky80pUBNCI2rGGhCZYjpSMTispb9qvPyO7CNgsqh5YGxTUUXAkuz37uXm68MFv5YOoofMZ1f39f2OU0sIhisaCgYKAbgSARMSFQHGX2MiEUQ1pMH6u7K92FgWTPp4Uw0178",
"refreshToken": "",
"avatarUrl": "https://lh3.googleusercontent.com/a/ACg8ocLvTh2_Injp6f23coUiAfTQB_WmLki0vmQinLak3pnY_xnznQ=s96-c"
},
"record": {
"avatar": "colorful_bird_sits_branch_forest_uiu589q5aa.jpg",
"collectionId": "_pb_users_auth_",
"collectionName": "users",
"created": "2025-03-10 13:17:51.909Z",
"email": "neha@siliconpin.com",
"emailVisibility": true,
"id": "7fp08mzhs7qgmg9",
"name": "Neha Ghosh",
"phone": "9090935312",
"provider": "",
"type": "admin",
"updated": "2025-04-30 09:34:24.970Z",
"verified": true
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NDY2MTc3MTMsImlkIjoiN2ZwMDhtemhzN3FnbWc5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.Wig_LpSxOd03z389n186t2AwUu1Ypxmj3Apf5LdciaQ"
}

6790
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@
"@shadcn/ui": "^0.0.4",
"@types/date-fns": "^2.5.3",
"@types/react": "^19.0.12",
"@uiw/react-markdown-preview": "^5.1.4",
"@uiw/react-md-editor": "^3.25.6",
"add": "^2.0.6",
"astro": "^5.5.2",

1
public/assets/gitea.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Gitea" role="img" viewBox="0 0 512 512" width="40px" height="40px" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <rect rx="15%" height="512" width="512" fill="#ffffff"></rect> <path d="M419 150c-98 7-186 2-276-1-27 0-63 19-61 67 3 75 71 82 99 83 3 14 35 62 59 65h104c63-5 109-213 75-214zm-311 67c-3-21 7-42 42-42 3 39 10 61 22 96-32-5-59-15-64-54z" fill="#592"></path> <path d="m293 152v70" stroke="#ffffff" stroke-width="9"></path> <g transform="rotate(25.7 496 -423)" stroke-width="7" fill="#592"> <path d="M561 246h97" stroke="#592"></path> <rect x="561" y="246" width="97" height="97" rx="16" fill="#ffffff"></rect> <path d="M592 245v75" stroke="#592"></path> <path d="M592 273c45 0 38-5 38 48" fill="none" stroke="#592"></path> <circle cx="592" cy="320" r="10"></circle> <circle cx="630" cy="320" r="10"></circle> <circle cx="592" cy="273" r="10"></circle> </g> </g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,22 @@
let endpoint = 'https://api.utho.com/v2/kubernetes/deploy';
let kuberPayload = {
"dcslug": "inmumbaizone2",
"cluster_label": "MyK8S-flZuUYtk-nvn9h",
"cluster_version": "1.30.0-utho",
"nodepools": [
{
"label": "pool-GPlb5CRP",
"size": "10215",
"count": "2",
"ebs": [
{
"disk": "30",
"type": "nvme"
}
]
}
],
"vpc": "81b2bd94-61dc-424b-a1ca-ca4c810ed4c4",
"network_type": "publicprivate",
"cpumodel": "amd"
}

View File

@ -0,0 +1,470 @@
import React, { useState, useEffect } from "react";
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import Loader from "../ui/loader";
import { useToast } from "../ui/toast";
import { useIsLoggedIn } from '../../lib/isLoggedIn';
export default function Kubernetes() {
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingData, setIsFetchingData] = useState(true);
const [deployError, setDeployError] = useState(null);
const [deployStatus, setDeployStatus] = useState({});
const [vpcs, setVpcs] = useState([]);
const [nodePools, setNodePools] = useState([{ label: '', size: '', count: 1, ebs: [{ disk: '', type: 'nvme' }] }]);
const [formData, setFormData] = useState({
dcslug: 'inmumbaizone2',
cluster_label: '',
cluster_version: '1.30.0-utho',
vpc: '',
network_type: 'publicprivate',
cpumodel: 'amd'
});
const availableSizes = [
{ id: '10215', name: '2 vCPU, 4GB RAM' },
{ id: '10216', name: '4 vCPU, 8GB RAM' },
{ id: '10217', name: '8 vCPU, 16GB RAM' },
{ id: '10218', name: '16 vCPU, 32GB RAM' }
];
const availableVersions = [
'1.30.0-utho',
'1.29.0-utho',
'1.28.0-utho'
];
useEffect(() => {
const fetchData = async () => {
try {
// Simulate fetching VPCs - replace with actual API call
const mockVpcs = [
{ id: '81b2bd94-61dc-424b-a1ca-ca4c810ed4c4', name: 'Default VPC (Mumbai)' },
{ id: 'c17032c9-3cfd-4028-8f2a-f3f5aa8c2976', name: 'Default VPC (Bangalore)' }
];
setVpcs(mockVpcs);
} catch (error) {
showToast({
title: "Error",
description: "Failed to load configuration data",
variant: "destructive"
});
} finally {
setIsFetchingData(false);
}
};
fetchData();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
const payload = {
...formData,
nodepools: nodePools.map(pool => ({
...pool,
count: String(pool.count), // Ensure count is string as per API
ebs: pool.ebs.map(disk => ({
...disk,
disk: String(disk.disk) // Ensure disk size is string
}))
}))
};
const response = await fetch("https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=deploy&source=chanel_1", {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.status === "success") {
setDeployStatus({
status: data.status,
clusterId: data.clusterId,
endpoint: data.endpoint
});
} else {
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
}
} catch (error) {
setDeployError(error.message);
showToast({
title: "Deployment Error",
description: error.message,
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleNodePoolChange = (index, field, value) => {
const updatedPools = [...nodePools];
updatedPools[index][field] = value;
setNodePools(updatedPools);
};
const handleDiskChange = (poolIndex, diskIndex, field, value) => {
const updatedPools = [...nodePools];
updatedPools[poolIndex].ebs[diskIndex][field] = value;
setNodePools(updatedPools);
};
const addNodePool = () => {
setNodePools([...nodePools, { label: '', size: '', count: 1, ebs: [{ disk: '', type: 'nvme' }] }]);
};
const removeNodePool = (index) => {
if (nodePools.length > 1) {
const updatedPools = [...nodePools];
updatedPools.splice(index, 1);
setNodePools(updatedPools);
}
};
const addDisk = (poolIndex) => {
const updatedPools = [...nodePools];
updatedPools[poolIndex].ebs.push({ disk: '', type: 'nvme' });
setNodePools(updatedPools);
};
const removeDisk = (poolIndex, diskIndex) => {
const updatedPools = [...nodePools];
if (updatedPools[poolIndex].ebs.length > 1) {
updatedPools[poolIndex].ebs.splice(diskIndex, 1);
setNodePools(updatedPools);
}
};
if (loading || (isLoggedIn && isFetchingData)) {
return <Loader />;
}
if (deployError) return <p>Error: {deployError}</p>;
if (!isLoggedIn) {
return <p className="text-center mt-8">
You are not Logged in. <a href="/login" className="text-[#6d9e37]">Click Here</a> to login first then you can add this service.
</p>;
}
if (deployStatus.status === 'success') {
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Kubernetes Cluster Deployed Successfully</CardTitle>
<CardDescription>Your Kubernetes cluster is now being provisioned.</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<Label>Cluster ID:</Label>
<p className="font-mono p-2 bg-gray-100 rounded">{deployStatus.clusterId}</p>
</div>
<div>
<Label>API Endpoint:</Label>
<p className="font-mono p-2 bg-gray-100 rounded">{deployStatus.endpoint}</p>
</div>
<div className="pt-4">
<Button onClick={() => window.location.href = '/services/kubernetes'}>
Back to Kubernetes
</Button>
</div>
</div>
</CardContent>
</Card>
);
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Create New Kubernetes Cluster</CardTitle>
<CardDescription>Configure your Kubernetes cluster with the desired node pools and settings.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="cluster_label">Cluster Name *</Label>
<Input
id="cluster_label"
name="cluster_label"
value={formData.cluster_label}
onChange={handleChange}
placeholder="my-cluster"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="dcslug">Data Center *</Label>
<Select
name="dcslug"
value={formData.dcslug}
onValueChange={(value) => setFormData({...formData, dcslug: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="inmumbaizone2">Mumbai</SelectItem>
<SelectItem value="inbangalore">Bangalore</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="cluster_version">Kubernetes Version *</Label>
<Select
name="cluster_version"
value={formData.cluster_version}
onValueChange={(value) => setFormData({...formData, cluster_version: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select version" />
</SelectTrigger>
<SelectContent>
{availableVersions.map(version => (
<SelectItem key={version} value={version}>
{version}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="vpc">VPC *</Label>
<Select
name="vpc"
value={formData.vpc}
onValueChange={(value) => setFormData({...formData, vpc: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select VPC" />
</SelectTrigger>
<SelectContent>
{vpcs.map(vpc => (
<SelectItem key={vpc.id} value={vpc.id}>
{vpc.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label>Network Type *</Label>
<div className="flex gap-4">
<label className="flex items-center space-x-2">
<input
type="radio"
name="network_type"
value="publicprivate"
checked={formData.network_type === 'publicprivate'}
onChange={handleChange}
className="text-[#6d9e37]"
/>
<span>Public + Private</span>
</label>
<label className="flex items-center space-x-2">
<input
type="radio"
name="network_type"
value="private"
checked={formData.network_type === 'private'}
onChange={handleChange}
className="text-[#6d9e37]"
/>
<span>Private Only</span>
</label>
</div>
</div>
<div className="space-y-2">
<Label>CPU Model *</Label>
<div className="flex gap-4">
<label className="flex items-center space-x-2">
<input
type="radio"
name="cpumodel"
value="amd"
checked={formData.cpumodel === 'amd'}
onChange={handleChange}
className="text-[#6d9e37]"
/>
<span>AMD</span>
</label>
<label className="flex items-center space-x-2">
<input
type="radio"
name="cpumodel"
value="intel"
checked={formData.cpumodel === 'intel'}
onChange={handleChange}
className="text-[#6d9e37]"
/>
<span>Intel</span>
</label>
</div>
</div>
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-lg">Node Pools</Label>
<Button type="button" onClick={addNodePool} variant="outline">
Add Node Pool
</Button>
</div>
{nodePools.map((pool, poolIndex) => (
<div key={poolIndex} className="p-4 border rounded-lg space-y-4">
<div className="flex justify-between items-center">
<h4 className="font-medium">Node Pool #{poolIndex + 1}</h4>
{nodePools.length > 1 && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => removeNodePool(poolIndex)}
>
Remove
</Button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Pool Label</Label>
<Input
value={pool.label}
onChange={(e) => handleNodePoolChange(poolIndex, 'label', e.target.value)}
placeholder="worker-pool"
/>
</div>
<div className="space-y-2">
<Label>Node Size *</Label>
<Select
value={pool.size}
onValueChange={(value) => handleNodePoolChange(poolIndex, 'size', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select size" />
</SelectTrigger>
<SelectContent>
{availableSizes.map(size => (
<SelectItem key={size.id} value={size.id}>
{size.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Node Count *</Label>
<Input
type="number"
min="1"
value={pool.count}
onChange={(e) => handleNodePoolChange(poolIndex, 'count', parseInt(e.target.value) || 1)}
/>
</div>
</div>
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-md">EBS Volumes</Label>
<Button
type="button"
onClick={() => addDisk(poolIndex)}
variant="outline"
size="sm"
>
Add Disk
</Button>
</div>
{pool.ebs.map((disk, diskIndex) => (
<div key={diskIndex} className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
<div className="space-y-2">
<Label>Disk Size (GB) *</Label>
<Input
type="number"
min="1"
value={disk.disk}
onChange={(e) => handleDiskChange(poolIndex, diskIndex, 'disk', e.target.value)}
placeholder="30"
/>
</div>
<div className="space-y-2">
<Label>Disk Type *</Label>
<Select
value={disk.type}
onValueChange={(value) => handleDiskChange(poolIndex, diskIndex, 'type', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="nvme">NVMe SSD</SelectItem>
<SelectItem value="ssd">Standard SSD</SelectItem>
<SelectItem value="hdd">HDD</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex justify-end">
{pool.ebs.length > 1 && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => removeDisk(poolIndex, diskIndex)}
>
Remove
</Button>
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
<div className="flex justify-end pt-4">
<Button type="submit" disabled={isLoading} className="w-full md:w-auto">
{isLoading ? "Deploying Cluster..." : "Deploy Kubernetes Cluster"}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,487 @@
import React, { useState, useEffect } from "react";
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import Loader from "../ui/loader";
import { useToast } from "../ui/toast";
import { useIsLoggedIn } from '../../lib/isLoggedIn';
export default function Kubernetes() {
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingData, setIsFetchingData] = useState(true);
const [deployError, setDeployError] = useState(null);
const [deployStatus, setDeployStatus] = useState({});
const [vpcs, setVpcs] = useState([]);
const [nodePools, setNodePools] = useState([{ label: `${getRandomString()}`, size: '10215', count: 1, ebs: [{ disk: 5, type: 'nvme' }] }]);
const [formData, setFormData] = useState({ dcslug: 'inmumbaizone2', cluster_label: `${getRandomString()}`, cluster_version: '1.30.0-utho', network_type: 'publicprivate', });
function getRandomString( length = 10, { includeNumbers = true, includeLowercase = true, includeUppercase = true, } = {} ) {
let chars = '';
const numbers = '0123456789';
const lowercase = 'abcdefghijklmnopqrstuvwxyz';
const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if (includeNumbers) chars += numbers;
if (includeLowercase) chars += lowercase;
if (includeUppercase) chars += uppercase;
if (!chars) {
throw new Error('At least one character type must be included');
}
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
const availableSizes = [
{ id: '10215', name: '2 vCPU, 4GB RAM' },
{ id: '10216', name: '4 vCPU, 8GB RAM' },
{ id: '10217', name: '8 vCPU, 16GB RAM' },
{ id: '10218', name: '16 vCPU, 32GB RAM' }
];
const availableVersions = [
'1.30.0-utho',
'1.29.0-utho',
'1.28.0-utho'
];
useEffect(() => {
const fetchData = async () => {
try {
// Simulate fetching VPCs - replace with actual API call
const mockVpcs = [
{ id: '81b2bd94-61dc-424b-a1ca-ca4c810ed4c4', name: 'Default VPC (Mumbai)' },
{ id: 'c17032c9-3cfd-4028-8f2a-f3f5aa8c2976', name: 'Default VPC (Bangalore)' }
];
setVpcs(mockVpcs);
} catch (error) {
showToast({
title: "Error",
description: "Failed to load configuration data",
variant: "destructive"
});
} finally {
setIsFetchingData(false);
}
};
fetchData();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
const payload = {
...formData,
nodepools: nodePools.map(pool => ({
...pool,
count: String(pool.count), // Ensure count is string as per API
ebs: pool.ebs.map(disk => ({
...disk,
disk: String(disk.disk) // Ensure disk size is string
}))
}))
};
const response = await fetch("https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=deploy&source=chanel_1", {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.status === "success") {
setDeployStatus({
status: data.status,
clusterId: data.clusterId,
endpoint: data.endpoint
});
} else {
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
}
} catch (error) {
setDeployError(error.message);
showToast({
title: "Deployment Error",
description: error.message,
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleNodePoolChange = (index, field, value) => {
const updatedPools = [...nodePools];
updatedPools[index][field] = value;
setNodePools(updatedPools);
};
const handleDiskChange = (poolIndex, diskIndex, field, value) => {
const updatedPools = [...nodePools];
updatedPools[poolIndex].ebs[diskIndex][field] = value;
setNodePools(updatedPools);
};
const addNodePool = () => {
setNodePools([...nodePools, { label: '', size: '', count: 1, ebs: [{ disk: '', type: 'nvme' }] }]);
};
const removeNodePool = (index) => {
if (nodePools.length > 1) {
const updatedPools = [...nodePools];
updatedPools.splice(index, 1);
setNodePools(updatedPools);
}
};
const addDisk = (poolIndex) => {
const updatedPools = [...nodePools];
updatedPools[poolIndex].ebs.push({ disk: '', type: 'nvme' });
setNodePools(updatedPools);
};
const removeDisk = (poolIndex, diskIndex) => {
const updatedPools = [...nodePools];
if (updatedPools[poolIndex].ebs.length > 1) {
updatedPools[poolIndex].ebs.splice(diskIndex, 1);
setNodePools(updatedPools);
}
};
if (loading || (isLoggedIn && isFetchingData)) {
return <Loader />;
}
if (deployError) return <p>Error: {deployError}</p>;
if (!isLoggedIn) {
return <p className="text-center mt-8">
You are not Logged in. <a href="/login" className="text-[#6d9e37]">Click Here</a> to login first then you can add this service.
</p>;
}
if (deployStatus.status === 'success') {
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Kubernetes Cluster Deployed Successfully</CardTitle>
<CardDescription>Your Kubernetes cluster is now being provisioned.</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<Label>Cluster ID:</Label>
<p className="font-mono p-2 bg-gray-100 rounded">{deployStatus.id}</p>
</div>
{
}
{/* <div>
<Label>API Endpoint:</Label>
<p className="font-mono p-2 bg-gray-100 rounded">{deployStatus.endpoint}</p>
</div> */}
<div className="pt-4">
<Button onClick={() => window.location.href = '/services/kubernetis'}>
Back to Kubernetes
</Button>
</div>
</div>
</CardContent>
</Card>
);
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Create New Kubernetes Cluster</CardTitle>
<CardDescription>Configure your Kubernetes cluster with the desired node pools and settings.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="cluster_label">Cluster Name *</Label>
<Input
id="cluster_label"
name="cluster_label"
value={formData.cluster_label}
onChange={handleChange}
placeholder="my-cluster"
required
/>
</div>
{/* <div className="space-y-2">
<Label htmlFor="dcslug">Data Center *</Label>
<Select
name="dcslug"
value={formData.dcslug}
onValueChange={(value) => setFormData({...formData, dcslug: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
<SelectItem selected value="inmumbaizone2">Mumbai</SelectItem>
<SelectItem value="inbangalore">Bangalore</SelectItem>
</SelectContent>
</Select>
</div> */}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="cluster_version">Kubernetes Version *</Label>
<Select
name="cluster_version"
value={formData.cluster_version}
onValueChange={(value) => setFormData({...formData, cluster_version: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select version" />
</SelectTrigger>
<SelectContent>
{availableVersions.map(version => (
<SelectItem key={version} value={version}>
{version}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* <div className="space-y-2">
<Label htmlFor="vpc">VPC *</Label>
<Select
name="vpc"
value={formData.vpc}
onValueChange={(value) => setFormData({...formData, vpc: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select VPC" />
</SelectTrigger>
<SelectContent>
{vpcs.map(vpc => (
<SelectItem key={vpc.id} value={vpc.id}>
{vpc.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div> */}
<div className="space-y-2">
<Label>Network Type *</Label>
<div className="flex gap-4 border border-neutral-600 rounded-md p-2">
<label className="flex items-center space-x-2">
<input
type="radio"
name="network_type"
value="publicprivate"
checked={formData.network_type === 'publicprivate'}
onChange={handleChange}
className="text-[#6d9e37] accent-[#6d9e37]"
/>
<span>Public + Private</span>
</label>
<label className="flex items-center space-x-2">
<input
type="radio"
name="network_type"
value="private"
checked={formData.network_type === 'private'}
onChange={handleChange}
className="text-[#6d9e37] accent-[#6d9e37]"
/>
<span>Private Only</span>
</label>
</div>
</div>
</div>
{/* <div className="space-y-2">
<Label>CPU Model *</Label>
<div className="flex gap-4">
<label className="flex items-center space-x-2">
<input
type="radio"
name="cpumodel"
value="amd"
checked={formData.cpumodel === 'amd'}
onChange={handleChange}
className="text-[#6d9e37]"
/>
<span>AMD</span>
</label>
<label className="flex items-center space-x-2">
<input
type="radio"
name="cpumodel"
value="intel"
checked={formData.cpumodel === 'intel'}
onChange={handleChange}
className="text-[#6d9e37]"
/>
<span>Intel</span>
</label>
</div>
</div> */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-lg">Node Pools</Label>
<Button type="button" onClick={addNodePool} variant="outline">
Add Node Pool
</Button>
</div>
{nodePools.map((pool, poolIndex) => (
<div key={poolIndex} className="p-4 border rounded-lg space-y-4">
<div className="flex justify-between items-center">
<h4 className="font-medium">Node Pool #{poolIndex + 1}</h4>
{nodePools.length > 1 && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => removeNodePool(poolIndex)}
>
Remove
</Button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Pool Label</Label>
<Input
value={pool.label}
onChange={(e) => handleNodePoolChange(poolIndex, 'label', e.target.value)}
placeholder="worker-pool"
/>
</div>
<div className="space-y-2">
<Label>Node Size *</Label>
<Select
value={pool.size}
onValueChange={(value) => handleNodePoolChange(poolIndex, 'size', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select size" />
</SelectTrigger>
<SelectContent>
{availableSizes.map((size, index) => (
<SelectItem selected={index === 0} key={size.id} value={size.id}>
{size.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Node Count *</Label>
<Input
type="number"
min="1"
value={pool.count}
onChange={(e) => handleNodePoolChange(poolIndex, 'count', parseInt(e.target.value) || 1)}
/>
</div>
</div>
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-md">EBS Volumes</Label>
<Button
type="button"
onClick={() => addDisk(poolIndex)}
variant="outline"
size="sm"
>
Add Disk
</Button>
</div>
{pool.ebs.map((disk, diskIndex) => (
<div key={diskIndex} className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
<div className="space-y-2">
<Label>Disk Size (GB) *</Label>
<Input
type="number"
min="1"
value={disk.disk}
onChange={(e) => handleDiskChange(poolIndex, diskIndex, 'disk', e.target.value)}
placeholder="30"
/>
</div>
<div className="space-y-2">
<Label>Disk Type *</Label>
<Select
value={disk.type}
onValueChange={(value) => handleDiskChange(poolIndex, diskIndex, 'type', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="nvme">NVMe SSD</SelectItem>
<SelectItem value="ssd">Standard SSD</SelectItem>
<SelectItem value="hdd">HDD</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex justify-end">
{pool.ebs.length > 1 && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => removeDisk(poolIndex, diskIndex)}
>
Remove
</Button>
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
<div className="flex justify-end pt-4">
<Button type="submit" disabled={isLoading} className="w-full md:w-auto">
{isLoading ? "Deploying Cluster..." : "Deploy Kubernetes Cluster"}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@ -6,477 +6,462 @@ import { Input } from "../ui/input";
import { Label } from "../ui/label";
import Loader from "../ui/loader";
import { useToast } from "../ui/toast";
import { Switch } from "../ui/switch";
import { useIsLoggedIn } from '../../lib/isLoggedIn';
export default function NewKubernetesService() {
export default function Kubernetes() {
const { isLoggedIn, loading } = useIsLoggedIn();
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingData, setIsFetchingData] = useState(true);
const [plans, setPlans] = useState([]);
const [vpcs, setVpcs] = useState([]);
const [subnets, setSubnets] = useState([]);
const PUBLIC_UTHO_API_KEY = import.meta.env.PUBLIC_UTHO_API_KEY;
const [deployError, setDeployError] = useState(null);
const [deployStatus, setDeployStatus] = useState({});
const [clusterStatus, setClusterStatus] = useState('');
const [nodePools, setNodePools] = useState([{
label: `${getRandomString()}`,
size: '10215',
count: 1,
ebs: [{ disk: 30, type: 'nvme' }]
}]);
const [formData, setFormData] = useState({
dcslug: "innoida",
cluster_version: "1.24",
cluster_label: "",
nodepools: [{
label: "default-pool",
size: "",
count: 1,
maxCount: 1
}],
firewall: "",
vpc: "",
subnet: "",
network_type: "public",
cpumodel: "intel"
const [formData, setFormData] = useState({
dcslug: 'inmumbaizone2',
cluster_label: `${getRandomString()}`,
});
// Generate random string for IDs
function getRandomString(length = 8) {
return Math.random().toString(36).substring(2, length+2);
}
const availableSizes = [
{ id: '10215', name: '2 vCPU, 4GB RAM' },
{ id: '10216', name: '4 vCPU, 8GB RAM' },
{ id: '10217', name: '8 vCPU, 16GB RAM' },
{ id: '10218', name: '16 vCPU, 32GB RAM' }
];
// Check cluster status periodically after deployment
useEffect(() => {
const fetchInitialData = async () => {
setIsFetchingData(true);
if (!deployStatus.clusterId || deployStatus.isReady) return;
const checkStatus = async () => {
try {
const [plansResponse, vpcsResponse] = await Promise.all([
fetch("https://api.utho.com/v2/plans", {
headers: {
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
"Content-Type": "application/json"
}
}),
fetch("https://api.utho.com/v2/vpc", {
headers: {
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
"Content-Type": "application/json"
}
})
]);
const plansData = await plansResponse.json();
const vpcsData = await vpcsResponse.json();
setPlans(plansData.plans || []);
setVpcs(vpcsData.vpc || []);
// Set default VPC if available
if (vpcsData.vpc?.length > 0) {
const firstVpc = vpcsData.vpc[0];
setFormData(prev => ({
...prev,
vpc: firstVpc.id
}));
// Fetch subnets for the default VPC
await fetchSubnets(firstVpc.id);
const response = await fetch(
`https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=status&clusterId=${deployStatus.clusterId}&source=chanel_1`,
{ credentials: "include" }
);
if (!response.ok) throw new Error("Failed to check status");
const data = await response.json();
// Update status
setClusterStatus(data.status || 'pending');
if (data.status === 'active') {
// Cluster is ready for configuration download
setDeployStatus(prev => ({ ...prev, isReady: true }));
showToast({
title: "Cluster Ready",
description: "Your Kubernetes cluster is now fully configured.",
variant: "default"
});
return; // Stop checking when ready
} else if (data.status === 'failed') {
throw new Error("Cluster deployment failed");
}
// Continue checking every 20 seconds if not ready
setTimeout(checkStatus, 20000);
} catch (error) {
console.error("Initial data fetch error:", error);
showToast({
title: "Error",
description: "Failed to load initial configuration data",
variant: "destructive"
});
} finally {
setIsFetchingData(false);
console.error("Status check error:", error);
// Retry after delay if not a fatal error
if (!error.message.includes('failed')) {
setTimeout(checkStatus, 30000);
} else {
setDeployError(error.message);
}
}
};
fetchInitialData();
}, []);
const fetchSubnets = async (vpcId) => {
try {
// First try to get subnets from the VPC data
const selectedVpc = vpcs.find(vpc => vpc.id === vpcId);
if (selectedVpc?.subnet?.length > 0) {
setSubnets(selectedVpc.subnet);
setFormData(prev => ({
...prev,
subnet: selectedVpc.subnet[0].id
}));
return;
}
// If no subnets in VPC data, try the subnets endpoint
const response = await fetch(`https://api.utho.com/v2/vpc/${vpcId}/subnets`, {
headers: {
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
"Content-Type": "application/json"
}
});
if (response.ok) {
const data = await response.json();
setSubnets(data.subnets || []);
if (data.subnets?.length > 0) {
setFormData(prev => ({
...prev,
subnet: data.subnets[0].id
}));
}
}
} catch (error) {
console.error("Error fetching subnets:", error);
}
};
const handleVpcChange = async (vpcId) => {
setFormData(prev => ({
...prev,
vpc: vpcId,
subnet: ""
}));
await fetchSubnets(vpcId);
};
const timer = setTimeout(checkStatus, 15000);
return () => clearTimeout(timer);
}, [deployStatus.clusterId, deployStatus.isReady]);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
setDeployError(null);
try {
// Validate required fields including subnet
if (!formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet) {
throw new Error("Please fill all required fields including VPC and Subnet");
}
const payload = {
dcslug: formData.dcslug,
cluster_version: formData.cluster_version,
cluster_label: formData.cluster_label,
nodepools: formData.nodepools.map(pool => ({
label: pool.label,
size: pool.size,
count: pool.count.toString(),
maxCount: pool.maxCount.toString()
...formData,
cluster_version: '1.30.0-utho',
nodepools: nodePools.map(pool => ({
...pool,
count: String(pool.count),
ebs: pool.ebs.map(disk => ({
...disk,
disk: String(disk.disk)
}))
})),
firewall: formData.firewall || undefined,
vpc: formData.vpc,
subnet: formData.subnet, // Now required based on API behavior
network_type: formData.network_type,
cpumodel: formData.cpumodel
vpc: '81b2bd94-61dc-424b-a1ca-ca4c810ed4c4',
network_type: 'publicprivate',
cpumodel: 'amd'
};
console.log("Deployment payload:", payload);
const response = await fetch(
"https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=deploy&source=chanel_1",
{
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
}
);
const response = await fetch("https://api.utho.com/v2/kubernetes/deploy", {
method: "POST",
headers: {
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error("Deployment request failed");
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
}
if (data.status === "success") {
showToast({
title: "Success",
description: `Cluster ${data.cluster_id || data.id} is being deployed`,
variant: "success"
setDeployStatus({
status: "success",
clusterId: data.clusterId,
endpoint: data.endpoint,
isReady: false
});
showToast({
title: "Deployment Started",
description: "Your cluster is being provisioned. This may take 10-15 minutes.",
variant: "default"
});
// Reset form
setFormData(prev => ({
...prev,
cluster_label: "",
nodepools: [{
label: "default-pool",
size: "",
count: 1,
maxCount: 1
}],
vpc: vpcs[0]?.id || "",
subnet: subnets[0]?.id || ""
}));
} else {
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
throw new Error(data.message || "Cluster deployment failed");
}
} catch (error) {
setDeployError(error.message);
showToast({
title: "Deployment Failed",
description: error.message,
variant: "destructive"
});
console.error("Deployment error:", error);
} finally {
setIsLoading(false);
}
};
const downloadKubeConfig = async () => {
if (!deployStatus.clusterId) return;
try {
// Trigger download via backend
window.open(
`https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=download&clusterId=${deployStatus.clusterId}&source=chanel_1`,
'_blank'
);
} catch (error) {
showToast({
title: "Download Failed",
description: "Could not download configuration. Please try again.",
variant: "destructive"
});
}
};
// Form field handlers
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleNodePoolChange = (index, field, value) => {
const updatedNodePools = [...formData.nodepools];
updatedNodePools[index][field] = value;
setFormData(prev => ({
...prev,
nodepools: updatedNodePools
}));
const updatedPools = [...nodePools];
updatedPools[index][field] = value;
setNodePools(updatedPools);
};
if (isFetchingData) {
const handleDiskChange = (poolIndex, diskIndex, field, value) => {
const updatedPools = [...nodePools];
updatedPools[poolIndex].ebs[diskIndex][field] = value;
setNodePools(updatedPools);
};
const addNodePool = () => {
setNodePools([...nodePools, {
label: `${getRandomString()}`,
size: '10215',
count: 1,
ebs: [{ disk: 30, type: 'nvme' }]
}]);
};
const removeNodePool = (index) => {
if (nodePools.length > 1) {
const updatedPools = [...nodePools];
updatedPools.splice(index, 1);
setNodePools(updatedPools);
}
};
const addDisk = (poolIndex) => {
const updatedPools = [...nodePools];
updatedPools[poolIndex].ebs.push({ disk: 30, type: 'nvme' });
setNodePools(updatedPools);
};
const removeDisk = (poolIndex, diskIndex) => {
const updatedPools = [...nodePools];
if (updatedPools[poolIndex].ebs.length > 1) {
updatedPools[poolIndex].ebs.splice(diskIndex, 1);
setNodePools(updatedPools);
}
};
if (loading) {
return <Loader />;
}
if (!isLoggedIn) {
return (
<div className="flex items-center justify-center h-64">
<Loader />
<span className="ml-2">Loading configuration...</span>
</div>
<p className="text-center mt-8">
You must be logged in to deploy clusters. <a href="/login" className="text-[#6d9e37]">Login here</a>.
</p>
);
}
if (deployStatus.status === 'success') {
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Kubernetes Cluster Deployment</CardTitle>
<CardDescription>
{deployStatus.isReady
? "Your cluster is ready to use"
: "Your cluster is being provisioned"}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label>Current Status</Label>
<div className="p-3 bg-gray-100 rounded-md">
<div className="flex items-center gap-2">
{!deployStatus.isReady && <Loader size="sm" />}
<span className="font-medium">
{deployStatus.isReady ? 'Ready' : (clusterStatus || 'Starting deployment')}
</span>
</div>
{!deployStatus.isReady && (
<p className="text-sm text-muted-foreground mt-2">
This process may take several minutes. Please don't close this page.
</p>
)}
</div>
</div>
<div className="space-y-4">
<div>
<Label>Cluster ID</Label>
<p className="font-mono p-2 bg-gray-100 rounded text-sm">
{deployStatus.clusterId}
</p>
</div>
<div>
<Label>API Endpoint</Label>
<p className="font-mono p-2 bg-gray-100 rounded text-sm">
{deployStatus.endpoint || 'Pending...'}
</p>
</div>
</div>
{deployStatus.isReady ? (
<div className="flex flex-col gap-4 pt-4">
<Button onClick={downloadKubeConfig}>
Download Configuration File
</Button>
<Button variant="outline" onClick={() => window.location.reload()}>
Deploy Another Cluster
</Button>
</div>
) : (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader size="sm" />
<span>Preparing your cluster... This may take several minutes</span>
</div>
)}
</CardContent>
</Card>
);
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Deploy New Kubernetes Cluster</CardTitle>
<CardTitle>Create Kubernetes Cluster</CardTitle>
<CardDescription>
Configure your Kubernetes cluster with the required parameters
Configure your Kubernetes cluster with the desired specifications
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Cluster Label */}
<div className="space-y-2">
<Label htmlFor="cluster_label">Cluster Label *</Label>
<Input
id="cluster_label"
name="cluster_label"
value={formData.cluster_label}
onChange={handleChange}
placeholder="my-cluster"
required
/>
</div>
{/* Data Center and Version */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="dcslug">Data Center *</Label>
<Select
name="dcslug"
value={formData.dcslug}
onValueChange={(value) => setFormData({...formData, dcslug: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="innoida">Noida</SelectItem>
<SelectItem value="inmumbaizone2">Mumbai Zone 2</SelectItem>
<SelectItem value="indelhi">Delhi</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="cluster_version">Kubernetes Version *</Label>
<Select
name="cluster_version"
value={formData.cluster_version}
onValueChange={(value) => setFormData({...formData, cluster_version: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select version" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1.24">1.24</SelectItem>
<SelectItem value="1.25">1.25</SelectItem>
<SelectItem value="1.26">1.26</SelectItem>
<SelectItem value="1.27">1.27</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Network Type and CPU Model */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="network_type">Network Type *</Label>
<Select
name="network_type"
value={formData.network_type}
onValueChange={(value) => setFormData({...formData, network_type: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select network type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="public">Public</SelectItem>
<SelectItem value="private">Private</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="cpumodel">CPU Model *</Label>
<Select
name="cpumodel"
value={formData.cpumodel}
onValueChange={(value) => setFormData({...formData, cpumodel: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select CPU model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="intel">Intel</SelectItem>
<SelectItem value="amd">AMD</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Node Pool Configuration */}
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-medium">Node Pool Configuration</h3>
{formData.nodepools.map((pool, index) => (
<div key={index} className="p-4 border rounded-lg space-y-4">
<div className="space-y-2">
<Label htmlFor={`pool-label-${index}`}>Pool Label *</Label>
<Input
id={`pool-label-${index}`}
value={pool.label}
onChange={(e) => handleNodePoolChange(index, 'label', e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="cluster_label">Cluster Name *</Label>
<Input
id="cluster_label"
name="cluster_label"
value={formData.cluster_label}
onChange={handleChange}
placeholder="my-cluster"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor={`pool-size-${index}`}>Node Size *</Label>
<Select
value={pool.size}
onValueChange={(value) => handleNodePoolChange(index, 'size', value)}
required
>
<SelectTrigger>
<SelectValue placeholder="Select node size" />
</SelectTrigger>
<SelectContent>
{plans.map(plan => (
<SelectItem key={plan.id} value={plan.id}>
{`${plan.cpu} vCPU, ${Math.floor(parseInt(plan.ram)/1024)}GB RAM`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor={`pool-count-${index}`}>Node Count *</Label>
<Input
id={`pool-count-${index}`}
type="number"
min="1"
value={pool.count}
onChange={(e) => handleNodePoolChange(index, 'count', parseInt(e.target.value))}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor={`pool-maxCount-${index}`}>Max Nodes</Label>
<Input
id={`pool-maxCount-${index}`}
type="number"
min={pool.count}
value={pool.maxCount}
onChange={(e) => handleNodePoolChange(index, 'maxCount', parseInt(e.target.value))}
/>
</div>
</div>
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-lg">Node Pools</Label>
<Button
type="button"
onClick={addNodePool}
variant="outline"
size="sm"
>
Add Node Pool
</Button>
</div>
))}
</div>
{/* VPC and Subnet */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="vpc">VPC *</Label>
<Select
name="vpc"
value={formData.vpc}
onValueChange={handleVpcChange}
required
>
<SelectTrigger>
<SelectValue placeholder="Select VPC" />
</SelectTrigger>
<SelectContent>
{vpcs.map(vpc => (
<SelectItem key={vpc.id} value={vpc.id}>
{vpc.name || vpc.id}
</SelectItem>
))}
</SelectContent>
</Select>
{nodePools.map((pool, poolIndex) => (
<div key={poolIndex} className="p-4 border rounded-lg space-y-4">
<div className="flex justify-between items-center">
<h4 className="font-medium">Node Pool #{poolIndex + 1}</h4>
{nodePools.length > 1 && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => removeNodePool(poolIndex)}
>
Remove
</Button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Pool Label</Label>
<Input
value={pool.label}
onChange={(e) => handleNodePoolChange(poolIndex, 'label', e.target.value)}
placeholder="worker-pool"
/>
</div>
<div className="space-y-2">
<Label>Node Size *</Label>
<Select
value={pool.size}
onValueChange={(value) => handleNodePoolChange(poolIndex, 'size', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select size" />
</SelectTrigger>
<SelectContent>
{availableSizes.map(size => (
<SelectItem key={size.id} value={size.id}>
{size.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Node Count *</Label>
<Input
type="number"
min="1"
value={pool.count}
onChange={(e) => handleNodePoolChange(poolIndex, 'count', parseInt(e.target.value) || 1)}
/>
</div>
</div>
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-md">Storage Volumes</Label>
<Button
type="button"
onClick={() => addDisk(poolIndex)}
variant="outline"
size="sm"
>
Add Volume
</Button>
</div>
{pool.ebs.map((disk, diskIndex) => (
<div key={diskIndex} className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
<div className="space-y-2">
<Label>Size (GB) *</Label>
<Input
type="number"
min="1"
value={disk.disk}
onChange={(e) => handleDiskChange(poolIndex, diskIndex, 'disk', e.target.value)}
placeholder="30"
/>
</div>
<div className="space-y-2">
<Label>Type *</Label>
<Select
value={disk.type}
onValueChange={(value) => handleDiskChange(poolIndex, diskIndex, 'type', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem selected value="nvme">NVMe SSD</SelectItem>
{/* <SelectItem value="ssd">Standard SSD</SelectItem>
<SelectItem value="hdd">HDD</SelectItem> */}
</SelectContent>
</Select>
</div>
<div className="flex justify-end">
{pool.ebs.length > 1 && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => removeDisk(poolIndex, diskIndex)}
>
Remove
</Button>
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
<div className="space-y-2">
<Label htmlFor="subnet">Subnet *</Label>
<Select
name="subnet"
value={formData.subnet}
onValueChange={(value) => setFormData({...formData, subnet: value})}
required
disabled={!formData.vpc || subnets.length === 0}
>
<SelectTrigger>
<SelectValue placeholder={subnets.length === 0 ? "No subnets available" : "Select subnet"}>
{formData.subnet || (subnets.length === 0 ? "No subnets available" : "Select subnet")}
</SelectValue>
</SelectTrigger>
<SelectContent>
{subnets.map(subnet => (
<SelectItem key={subnet.id} value={subnet.id}>
{subnet.name || subnet.id} ({subnet.network}/{subnet.size})
</SelectItem>
))}
</SelectContent>
</Select>
{subnets.length === 0 && formData.vpc && (
<p className="text-sm text-muted-foreground mt-1">
No subnets found in this VPC. Please create a subnet first.
</p>
)}
</div>
</div>
{/* Firewall (Optional) */}
<div className="space-y-2">
<Label htmlFor="firewall">Firewall ID (Optional)</Label>
<Input
id="firewall"
name="firewall"
value={formData.firewall}
onChange={handleChange}
placeholder="Firewall ID"
/>
</div>
<div className="flex justify-end pt-4">
<Button
type="submit"
disabled={isLoading || !formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet}
disabled={isLoading}
className="w-full md:w-auto"
>
{isLoading ? (
<>
<Loader className="mr-2 h-4 w-4" />
<Loader size="sm" className="mr-2" />
Deploying...
</>
) : "Deploy Kubernetes Cluster"}
) : (
"Deploy Cluster"
)}
</Button>
</div>
</form>

View File

@ -9,48 +9,294 @@ import { useToast } from "../ui/toast";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
import { Textarea } from "../ui/textarea";
import { Switch } from "../ui/switch";
import { useIsLoggedIn } from '../../lib/isLoggedIn';
export default function NewCloudInstance() {
const PUBLIC_UTHO_API_KEY = import.meta.env.PUBLIC_UTHO_API_KEY;
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingData, setIsFetchingData] = useState(true);
const [plans, setPlans] = useState([]);
const [sshKeys, setSshKeys] = useState([]);
const [deployError, setDeployError] = useState()
const [deployStatus, setDeployStatus] = useState({});
const [copied, setCopied] = useState(false);
const [plans, setPlans] = useState([
{
"id": "10027",
"type": "cloud",
"slug": "dedicated-cpu",
"disk": "80",
"ram": "4096",
"cpu": "2",
"bandwidth": "1000",
"dedicated_vcore": "1",
"is_available": "YES",
"price": 26.83,
"price_currency": 26.83,
"price_cur": "$26.8"
},
{
"id": "10028",
"type": "cloud",
"slug": "dedicated-cpu",
"disk": "160",
"ram": "8192",
"cpu": "4",
"bandwidth": "2000",
"dedicated_vcore": "1",
"is_available": "YES",
"price": 53.72,
"price_currency": 53.72,
"price_cur": "$53.7"
},
{
"id": "10029",
"type": "cloud",
"slug": "dedicated-cpu",
"disk": "480",
"ram": "32768",
"cpu": "8",
"bandwidth": "3000",
"dedicated_vcore": "1",
"is_available": "YES",
"price": 151.91,
"price_currency": 151.91,
"price_cur": "$151.9"
},
{
"id": "10030",
"type": "cloud",
"slug": "dedicated-cpu",
"disk": "960",
"ram": "65536",
"cpu": "16",
"bandwidth": "4000",
"dedicated_vcore": "1",
"is_available": "YES",
"price": 292.19,
"price_currency": 292.19,
"price_cur": "$292.2"
},
]);
// const [sshKeys, setSshKeys] = useState([]);
const [showAddSshKey, setShowAddSshKey] = useState(false);
const [vpcs, setVpcs] = useState();
const [images, setImages] = useState([
{
"distro": null,
"distribution": "Alma Linux",
"version": "9.2 x86_64",
"image": "almalinux-9.2-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Alma Linux",
"version": "8.4 x86_64",
"image": "AlmaLinux_8.4_x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Centos",
"version": "7.3 x86_64",
"image": "centos-7.3-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Centos",
"version": "8 x86_64",
"image": "centos-8-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Centos",
"version": "7.9 x86_64",
"image": "centos-7.9-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Centos",
"version": "Ameyo x86_64",
"image": "AmeyoCentos7",
"cost": 0
},
{
"distro": null,
"distribution": "Debian",
"version": "9.4 x86_64",
"image": "debian-9.4-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Debian",
"version": "9.13 x86_64",
"image": "debian-9.13-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Debian",
"version": "10 x86_64",
"image": "debian-10-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Debian",
"version": "11 x86_64",
"image": "debian-11-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Debian",
"version": "12 x86_64",
"image": "debian-12-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Fedora",
"version": "34 x86_64",
"image": "fedora-34-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Fedora",
"version": "32 x86_64",
"image": "fedora-32-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Fedora",
"version": "33 x86_64",
"image": "fedora-33-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "FortiOS",
"version": "7.6.2 ",
"image": "fortios-762",
"cost": 0
},
{
"distro": null,
"distribution": "Rocky Linux",
"version": "8.7 x86_64",
"image": "rocky-8.7-86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Rocky Linux",
"version": "8.8 x86_64",
"image": "rocky-8.8-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Rocky Linux",
"version": "9.1 x86_64",
"image": "rocky-9.1-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Rocky Linux",
"version": "9.2 x86_64",
"image": "rocky-9.2-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Ubuntu",
"version": "20.04 x86_64",
"image": "ubuntu-20.04-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Ubuntu",
"version": "18.04 x86_64",
"image": "ubuntu-18.10-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Ubuntu",
"version": "22.04 x86_64",
"image": "ubuntu-22.04-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Windows",
"version": "Server 2012R2 x86_64",
"image": "windows-server2012r2-x86_64",
"cost": 1200
},
{
"distro": null,
"distribution": "Windows",
"version": "Server 2016 x86_64",
"image": "windows-2016",
"cost": 1200
},
{
"distro": null,
"distribution": "Windows",
"version": "Server 2019 x86_64",
"image": "windows2019-10nov2020",
"cost": 1200
},
{
"distro": null,
"distribution": "Windows",
"version": "Server 2022 x86_64",
"image": "windows-2022",
"cost": 1200
}
])
const [formData, setFormData] = useState({
dcslug: "inmumbaizone2",
planid: "",
hostname: "",
image: "debian-12-x86_64",
auth: "ssh_key",
sshkeys: "",
password: "",
enable_publicip: true,
enablebackup: false,
vpc: "bcb68d3d-60f5-495f-9513-6b6a4e53a470",
billingcycle: "hourly"
dcslug: '',
planid: '',
billingcycle: "hourly",
auth: "option2",
enable_publicip: '',
subnetRequired: '',
firewall: "23434645",
cpumodel: "intel",
enablebackup: '',
root_password: '',
support: "unmanaged",
vpc: '',
cloud: [
{
hostname: ''
}
],
image: '',
sshkeys: ''
});
useEffect(() => {
const fetchData = async () => {
try {
// Fetch plans and SSH keys in parallel
const [plansResponse, sshResponse] = await Promise.all([
fetch("https://api.utho.com/v2/plans", {
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
}),
fetch("https://api.utho.com/v2/key", {
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
})
]);
const plansData = await plansResponse.json();
const sshData = await sshResponse.json();
setPlans(plansData.plans || []);
setSshKeys(sshData.key || []);
const vpcResponse = await fetch('https://api.utho.com/v2/vpc', {
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
});
const vpcsData = await vpcResponse.json();
setVpcs(vpcsData.vpc || []);
} catch (error) {
showToast({
title: "Error",
@ -61,9 +307,10 @@ export default function NewCloudInstance() {
setIsFetchingData(false);
}
};
fetchData();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
@ -73,26 +320,37 @@ export default function NewCloudInstance() {
const payload = {
dcslug: formData.dcslug,
planid: formData.planid,
hostname: formData.hostname,
billingcycle: "hourly",
auth: "option2",
enable_publicip: formData.enable_publicip ? true : false,
subnetRequired: false,
firewall: "23434645",
cpumodel: "intel",
enablebackup: formData.enablebackup ? true : false,
root_password: formData.rootPassword,
support: "unmanaged",
vpc: formData.dcslug === 'inmumbaizone2' ? '68c36d65-bda8-43cc-94b5-ae28bb2c3306' : formData.dcslug === 'inbangalore' ? 'c17032c9-3cfd-4028-8f2a-f3f5aa8c2976' : '',
cloud: [
{
hostname: formData.hostname
}
],
image: formData.image,
enable_publicip: formData.enable_publicip ? "true" : "false",
enablebackup: formData.enablebackup ? "true" : "false",
cloud: [{ hostname: formData.hostname }],
vpc: "bcb68d3d-60f5-495f-9513-6b6a4e53a470",
billingcycle: "hourly"
};
sshkeys: formData.sshkeys
};
// inbangalore = c17032c9-3cfd-4028-8f2a-f3f5aa8c2976
// inmumbai = 68c36d65-bda8-43cc-94b5-ae28bb2c3306
// Add authentication based on selected method
if (formData.auth === "ssh_key") {
payload.sshkeys = formData.sshkeys;
} else {
payload.password = formData.password;
}
const response = await fetch("https://api.utho.com/v2/cloud/deploy", {
console.log('payload', payload)
const response = await fetch("https://host-api.cs1.hz.siliconpin.com/v1/vps/?query=deploy&source=chanel_1", {
credentials: "include",
method: "POST",
headers: {
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
@ -101,11 +359,12 @@ export default function NewCloudInstance() {
const data = await response.json();
if (data.status === "success") {
showToast({
title: "Deployment Started",
description: `Server ${data.cloudid} is being deployed with IP ${data.ipv4}`,
variant: "success"
});
setDeployStatus({status : data.status, ip : data.ipv4})
// showToast({
// title: "Deployment Started",
// description: `Server ${data.cloudid} is being deployed with IP ${data.ipv4}`,
// variant: "success"
// });
// Reset form after successful deployment
setFormData(prev => ({
...prev,
@ -116,11 +375,7 @@ export default function NewCloudInstance() {
throw new Error(data.message || "Failed to deploy instance");
}
} catch (error) {
showToast({
title: "Deployment Failed",
description: error.message,
variant: "destructive"
});
setDeployError(error.message)
} finally {
setIsLoading(false);
}
@ -175,17 +430,60 @@ export default function NewCloudInstance() {
}
};
if (isFetchingData) {
const handleCopy = (text) => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1000); // revert after 1 second
};
if (loading || (isLoggedIn && isFetchingData)) {
return <Loader />;
}
if (deployError) return <p>Error: {deployError?.message}</p>;
if (!isLoggedIn) {
return <p className="text-center mt-8">
You are not Logged in. <a href="/login" className="text-[#6d9e37]">Click Here</a> to login first then you can add this service.
</p>;
}
if(deployStatus.status === 'success'){
return (
<>
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Create New Vertual Private Server (VPS)</CardTitle>
<CardDescription>Provide the root password then you can access vps using the password. <br /> later on you can setup ssh key</CardDescription>
</CardHeader>
<CardContent>
<p>Your New VPS</p>
<div className="flex flex-col my-2">
<Label>IP Address:</Label>
<div className="flex">
<Input className="rounded-l-md rounded-r-none text-xl font-bold border-[2px] border-[#6d9e37]" type="text" readOnly value={deployStatus.ip} />
<Button className="rounded-l-none rounded-r-md" onClick={() => handleCopy(deployStatus.ip)}>{copied ? 'Copied!' : 'Copy' }</Button>
</div>
</div>
<div className="flex justify-center place-items-center">
<Button className="" onClick={() => window.location.href = '/services/cloud-instance'}>Back</Button>
</div>
</CardContent>
</Card>
</>
)
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Deploy New Cloud Instance</CardTitle>
<CardDescription>Configure your cloud server with essential parameters</CardDescription>
<CardTitle>Create New Vertual Private Server (VPS)</CardTitle>
<CardDescription>Provide the root password then you can access vps using the password. <br /> later on you can setup ssh key</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="hostname">Hostname *</Label>
@ -211,9 +509,8 @@ export default function NewCloudInstance() {
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="inmumbaizone2">Mumbai Zone 2</SelectItem>
<SelectItem value="innoida">Noida</SelectItem>
<SelectItem value="indelhi">Delhi</SelectItem>
<SelectItem value="inmumbaizone2">Mumbai</SelectItem>
<SelectItem value="inbangalore">Bangalore</SelectItem>
</SelectContent>
</Select>
</div>
@ -230,38 +527,53 @@ export default function NewCloudInstance() {
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
<SelectContent>
{/* {plans.map(plan => (
{plans.map(plan => (
<SelectItem key={plan.id} value={plan.id}>
{`${plan.cpu} vCPU, ${plan.ram}MB RAM - ${plan.price_cur}/mo`}
</SelectItem>
))} */}
<SelectItem key={plans[0].id} value={plans[0].id}>
))}
{/* <SelectItem key={plans[0].id} value={plans[0].id}>
{`${plans[0].cpu} vCPU, ${plans[0].ram}MB RAM - ${plans[0].price_cur}/mo`}
</SelectItem>
</SelectItem> */}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="image">Operating System *</Label>
<Select
name="image"
value={formData.image}
onValueChange={(value) => setFormData({...formData, image: value})}>
<SelectTrigger>
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
{
images.map((image, i) => (
<SelectItem key={i} value={image.image}>{image.image.split('-x86_64')[0]}</SelectItem>
))
}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="image">Operating System *</Label>
<Select
name="image"
value={formData.image}
onValueChange={(value) => setFormData({...formData, image: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
<SelectItem value="debian-12-x86_64">Debian 12</SelectItem>
<SelectItem value="ubuntu-22.04">Ubuntu 22.04</SelectItem>
<SelectItem value="centos-7.4-x86_64">CentOS 7.4</SelectItem>
</SelectContent>
</Select>
<div className="space-y-2">
<Label htmlFor="password">Root Password *</Label>
<Input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="Enter root password"
required={formData.auth === "password"}
/>
</div>
</div>
<div className="space-y-4">
{/* <div className="space-y-4">
<h3 className="text-lg font-medium">Authentication</h3>
<Tabs
value={formData.auth}
@ -332,7 +644,7 @@ export default function NewCloudInstance() {
</Button>
</div>
)}
</TabsContent>
</TabsContent>
<TabsContent value="password" className="space-y-2">
<div className="space-y-2">
@ -349,7 +661,7 @@ export default function NewCloudInstance() {
</div>
</TabsContent>
</Tabs>
</div>
</div> */}
<div className="space-y-4">
<h3 className="text-lg font-medium">Options</h3>

View File

@ -0,0 +1,615 @@
import React, { useState, useEffect } from "react";
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import Loader from "../ui/loader";
import { useToast } from "../ui/toast";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
import { Textarea } from "../ui/textarea";
import { Switch } from "../ui/switch";
export default function NewCloudInstance() {
const PUBLIC_UTHO_API_KEY = import.meta.env.PUBLIC_UTHO_API_KEY;
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingData, setIsFetchingData] = useState(true);
const [plans, setPlans] = useState([]);
const [sshKeys, setSshKeys] = useState([]);
const [showAddSshKey, setShowAddSshKey] = useState(false);
const [vpcs, setVpcs] = useState();
const [images, setImages] = useState([
{
"distro": null,
"distribution": "Alma Linux",
"version": "9.2 x86_64",
"image": "almalinux-9.2-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Alma Linux",
"version": "8.4 x86_64",
"image": "AlmaLinux_8.4_x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Centos",
"version": "7.3 x86_64",
"image": "centos-7.3-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Centos",
"version": "8 x86_64",
"image": "centos-8-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Centos",
"version": "7.9 x86_64",
"image": "centos-7.9-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Centos",
"version": "Ameyo x86_64",
"image": "AmeyoCentos7",
"cost": 0
},
{
"distro": null,
"distribution": "Debian",
"version": "9.4 x86_64",
"image": "debian-9.4-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Debian",
"version": "9.13 x86_64",
"image": "debian-9.13-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Debian",
"version": "10 x86_64",
"image": "debian-10-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Debian",
"version": "11 x86_64",
"image": "debian-11-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Debian",
"version": "12 x86_64",
"image": "debian-12-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Fedora",
"version": "34 x86_64",
"image": "fedora-34-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Fedora",
"version": "32 x86_64",
"image": "fedora-32-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Fedora",
"version": "33 x86_64",
"image": "fedora-33-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "FortiOS",
"version": "7.6.2 ",
"image": "fortios-762",
"cost": 0
},
{
"distro": null,
"distribution": "Rocky Linux",
"version": "8.7 x86_64",
"image": "rocky-8.7-86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Rocky Linux",
"version": "8.8 x86_64",
"image": "rocky-8.8-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Rocky Linux",
"version": "9.1 x86_64",
"image": "rocky-9.1-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Rocky Linux",
"version": "9.2 x86_64",
"image": "rocky-9.2-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Ubuntu",
"version": "20.04 x86_64",
"image": "ubuntu-20.04-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Ubuntu",
"version": "18.04 x86_64",
"image": "ubuntu-18.10-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Ubuntu",
"version": "22.04 x86_64",
"image": "ubuntu-22.04-x86_64",
"cost": 0
},
{
"distro": null,
"distribution": "Windows",
"version": "Server 2012R2 x86_64",
"image": "windows-server2012r2-x86_64",
"cost": 1200
},
{
"distro": null,
"distribution": "Windows",
"version": "Server 2016 x86_64",
"image": "windows-2016",
"cost": 1200
},
{
"distro": null,
"distribution": "Windows",
"version": "Server 2019 x86_64",
"image": "windows2019-10nov2020",
"cost": 1200
},
{
"distro": null,
"distribution": "Windows",
"version": "Server 2022 x86_64",
"image": "windows-2022",
"cost": 1200
}
])
const [formData, setFormData] = useState({
dcslug: '',
planid: '',
billingcycle: "hourly",
auth: "option2",
enable_publicip: '',
subnetRequired: '',
firewall: "23434645",
cpumodel: "intel",
enablebackup: '',
root_password: '',
support: "unmanaged",
vpc: '',
cloud: [
{
hostname: ''
}
],
image: '',
sshkeys: ''
});
useEffect(() => {
const fetchData = async () => {
try {
// Fetch plans and SSH keys in parallel
const [plansResponse, sshResponse, vpcLists] = await Promise.all([
fetch("https://api.utho.com/v2/plans", {
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
}),
fetch("https://api.utho.com/v2/key", {
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
}),
fetch('https://api.utho.com/v2/vpc', {
headers: {"Authorization" : `Bearer ${PUBLIC_UTHO_API_KEY}`}
})
]);
const plansData = await plansResponse.json();
const sshData = await sshResponse.json();
const vpcsData = await vpcLists.json();
setPlans(plansData.plans || []);
setSshKeys(sshData.key || []);
setVpcs(vpcsData.vpc || []);
} catch (error) {
showToast({
title: "Error",
description: "Failed to load configuration data",
variant: "destructive"
});
} finally {
setIsFetchingData(false);
}
};
fetchData();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
const payload = {
dcslug: formData.dcslug,
planid: formData.planid,
billingcycle: "hourly",
auth: "option2",
enable_publicip: formData.enable_publicip ? true : false,
subnetRequired: false,
firewall: "23434645",
cpumodel: "intel",
enablebackup: formData.enablebackup ? true : false,
root_password: formData.rootPassword,
support: "unmanaged",
vpc: 'c17032c9-3cfd-4028-8f2a-f3f5aa8c2976',
cloud: [
{
hostname: formData.hostname
}
],
image: formData.image,
sshkeys: formData.sshkeys
};
// Add authentication based on selected method
if (formData.auth === "ssh_key") {
payload.sshkeys = formData.sshkeys;
} else {
payload.password = formData.password;
}
console.log('payload', payload)
const response = await fetch("https://host-api.cs1.hz.siliconpin.com/v1/vps/?query=deploy&source=chanel_1", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.status === "success") {
showToast({
title: "Deployment Started",
description: `Server ${data.cloudid} is being deployed with IP ${data.ipv4}`,
variant: "success"
});
// Reset form after successful deployment
setFormData(prev => ({
...prev,
hostname: "",
planid: ""
}));
} else {
throw new Error(data.message || "Failed to deploy instance");
}
} catch (error) {
showToast({
title: "Deployment Failed",
description: error.message,
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
};
const handleAddSshKey = async () => {
try {
const response = await fetch('https://api.utho.com/v2/key/import', {
method: "POST",
headers: {
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: formData.newSshKeyName,
sshkey: formData.newSshKeyValue
})
});
const data = await response.json();
if (data.status === 'success') {
// Refresh SSH keys list
const sshResponse = await fetch("https://api.utho.com/v2/key", {
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
});
const sshData = await sshResponse.json();
setSshKeys(sshData.key || []);
showToast({
title: "SSH Key Added",
description: "Your SSH key has been successfully added",
variant: "success"
});
setShowAddSshKey(false);
}
} catch (error) {
showToast({
title: "Error",
description: "Failed to add SSH key",
variant: "destructive"
});
}
};
if (isFetchingData) {
return <Loader />;
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Deploy New Cloud Instance</CardTitle>
<CardDescription>Configure your cloud server with essential parameters</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="hostname">Hostname *</Label>
<Input
id="hostname"
name="hostname"
value={formData.hostname}
onChange={handleChange}
placeholder="server1.example.com"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="dcslug">Data Center *</Label>
<Select
name="dcslug"
value={formData.dcslug}
onValueChange={(value) => setFormData({...formData, dcslug: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="inmumbaizone2">Mumbai</SelectItem>
<SelectItem value="inbangalore">Bangalore</SelectItem>
<SelectItem value="innoida">Delhi (Noida)</SelectItem>
<SelectItem value="defra1">Germany</SelectItem>
<SelectItem value="uslosangeles">Los Angeles</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="planid">Plan *</Label>
<Select
name="planid"
value={formData.planid}
onValueChange={(value) => setFormData({...formData, planid: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
<SelectContent>
{plans.map(plan => (
<SelectItem key={plan.id} value={plan.id}>
{`${plan.cpu} vCPU, ${plan.ram}MB RAM - ${plan.price_cur}/mo`}
</SelectItem>
))}
{/* <SelectItem key={plans[0].id} value={plans[0].id}>
{`${plans[0].cpu} vCPU, ${plans[0].ram}MB RAM - ${plans[0].price_cur}/mo`}
</SelectItem> */}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="image">Operating System *</Label>
<Select
name="image"
value={formData.image}
onValueChange={(value) => setFormData({...formData, image: value})}>
<SelectTrigger>
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
{
images.map((image, i) => (
<SelectItem key={i} value={image.image}>{image.image.split('-x86_64')[0]}</SelectItem>
))
}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="vpc">VPC *</Label>
<Select
name="vpc"
value={formData.vpc}
onValueChange={(value) => setFormData({...formData, vpc: value})}>
<SelectTrigger>
<SelectValue placeholder="Select VPC" />
</SelectTrigger>
<SelectContent>
{
vpcs.map((vpc, n) => (
<SelectItem key={n} value={`${vpc.id}`}>{vpc.dclocation.location}</SelectItem>
))
}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">Authentication</h3>
<Tabs
value={formData.auth}
onValueChange={(value) => setFormData({...formData, auth: value})}
className="w-full">
<TabsList className="grid w-full grid-cols-2 gap-4">
<TabsTrigger className={`p-2 rounded-md ${formData.auth === 'ssh_key' ? 'bg-[#6d9e37] text-[#FFF]' : 'border border-[#6d9e37]'}`} value="ssh_key">SSH Key</TabsTrigger>
<TabsTrigger className={`p-2 rounded-md ${formData.auth === 'password' ? 'bg-[#6d9e37] text-[#FFF]' : 'border border-[#6d9e37]'}`} value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="ssh_key" className="space-y-4">
<div className="space-y-2">
<Label>Select SSH Key</Label>
<div className="flex gap-2">
<Select
value={formData.sshkeys}
onValueChange={(value) => setFormData({...formData, sshkeys: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select SSH key" />
</SelectTrigger>
<SelectContent>
{sshKeys.map(key => (
<SelectItem key={key.id} value={key.id}>{key.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="outline"
className="whitespace-nowrap"
onClick={() => setShowAddSshKey(!showAddSshKey)}
>
{showAddSshKey ? "Cancel" : "Add New"}
</Button>
</div>
</div>
{showAddSshKey && (
<div className="space-y-4 p-4 border rounded-lg">
<div className="space-y-2">
<Label htmlFor="newSshKeyName">Key Name</Label>
<Input
id="newSshKeyName"
name="newSshKeyName"
value={formData.newSshKeyName}
onChange={handleChange}
placeholder="My Laptop Key"
/>
</div>
<div className="space-y-2">
<Label htmlFor="newSshKeyValue">Public Key</Label>
<Textarea
id="newSshKeyValue"
name="newSshKeyValue"
value={formData.newSshKeyValue}
onChange={handleChange}
placeholder="ssh-rsa AAAAB3NzaC1yc2E..."
rows={4}
/>
</div>
<Button
type="button"
onClick={handleAddSshKey}
disabled={!formData.newSshKeyName || !formData.newSshKeyValue}
>
Add SSH Key
</Button>
</div>
)}
</TabsContent>
<TabsContent value="password" className="space-y-2">
<div className="space-y-2">
<Label htmlFor="password">Root Password *</Label>
<Input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="Enter root password"
required={formData.auth === "password"}
/>
</div>
</TabsContent>
</Tabs>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">Options</h3>
<div className="flex flex-col gap-4">
<div className="flex items-center space-x-2">
<Switch
id="enable_publicip"
checked={formData.enable_publicip}
onCheckedChange={(checked) => setFormData({...formData, enable_publicip: checked})}
/>
<Label htmlFor="enable_publicip">Enable Public IP</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="enablebackup"
checked={formData.enablebackup}
onCheckedChange={(checked) => setFormData({...formData, enablebackup: checked})}
/>
<Label htmlFor="enablebackup">Enable Weekly Backups (+20%)</Label>
</div>
</div>
</div>
<div className="flex justify-end pt-4">
<Button type="submit" disabled={isLoading} className="w-full md:w-auto">
{isLoading ? "Deploying..." : "Deploy Instance"}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,43 @@
const payload = {
"dcslug": "inbangalore",
"planid": "10045",
"billingcycle": "hourly",
"auth": "option2",
"enable_publicip": "true",
"subnetRequired": "false",
"firewall": "23434645",
"cpumodel": "intel",
"enablebackup": "false",
"root_password": "5qT381g@K@pDsq",
"support": "unmanaged",
"vpc": "c17032c9-3cfd-4028-8f2a-f3f5aa8c2976",
"cloud": [
{
"hostname": "cloudserver-ep0yNFPI.mhc"
}
],
"image": "debian-9.4-x86_64",
"sshkeys": "74131237"
};
// const payload = {
// dcslug: formData.dcslug,
// planid: formData.planid,
// billingcycle: "hourly",
// auth: "option2",
// enable_publicip: formData.enable_publicip ? true : false,
// subnetRequired: false,
// firewall: "23434645",
// cpumodel: "intel",
// enablebackup: formData.enablebackup ? true : false,
// root_password: formData.rootPassword,
// support: "unmanaged",
// vpc: formData.vpc,
// cloud: [
// {
// hostname: formData.hostname
// }
// ],
// image: "debian-9.4-x86_64",
// sshkeys: formData.sshkeys
// };

View File

@ -54,7 +54,7 @@ const CommentSystem = ({ topicId, pbId }) => {
params.append('pb_id', pb_id);
}
const response = await fetch(`/api/comments?${params.toString()}`);
const response = await fetch(`https://sp-comments-prod-pocndhvgmcnbacgb.siliconpin.com/comments?${params.toString()}`);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
@ -89,9 +89,9 @@ const CommentSystem = ({ topicId, pbId }) => {
payload.parent_comment_id = parentId;
}
const endpoint = parentId ? '/api/comments/reply' : '/api/comments';
const endpoint = parentId ? '/comments/reply' : '/comments';
const response = await fetch(endpoint, {
const response = await fetch(`https://sp-comments-prod-pocndhvgmcnbacgb.siliconpin.com${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)

View File

@ -104,7 +104,7 @@ const LoginPage = () => {
setStatus({ message: '', isError: false });
const authData = await pb.collection('users').authWithOAuth2({ provider });
console.log('Authdata', authData);
if (!authData?.record) {
throw new Error("No user record found");
}
@ -344,6 +344,14 @@ const LoginPage = () => {
>
<img src="/assets/github.svg" alt="GitHub" className="h-6 w-6" />
</Button>
{/* <Button
variant="outline"
onClick={() => loginWithOAuth2('gitea')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/gitea.svg" alt="GitHub" className="h-6 w-6" />
</Button> */}
</div>
</CardContent>

View File

@ -7,8 +7,12 @@ import { Button } from './ui/button';
import { Separator } from './ui/separator';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import { CustomTabs } from './ui/tabs';
import Cookies from 'js-cookie';
const PUBLIC_MINIO_UPLOAD_URL = 'https://file-vault-prod-pdgctwgsnkiy.siliconpin.com/upload';
const IMAGE_URL_PREFIX = 'https://file-delivery.ndgctwgsnkiy.siliconpin.com/';
// const PUBLIC_MINIO_UPLOAD_URL = import.meta.env.PUBLIC_MINIO_UPLOAD_URL;
const PUBLIC_MINIO_UPLOAD_URL = import.meta.env.PUBLIC_MINIO_UPLOAD_URL;
const PUBLIC_TOPIC_API_URL = import.meta.env.PUBLIC_TOPIC_API_URL;
const NewTopic = () => {
@ -27,26 +31,30 @@ const NewTopic = () => {
const [imageUploadPreview, setImageUploadPreview] = useState('');
const [editorMode, setEditorMode] = useState('edit');
// Upload file to MinIO
const uploadToMinIO = async (file, onProgress) => {
const siliconId = Cookies.get('siliconId'); // Get from cookie or anywhere
const token = Cookies.get('token'); // Get from cookie or storage
const formData = new FormData();
formData.append('file', file);
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
try {
const response = await fetch(PUBLIC_MINIO_UPLOAD_URL, {
method: 'POST',
body: formData,
credentials: 'include'
headers: {
'x-user-data': siliconId,
'Authorization': token,
},
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
throw new Error(`Upload failed: ${response.statusText}`);
}
const data = await response.json();
console.log(data.publicUrl)
return data.publicUrl;
// console.log('Upload successful:', data);
return data;
} catch (error) {
console.error('Upload error:', error);
throw error;
@ -77,14 +85,48 @@ const NewTopic = () => {
setImageDialogOpen(true);
},
};
// Custom <br> insert command
const lineBreakCommand = {
name: 'linebreak',
keyCommand: 'linebreak',
buttonProps: { 'aria-label': 'Insert line break' },
icon: (
<svg width="16px" height="16px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M20 7V8.2C20 9.88016 20 10.7202 19.673 11.362C19.3854 11.9265 18.9265 12.3854 18.362 12.673C17.7202 13 16.8802 13 15.2 13H4M4 13L8 9M4 13L8 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>
),
execute: () => {
const textarea = document.querySelector('.w-md-editor-text-input');
if (!textarea) return;
// Get all default commands and replace the image command
const allCommands = commands.getCommands().map(cmd => {
if (cmd.name === 'image') {
return customImageCommand;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const value = formData.content || "";
const breakTag = '<br>';
const newValue = value.slice(0, start) + breakTag + value.slice(end);
setFormData(prev => ({
...prev,
content: newValue
}));
setTimeout(() => {
textarea.selectionStart = textarea.selectionEnd = start + breakTag.length;
textarea.focus();
}, 0);
}
return cmd;
});
};
// Get all commands (same as NewTopic)
const allCommands = [
...commands.getCommands().map(cmd => {
if (cmd.name === 'image') {
return customImageCommand;
}
return cmd;
}),
lineBreakCommand,
];
// Handle image URL insertion
@ -137,7 +179,7 @@ const NewTopic = () => {
});
// Insert markdown for the uploaded image
const imgMarkdown = `![Image](${uploadedUrl})`;
const imgMarkdown = `![Image](${uploadedUrl.url})`;
const textarea = document.querySelector('.w-md-editor-text-input');
if (textarea) {
const startPos = textarea.selectionStart;
@ -171,14 +213,14 @@ const NewTopic = () => {
try {
// Upload featured image if selected
let imageUrl = formData.imageUrl;
let imageUrl = formData.imageUrl.url;
if (imageFile) {
imageUrl = await uploadToMinIO(imageFile, (progress) => {
console.log('imageUrl', imageUrl)
setUploadProgress(progress);
});
}
// let finalImageUrl = imageUrl.publicUrl;
imageUrl = imageUrl.url;
// Prepare payload
const payload = {
...formData,
@ -296,6 +338,7 @@ const NewTopic = () => {
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent position="popper" className="z-50">
<SelectItem value="uncategorized">Uncategorized</SelectItem>
<SelectItem value="php">PHP Hosting</SelectItem>
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
</SelectContent>

View File

@ -0,0 +1,144 @@
import React, { useState } from 'react';
const ImageUploadTest = () => {
const [file, setFile] = useState(null);
const [preview, setPreview] = useState('');
const [status, setStatus] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
if (!selectedFile) return;
// Check if file is an image
if (!selectedFile.type.match('image.*')) {
setStatus('Error: Please select an image file');
return;
}
setFile(selectedFile);
setStatus('');
// Create preview
const reader = new FileReader();
reader.onload = () => setPreview(reader.result);
reader.readAsDataURL(selectedFile);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!file) {
setStatus('Error: No file selected');
return;
}
setIsLoading(true);
setStatus('Uploading...');
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/check/', {
method: 'POST',
body: formData,
credentials: 'include'
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const result = await response.text();
setStatus(`Success: ${result}`);
} catch (error) {
setStatus(`Error: ${error.message}`);
} finally {
setIsLoading(false);
}
};
return (
<div style={{ maxWidth: '500px', margin: '20px auto', padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
<h2>Image Upload Test</h2>
<form onSubmit={handleSubmit} method='POST' action={'https://file-vault.ndgctwgsnkiy.siliconpin.com/upload'} encType="multipart/form-data">
<div style={{ margin: '20px 0' }}>
<input
type="file"
accept="image/*"
onChange={handleFileChange}
style={{ display: 'none' }}
id="file-upload"
required
/>
<label htmlFor="file-upload" style={{
display: 'inline-block',
padding: '10px 15px',
backgroundColor: '#f0f0f0',
borderRadius: '4px',
cursor: 'pointer',
border: '1px solid #ccc'
}}>
Choose Image
</label>
{file && (
<span style={{ marginLeft: '10px' }}>{file.name}</span>
)}
</div>
{preview && (
<div style={{ margin: '20px 0' }}>
<img
src={preview}
alt="Preview"
style={{ maxWidth: '100%', maxHeight: '200px', borderRadius: '4px' }}
/>
</div>
)}
<button
type="submit"
disabled={!file || isLoading}
style={{
padding: '10px 20px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
opacity: (!file || isLoading) ? 0.6 : 1
}}
>
{isLoading ? 'Uploading...' : 'Upload Image'}
</button>
</form>
{status && (
<div style={{
marginTop: '20px',
padding: '10px',
borderRadius: '4px',
backgroundColor: status.startsWith('Error') ? '#ffebee' : '#e8f5e9',
color: status.startsWith('Error') ? '#c62828' : '#2e7d32'
}}>
{status}
</div>
)}
<div style={{ marginTop: '20px', fontSize: '0.9em', color: '#666' }}>
<p>Note: This component tests uploading to:</p>
<code>https://file-vault.ndgctwgsnkiy.siliconpin.com/upload</code>
<p>Make sure:</p>
<ul>
<li>Your Go server is running</li>
<li>CORS is properly configured</li>
<li>You have the <code>siliconId</code> cookie set</li>
</ul>
</div>
</div>
);
};
export default ImageUploadTest;

View File

@ -0,0 +1,445 @@
import React, { useState, useRef, useEffect } from 'react';
import { Button } from '../ui/button';
const FreeTextToSpeech = () => {
// State management
const [text, setText] = useState('');
const [voices, setVoices] = useState([]);
const [selectedVoice, setSelectedVoice] = useState('');
const [pitch, setPitch] = useState(1);
const [rate, setRate] = useState(1);
const [volume, setVolume] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [status, setStatus] = useState('Enter text to convert to speech');
const [isSpeaking, setIsSpeaking] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [copied, setCopied] = useState(false);
const [debugLogs, setDebugLogs] = useState([]);
const [showDebug, setShowDebug] = useState(false);
// Refs
const synthRef = useRef(null);
const utteranceRef = useRef(null);
const audioRef = useRef(null);
const textareaRef = useRef(null);
// Debug logging
const addDebugLog = (message) => {
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const logMessage = `${timestamp}: ${message}`;
setDebugLogs(prev => [...prev.slice(-100), logMessage]);
console.debug(logMessage);
};
// Initialize speech synthesis
useEffect(() => {
if (typeof window !== 'undefined') {
synthRef.current = window.speechSynthesis;
const loadVoices = () => {
const availableVoices = synthRef.current.getVoices();
setVoices(availableVoices);
if (availableVoices.length > 0) {
setSelectedVoice(availableVoices[0].name);
}
addDebugLog(`Loaded ${availableVoices.length} voices`);
};
// Chrome loads voices asynchronously
synthRef.current.onvoiceschanged = loadVoices;
loadVoices();
return () => {
if (synthRef.current) {
synthRef.current.onvoiceschanged = null;
}
};
}
}, []);
// Handle speaking
const handleSpeak = () => {
if (!text.trim()) {
setError('Please enter some text to speak');
setStatus('Please enter some text to speak');
addDebugLog('No text provided for speech');
return;
}
if (!synthRef.current) {
setError('Speech synthesis not supported in this browser');
setStatus('Speech synthesis not supported');
addDebugLog('Speech synthesis API not available');
return;
}
if (isPaused) {
synthRef.current.resume();
setIsPaused(false);
setIsSpeaking(true);
setStatus('Resumed speaking');
addDebugLog('Resumed speech playback');
return;
}
// Cancel any current speech
synthRef.current.cancel();
// Create a new utterance
const utterance = new SpeechSynthesisUtterance(text);
utteranceRef.current = utterance;
// Set utterance properties
const selectedVoiceObj = voices.find(v => v.name === selectedVoice);
if (selectedVoiceObj) {
utterance.voice = selectedVoiceObj;
addDebugLog(`Using voice: ${selectedVoiceObj.name} (${selectedVoiceObj.lang})`);
}
utterance.pitch = pitch;
utterance.rate = rate;
utterance.volume = volume;
// Event handlers
utterance.onstart = () => {
setIsSpeaking(true);
setIsPaused(false);
setStatus('Speaking...');
addDebugLog('Speech started');
};
utterance.onend = () => {
setIsSpeaking(false);
setIsPaused(false);
setStatus('Finished speaking');
addDebugLog('Speech completed');
};
utterance.onerror = (event) => {
setIsSpeaking(false);
setIsPaused(false);
setStatus(`Error occurred: ${event.error}`);
addDebugLog(`Speech error: ${event.error}`);
};
// Speak the utterance
synthRef.current.speak(utterance);
setStatus('Started speaking...');
addDebugLog('Initiated speech synthesis');
};
const handlePause = () => {
if (isSpeaking && !isPaused && synthRef.current) {
synthRef.current.pause();
setIsPaused(true);
setIsSpeaking(false);
setStatus('Paused');
addDebugLog('Speech paused');
}
};
const handleStop = () => {
if (synthRef.current) {
synthRef.current.cancel();
setIsSpeaking(false);
setIsPaused(false);
setStatus('Stopped speaking');
addDebugLog('Speech stopped');
}
};
const handleDownload = () => {
setStatus('Download requires a TTS API service');
setError('Download functionality requires a TTS API service');
addDebugLog('Download attempted (not implemented)');
};
// Copy text to clipboard
const copyToClipboard = () => {
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
addDebugLog('Text copied to clipboard');
}).catch(err => {
const errorMsg = 'Failed to copy text to clipboard';
addDebugLog(`${errorMsg}: ${err.message}`);
setError(errorMsg);
});
};
// Clear all inputs and states
const clearAll = () => {
setText('');
setError(null);
setStatus('Enter text to convert to speech');
if (synthRef.current) {
synthRef.current.cancel();
}
setIsSpeaking(false);
setIsPaused(false);
addDebugLog('All inputs and states cleared');
};
// Helper functions
const clearDebugLogs = () => {
setDebugLogs([]);
addDebugLog('Debug logs cleared');
};
const toggleDebug = () => {
setShowDebug(!showDebug);
addDebugLog(`Debug panel ${showDebug ? 'hidden' : 'shown'}`);
};
return (
<div className="container mx-auto px-4 max-w-4xl my-6">
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">Text to Speech (Using Google)</h1>
<Button
onClick={toggleDebug}
variant="outline"
size="sm"
>
{showDebug ? 'Hide Debug' : 'Show Debug'}
</Button>
</div>
{/* Text Input Section */}
<div className="mb-6">
<div className="flex justify-between items-center mb-2">
<label htmlFor="tts-text" className="block text-sm font-medium text-gray-700">
Enter Text
</label>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={copyToClipboard}
disabled={!text || isSpeaking}
>
{copied ? 'Copied!' : 'Copy Text'}
</Button>
<Button
variant="outline"
size="sm"
onClick={clearAll}
disabled={isSpeaking}
>
Clear All
</Button>
</div>
</div>
<textarea
ref={textareaRef}
id="tts-text"
rows={6}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:border-[#6d9e37] bg-white text-gray-800"
placeholder="Type or paste your text here..."
value={text}
onChange={(e) => setText(e.target.value)}
disabled={isSpeaking}
/>
</div>
{/* Controls */}
<div className="mb-6 p-4 border border-gray-200 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Voice Selection */}
<div>
<label htmlFor="voice-select" className="block text-sm font-medium text-gray-700 mb-1">
Select Voice
</label>
<select
id="voice-select"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:border-[#6d9e37]"
value={selectedVoice}
onChange={(e) => setSelectedVoice(e.target.value)}
disabled={isSpeaking}
>
{voices.length > 0 ? (
voices.map((voice) => (
<option key={voice.name} value={voice.name}>
{voice.name} ({voice.lang})
</option>
))
) : (
<option value="">Loading voices...</option>
)}
</select>
</div>
{/* Pitch Control */}
<div>
<label htmlFor="pitch" className="block text-sm font-medium text-gray-700 mb-1">
Pitch: {pitch.toFixed(1)}
</label>
<input
type="range"
id="pitch"
min="0.5"
max="2"
step="0.1"
value={pitch}
onChange={(e) => setPitch(parseFloat(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-[#6d9e37]"
disabled={isSpeaking}
/>
</div>
{/* Rate Control */}
<div>
<label htmlFor="rate" className="block text-sm font-medium text-gray-700 mb-1">
Speed: {rate.toFixed(1)}
</label>
<input
type="range"
id="rate"
min="0.5"
max="2"
step="0.1"
value={rate}
onChange={(e) => setRate(parseFloat(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-[#6d9e37]"
disabled={isSpeaking}
/>
</div>
{/* Volume Control */}
<div>
<label htmlFor="volume" className="block text-sm font-medium text-gray-700 mb-1">
Volume: {volume.toFixed(1)}
</label>
<input
type="range"
id="volume"
min="0"
max="1"
step="0.1"
value={volume}
onChange={(e) => setVolume(parseFloat(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-[#6d9e37]"
disabled={isSpeaking}
/>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-wrap gap-3 mb-6">
<Button
onClick={handleSpeak}
disabled={!text.trim() || (isSpeaking && !isPaused)}
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
>
{isPaused ? 'Resume' : 'Speak'}
</Button>
<Button
onClick={handlePause}
disabled={!isSpeaking || isPaused}
variant="outline"
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
>
Pause
</Button>
<Button
onClick={handleStop}
disabled={!isSpeaking && !isPaused}
variant="outline"
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
>
Stop
</Button>
<Button
onClick={handleDownload}
variant="outline"
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
>
Download Audio
</Button>
</div>
{/* Status */}
<div className="mb-6 text-center">
<p className={`text-sm ${
error ? 'text-red-600' :
isSpeaking ? 'text-[#6d9e37]' :
isPaused ? 'text-amber-600' :
'text-gray-600'
}`}>
{status}
</p>
</div>
{/* Error Display */}
{error && (
<div className="mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-center text-red-600">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 className="font-medium">Error</h3>
</div>
<p className="mt-2 text-sm text-red-600">{error}</p>
</div>
)}
{/* Debug Section */}
{showDebug && (
<div className="mt-8 border rounded-lg overflow-hidden">
<div className="bg-gray-50 px-4 py-3 border-b flex justify-between items-center">
<h3 className="font-medium text-gray-700">Debug Logs</h3>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={clearDebugLogs}
className="text-sm">
Clear Logs
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
const blob = new Blob([debugLogs.join('\n')], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `tts-debug-${new Date().toISOString()}.log`;
a.click();
URL.revokeObjectURL(url);
}}
className="text-sm">
Export Logs
</Button>
</div>
</div>
<div className="p-4 bg-white max-h-60 overflow-y-auto">
{debugLogs.length > 0 ? (
<div className="space-y-1">
{debugLogs.map((log, index) => (
<div key={index} className="text-xs font-mono text-gray-800 border-b border-gray-100 pb-1">
{log}
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500">No debug logs yet. Interactions will appear here.</p>
)}
</div>
</div>
)}
</div>
</div>
);
};
export default FreeTextToSpeech;

View File

@ -0,0 +1,375 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { Button } from '../ui/button';
const TTS_API_ENDPOINT = 'https://tts41-nhdtuisbdhcvdth.siliconpin.com/tts';
export default function TextToSpeech() {
// State management
const [text, setText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [audioUrl, setAudioUrl] = useState(null);
const [status, setStatus] = useState('Enter text to convert to speech');
const [isPlaying, setIsPlaying] = useState(false);
const [copied, setCopied] = useState(false);
const [debugLogs, setDebugLogs] = useState([]);
const [showDebug, setShowDebug] = useState(false);
// Refs
const audioRef = useRef(null);
const textareaRef = useRef(null);
// Debug logging
const addDebugLog = useCallback((message) => {
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const logMessage = `${timestamp}: ${message}`;
setDebugLogs(prev => [...prev.slice(-100), logMessage]);
console.debug(logMessage);
}, []);
// Clean up audio URL on unmount
useEffect(() => {
return () => {
if (audioUrl) {
URL.revokeObjectURL(audioUrl);
addDebugLog('Component unmounting - revoking audio URL');
}
};
}, [audioUrl, addDebugLog]);
// Handle text to speech conversion
const convertToSpeech = useCallback(async () => {
if (!text.trim()) {
setError('Please enter some text to convert');
addDebugLog('No text provided for conversion');
return;
}
try {
setIsLoading(true);
setStatus('Converting text to speech...');
setError(null);
addDebugLog(`Starting TTS conversion for text: "${text.substring(0, 20)}..."`);
const response = await fetch(TTS_API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: text.trim() }),
});
if (!response.ok) {
const errorMsg = `API returned error status: ${response.status}`;
addDebugLog(errorMsg);
throw new Error(errorMsg);
}
const audioBlob = await response.blob();
const url = URL.createObjectURL(audioBlob);
setAudioUrl(url);
setStatus('Conversion complete. Ready to play or download.');
addDebugLog(`TTS conversion successful. Audio blob size: ${(audioBlob.size / 1024).toFixed(2)} KB`);
} catch (err) {
const errorMsg = err.message.includes('Failed to fetch')
? 'Network error: Could not connect to the TTS server'
: err.message;
addDebugLog(`Error during TTS conversion: ${errorMsg}`);
setError(errorMsg);
setStatus('Conversion failed');
} finally {
setIsLoading(false);
addDebugLog('TTS conversion process completed');
}
}, [text, addDebugLog]);
// Handle audio download
const downloadAudio = useCallback(() => {
if (!audioUrl) return;
addDebugLog('Initiating audio download');
const a = document.createElement('a');
a.href = audioUrl;
a.download = `tts-output-${new Date().toISOString().slice(0, 10)}.wav`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setStatus('Audio download started');
addDebugLog('Audio download initiated');
}, [audioUrl, addDebugLog]);
// Handle audio playback
const togglePlayback = useCallback(() => {
if (!audioUrl) return;
if (isPlaying) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
setIsPlaying(false);
setStatus('Playback stopped');
addDebugLog('Playback stopped');
} else {
audioRef.current.play()
.then(() => {
setIsPlaying(true);
setStatus('Playing audio...');
addDebugLog('Playback started');
})
.catch(err => {
setError('Failed to play audio');
addDebugLog(`Playback error: ${err.message}`);
});
}
}, [audioUrl, isPlaying, addDebugLog]);
// Handle audio playback end
const handleAudioEnd = useCallback(() => {
setIsPlaying(false);
setStatus('Playback finished');
addDebugLog('Playback finished naturally');
}, [addDebugLog]);
// Copy text to clipboard
const copyToClipboard = useCallback(() => {
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
addDebugLog('Text copied to clipboard');
}).catch(err => {
const errorMsg = 'Failed to copy text to clipboard';
addDebugLog(`${errorMsg}: ${err.message}`);
setError(errorMsg);
});
}, [text, addDebugLog]);
// Clear all inputs and states
const clearAll = useCallback(() => {
setText('');
setError(null);
setStatus('Enter text to convert to speech');
if (audioUrl) {
URL.revokeObjectURL(audioUrl);
setAudioUrl(null);
}
if (isPlaying) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
setIsPlaying(false);
}
addDebugLog('All inputs and states cleared');
}, [audioUrl, isPlaying, addDebugLog]);
// Helper functions
const clearDebugLogs = useCallback(() => {
setDebugLogs([]);
addDebugLog('Debug logs cleared');
}, [addDebugLog]);
const toggleDebug = useCallback(() => {
setShowDebug(!showDebug);
addDebugLog(`Debug panel ${showDebug ? 'hidden' : 'shown'}`);
}, [showDebug, addDebugLog]);
return (
<div className="container mx-auto px-4 max-w-4xl my-6">
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex justify-between items-center mb-6">
<div className='flex flex-col space-y-2'>
<h1 className="text-2xl font-bold text-gray-800">Text to Speech (Using our API)</h1>
<p className='text-gray-500'>Limitation: 5 Req / Min & 50 Req / Day </p>
<p className='text-gray-500'>Need More Purchase from our <a href="/services" className='text-[#6d9e37] font-bold'>Services</a></p>
</div>
<Button
onClick={toggleDebug}
variant="outline"
size="sm"
>
{showDebug ? 'Hide Debug' : 'Show Debug'}
</Button>
</div>
{/* Text Input Section */}
<div className="mb-6">
<div className="flex justify-between items-center mb-2">
<label htmlFor="tts-text" className="block text-sm font-medium text-gray-700">
Enter Text
</label>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={copyToClipboard}
disabled={!text || isLoading}
>
{copied ? 'Copied!' : 'Copy Text'}
</Button>
<Button
variant="outline"
size="sm"
onClick={clearAll}
disabled={isLoading}
>
Clear All
</Button>
</div>
</div>
<textarea
ref={textareaRef}
id="tts-text"
rows={6}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:border-[#6d9e37] bg-white text-gray-800"
placeholder="Type or paste your text here..."
value={text}
onChange={(e) => setText(e.target.value)}
disabled={isLoading}
maxLength={100}
/>
</div>
{/* Action Buttons */}
<div className="flex flex-wrap gap-3 mb-6">
<Button
onClick={convertToSpeech}
disabled={!text.trim() || isLoading}
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
>
{isLoading ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Converting...
</span>
) : (
'Convert to Speech'
)}
</Button>
<Button
onClick={togglePlayback}
disabled={!audioUrl || isLoading}
variant="outline"
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
>
{isPlaying ? (
<span className="flex items-center justify-center">
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Stop Playback
</span>
) : (
<span className="flex items-center justify-center">
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Play Audio
</span>
)}
</Button>
<Button
onClick={downloadAudio}
disabled={!audioUrl || isLoading}
variant="outline"
className="px-6 py-3 text-lg flex-1 min-w-[200px]"
>
<span className="flex items-center justify-center">
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Download Audio
</span>
</Button>
</div>
{/* Status */}
<div className="mb-6 text-center">
<p className={`text-sm ${
error ? 'text-red-600' :
isLoading ? 'text-[#6d9e37]' :
isPlaying ? 'text-[#6d9e37]' :
'text-gray-600'
}`}>
{status}
</p>
</div>
{/* Error Display */}
{error && (
<div className="mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-center text-red-600">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 className="font-medium">Error</h3>
</div>
<p className="mt-2 text-sm text-red-600">{error}</p>
</div>
)}
{/* Audio Player (hidden) */}
<audio
ref={audioRef}
src={audioUrl}
onEnded={handleAudioEnd}
className="hidden"
/>
{/* Debug Section */}
{showDebug && (
<div className="mt-8 border rounded-lg overflow-hidden">
<div className="bg-gray-50 px-4 py-3 border-b flex justify-between items-center">
<h3 className="font-medium text-gray-700">Debug Logs</h3>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={clearDebugLogs}
className="text-sm">
Clear Logs
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
const blob = new Blob([debugLogs.join('\n')], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `tts-debug-${new Date().toISOString()}.log`;
a.click();
URL.revokeObjectURL(url);
}}
className="text-sm">
Export Logs
</Button>
</div>
</div>
<div className="p-4 bg-white max-h-60 overflow-y-auto">
{debugLogs.length > 0 ? (
<div className="space-y-1">
{debugLogs.map((log, index) => (
<div key={index} className="text-xs font-mono text-gray-800 border-b border-gray-100 pb-1">
{log}
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500">No debug logs yet. Interactions will appear here.</p>
)}
</div>
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,36 @@
import React from "react";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../../components/ui/card";
import { Button } from "../ui/button";
export default function ToolsCard({key, toolName, toolSlug, toolFeatures, toolImg}) {
// console.log('tool data', {key, toolName, toolSlug, toolFeatures, toolImg})
return(
<>
<Card className="flex flex-col justify-between hover:shadow-[0_0_10px_#6d9e37] duration-500">
<img className="object-cover aspect-video rounded-t-md" src={toolImg} alt={toolName} />
<CardContent className="flex flex-col flex-1">
<CardTitle className="py-2">{toolName}</CardTitle>
<hr className="border-b-2 border-[#6d9e37]" />
<ul className="space-y-2 text-sm mb-4 mt-2">
{
toolFeatures.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-600'>{feature}</span>
</li>
))
}
</ul>
<div className="mt-auto">
<a href={`/web-tools/${toolSlug}`}>
<Button onClick={() => window.location.href=`/web-tools/${toolSlug}`} className="w-full">View</Button>
</a>
</div>
</CardContent>
</Card>
</>
)
}

View File

@ -18,7 +18,7 @@ export default function TopicDetail(props) {
const shareUrl = typeof window !== 'undefined' ? window.location.href : '';
const title = props.topic.title;
const text = `Check out this article: ${title}`;
const text = `Check out this Topic: ${title}`;
const shareOnSocialMedia = (platform) => {
switch (platform){
@ -108,7 +108,7 @@ export default function TopicDetail(props) {
return (
<div className="container mx-auto px-4 py-12">
<article className="max-w-4xl mx-auto">
<img src={props.topic.img ? props.topic.img : '/assets/images/thumb-place.jpg'} alt={props.topic.title} className="w-full h-[400px] aspect-video object-cover rounded-lg mb-8 shadow-md" />
<img src={props.topic.img ? props.topic.img : '/assets/images/thumb-place.jpg'} alt={props.topic.title} className="w-full object-cover rounded-lg mb-8 shadow-md" />
<h1 className="text-4xl font-bold text-[#6d9e37] mb-6">{props.topic.title}</h1>
{/* Enhanced Social Share Buttons */}
@ -194,7 +194,7 @@ export default function TopicDetail(props) {
</div>
</div>
<div className="font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }} ></div>
<div id="markdown-content" className="table-scroll-wrapper font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }} ></div>
<CommentSystem topicId={props.topic.id}/>
</article>
</div>

View File

@ -8,8 +8,10 @@ import { Separator } from './ui/separator';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import { CustomTabs } from './ui/tabs';
import Loader from "./ui/loader";
import Cookies from 'js-cookie';
const PUBLIC_TOPIC_API_URL = import.meta.env.PUBLIC_TOPIC_API_URL;
const PUBLIC_MINIO_UPLOAD_URL = import.meta.env.PUBLIC_MINIO_UPLOAD_URL;
const PUBLIC_MINIO_UPLOAD_URL = 'https://file-vault-prod-pdgctwgsnkiy.siliconpin.com/upload';
// const PUBLIC_MINIO_UPLOAD_URL = import.meta.env.PUBLIC_MINIO_UPLOAD_URL;
const urlParams = new URLSearchParams(window.location.search);
const slug = urlParams.get('slug');
// console.log('find slug from url', slug);
@ -66,24 +68,29 @@ export default function EditTopic (){
// Upload file to MinIO (same as NewTopic)
const uploadToMinIO = async (file, onProgress) => {
const siliconId = Cookies.get('siliconId'); // Get from cookie or anywhere
const token = Cookies.get('token'); // Get from cookie or storage
const formData = new FormData();
formData.append('file', file);
formData.append('bucket', 'siliconpin-uploads');
formData.append('folder', 'topic-images');
try {
const response = await fetch(PUBLIC_MINIO_UPLOAD_URL, {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
'x-user-data': siliconId,
'Authorization': token,
},
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
throw new Error(`Upload failed: ${response.statusText}`);
}
const data = await response.json();
return data.url;
// console.log('Upload successful:', data);
return data;
} catch (error) {
console.error('Upload error:', error);
throw error;
@ -115,14 +122,50 @@ export default function EditTopic (){
setImageDialogOpen(true);
},
};
// Custom <br> insert command
const lineBreakCommand = {
name: 'linebreak',
keyCommand: 'linebreak',
buttonProps: { 'aria-label': 'Insert line break' },
icon: (
<svg width="16px" height="16px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M20 7V8.2C20 9.88016 20 10.7202 19.673 11.362C19.3854 11.9265 18.9265 12.3854 18.362 12.673C17.7202 13 16.8802 13 15.2 13H4M4 13L8 9M4 13L8 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>
),
execute: () => {
const textarea = document.querySelector('.w-md-editor-text-input');
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const value = formData.content || "";
const breakTag = '<br>';
const newValue = value.slice(0, start) + breakTag + value.slice(end);
setFormData(prev => ({
...prev,
content: newValue
}));
setTimeout(() => {
textarea.selectionStart = textarea.selectionEnd = start + breakTag.length;
textarea.focus();
}, 0);
}
};
// Get all commands (same as NewTopic)
const allCommands = commands.getCommands().map(cmd => {
if (cmd.name === 'image') {
return customImageCommand;
}
return cmd;
});
const allCommands = [
...commands.getCommands().map(cmd => {
if (cmd.name === 'image') {
return customImageCommand;
}
return cmd;
}),
lineBreakCommand,
];
// Handle image URL insertion (same as NewTopic)
const handleInsertImageUrl = () => {
@ -172,7 +215,7 @@ export default function EditTopic (){
setUploadProgress(progress);
});
const imgMarkdown = `![Image](${uploadedUrl})`;
const imgMarkdown = `![Image](${uploadedUrl.url})`;
const textarea = document.querySelector('.w-md-editor-text-input');
if (textarea) {
const startPos = textarea.selectionStart;
@ -212,7 +255,7 @@ export default function EditTopic (){
setUploadProgress(progress);
});
}
imageUrl = imageUrl.url;
// Prepare payload
const payload = {
...formData,

File diff suppressed because one or more lines are too long

View File

@ -207,3 +207,15 @@ const contactSchema3 = {
</div>
</main>
</Layout>
<script src="https://suvodip35.github.io/my-chatbot-widget/chatbot.min.js"></script>
<script>
window.chatbotSettings = {
apiKey: "",
apiUrl: "",
allowedDomains: ["127.0.0.1"],
themeColor: "#6d9e37",
greetingMessage: "Hi! How can I help you today?",
chatbotTitle: "Chat with ঌপি AI",
autoOpen: true
};
</script>

View File

@ -120,7 +120,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
</p>
</section>
<section class="bg-neutral-800 rounded-lg p-6 sm:p-8 border border-neutral-700">
<section id="profile-delete" class="bg-neutral-800 rounded-lg p-6 sm:p-8 border border-neutral-700">
<h2 class="text-2xl font-bold text-white mb-4">6. Your Rights</h2>
<div class="space-y-4 text-neutral-300">
<p>

View File

@ -40,12 +40,6 @@ const services = [
buyButtonText: "",
buyButtonUrl: ""
},
{
imageUrl: '/assets/images/services/stt-streaming.png',
learnMoreUrl: '',
buyButtonText: 'Purchase',
buyButtonUrl: '/services/stt-streaming'
},
{
imageUrl: '/assets/images/services/kubernetes-edge.png',
learnMoreUrl: '/services/kubernetes',
@ -69,11 +63,23 @@ const services = [
learnMoreUrl: '/services/hire-an-ai-agent',
buyButtonText: '',
buyButtonUrl: ''
},
{
imageUrl: '/assets/images/services/stt-streaming.png',
learnMoreUrl: '',
buyButtonText: 'Purchase',
buyButtonUrl: '/services/stt-streaming'
},
{
imageUrl: '/assets/images/services/deployapp-thumb.jpg',
learnMoreUrl: '',
buyButtonText: 'Get VPS',
buyButtonUrl: '/services/cloud-instance/'
}
];
let data = [];
try {
const response = await fetch(`${PUBLIC_SERVICE_API_URL}?query=all-services-list`);
const response = await fetch(`https://siliconpin.com/v1/services/?query=all-services-list`);
const json = await response.json();
if (json.success) {
// data = [...json.data, ...services];

View File

@ -2,6 +2,6 @@
import Layout from "../../layouts/Layout.astro";
import NewKubernetisUtho from "../../components/BuyServices/NewKubernetisUtho";
---
<Layout title="Buy VPN | WireGuard | SiliconPin">
<Layout title="Deploy Kubernetis | Kubernetis | SiliconPin">
<NewKubernetisUtho client:load />
</Layout>

68
src/pages/test-file.html Normal file
View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Upload Test</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.upload-container {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
}
.file-input-wrapper {
margin: 20px 0;
}
.file-label {
display: inline-block;
padding: 10px 15px;
background-color: #f0f0f0;
border-radius: 4px;
cursor: pointer;
border: 1px solid #ccc;
}
.upload-btn {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.note {
margin-top: 20px;
font-size: 0.9em;
color: #666;
}
</style>
</head>
<body>
<div>
<h1>Image Upload Test</h1>
<div class="upload-container">
<!-- Pure HTML form with direct submission -->
<form
action="/v1/check/"
method="POST"
enctype="multipart/form-data"
>
<input type="submit" value="Upload Image" class="upload-btn">
</form>
</div>
</div>
</body>
</html>

7
src/pages/test-up.astro Normal file
View File

@ -0,0 +1,7 @@
---
import Layout from "../layouts/Layout.astro"
import TestFileUpload from "../components/TestFileUpload"
---
<Layout title="">
<TestFileUpload client:load />
</Layout>

View File

@ -2,7 +2,7 @@
import Layout from "../../layouts/Layout.astro";
import TopicsList from "../../components/Topics";
const TOPIC_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/';
const TOPIC_API_URL = 'https://siliconpin.com/v1/topics/';
let topics = [];
try {

View File

@ -0,0 +1,71 @@
---
import Layout from "../../layouts/Layout.astro";
import ToolsCard from "../../components/Tools/ToolsCard";
let toolsData = [
{
"id": "1",
"toolName": "Speech To Text",
"slug": "speech-to-text",
"img": "https://images.unsplash.com/photo-1554224155-6726b3ff858f?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80",
"features": [
"Real-time transcription",
"Multiple API integrations (Whisper, Vosk)",
"Audio recording capability",
"Export transcriptions as text files",
"Multi-language support",
"Punctuation and formatting options"
]
},
{
"id": "2",
"toolName": "Image Resize",
"slug": "image-resize",
"img": "https://images.unsplash.com/photo-1619597455322-4fbbd820250a?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80",
"features": [
"Bulk image processing",
"Preset dimensions for social media",
"Maintain aspect ratio",
"Quality adjustment",
"Multiple output formats (JPG, PNG, WEBP)",
"Preview before download",
"Drag and drop interface"
]
},
{
"id": "3",
"toolName": "Text to Speech",
"slug": "text-to-speech",
"img": "https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80",
"features": [
"High-quality speech synthesis",
"Multiple voice options",
"Adjustable playback speed",
"Download as WAV audio",
"Real-time preview",
"Text formatting support",
"API integration",
"Debug and logging features"
]
}
]
---
<Layout title="Web Tools | SiliconPin Tools">
<div class="container mx-auto px-4">
<h2 class="text-2xl font-bold my-4 text-[#6d9e37]">SiliconPin Web Toos</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{
toolsData.map((items, index) => (
<ToolsCard client:load
key={items.id}
toolName={items.toolName}
toolSlug={items.slug}
toolFeatures={items.features}
toolImg={items.img}
/>
))
}
</div>
</div>
</Layout>

View File

@ -5,7 +5,7 @@ import AudioToText from "../../components/Tools/AudioToText"
<Layout title="Whisper CPP Light">
<AudioToText client:load />
</Layout>
</Layout>
<!-- <div class="container mx-auto px-4" id="stt-container">
<h1 class="text-2xl font-bold mb-4">Audio File to STT</h1>

View File

@ -0,0 +1,9 @@
---
import Layout from "../../layouts/Layout.astro";
import TextToSpeech from "../../components/Tools/TextToSpeech";
import FreeTextToSpeech from "../../components/Tools/FreeTextToSpeech";
---
<Layout title="">
<TextToSpeech client:load />
<FreeTextToSpeech client:load />
</Layout>

View File

@ -0,0 +1,492 @@
---
import Layout from "../../layouts/Layout.astro"
---
<Layout title="Voice Chat">
<div class="container mx-auto max-w-2xl h-screen flex flex-col bg-gray-100 my-6">
<!-- Header -->
<div class="bg-[#6d9e37] text-white p-4 shadow-md">
<div class="flex items-center gap-x-4">
<div class="w-10 h-10 rounded-full bg-green-500 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
</div>
<div class="ml-3">
<h1 class="font-bold text-lg">Voice Assistant</h1>
<p class="text-xs opacity-80" id="status">Ready to record</p>
</div>
</div>
</div>
<!-- Chat Messages -->
<div id="chatMessages" class="flex-1 max-h-[400px] overflow-y-auto p-4 space-y-3 bg-gray-50">
<!-- Welcome message -->
<div class="flex items-start">
<div class="bg-white rounded-xl rounded-tl-none py-2 px-4 max-w-xs shadow-sm">
<p class="text-gray-800">Hello! Tap the microphone to start talking with me.</p>
</div>
</div>
</div>
<!-- Audio Visualizer -->
<div id="audioVisualizer" class="h-16 bg-gray-200 mx-4 rounded-lg flex items-center justify-center">
<div class="w-full h-8 flex items-center justify-center space-x-1" id="visualizerBars"></div>
</div>
<!-- Controls -->
<div class="p-4 bg-white border-t border-gray-200 flex justify-center">
<button id="startBtn" class="p-3 bg-[#6d9e37] rounded-full text-white hover:bg-[#6d9e37]/50 transition-colors shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
</svg>
</button>
<button id="stopBtn" disabled class="p-3 bg-red-500 rounded-full text-white hover:bg-red-600 transition-colors shadow-lg ml-4 opacity-50 cursor-not-allowed hidden">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
</svg>
</button>
</div>
</div>
</Layout>
<script is:inline>
// DOM elements
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const chatMessages = document.getElementById('chatMessages');
const audioVisualizer = document.getElementById('audioVisualizer');
const visualizerBars = document.getElementById('visualizerBars');
const statusDisplay = document.getElementById('status');
// Audio variables
let audioContext;
let mediaStream;
let processor;
let recording = false;
let audioChunks = [];
let audioBlob;
let silenceTimer;
let lastSoundTime;
let activeAudioElement = null;
let lastResponseAudio = null;
// Configuration
const config = {
sampleRate: 16000, // 16kHz sample rate
numChannels: 1, // Mono
bitDepth: 16, // 16-bit
silenceThreshold: 0.02, // Threshold for silence detection
silenceTimeout: 1500, // Stop after 1.5s of silence
maxRecordingTime: 30000 // Max recording time in ms (30 seconds)
};
// Create visualizer bars
function createVisualizerBars() {
visualizerBars.innerHTML = '';
for (let i = 0; i < 12; i++) {
const bar = document.createElement('div');
bar.className = 'bg-gray-300 h-1 w-1 rounded-full transition-all duration-75';
bar.style.height = '2px';
visualizerBars.appendChild(bar);
}
}
// Add message to chat
function addMessage(text = null, audioUrl = null, isUser = false, isSystem = false) {
const messageDiv = document.createElement('div');
messageDiv.className = `flex ${isUser ? 'justify-end' : 'justify-start'} mb-3`;
const bubble = document.createElement('div');
bubble.className = `rounded-xl py-2 px-4 max-w-xs shadow-sm ${isUser ? 'bg-green-100 rounded-tr-none' : isSystem ? 'bg-gray-200 rounded-tl-none' : 'bg-white rounded-tl-none'}`;
if (audioUrl) {
// Audio message with text
const audioId = `audio-${Date.now()}`;
bubble.innerHTML = `
<div class="flex flex-col space-y-2">
${text ? `<p class="text-gray-800">${text}</p>` : ''}
<div class="flex items-center space-x-2">
<button class="play-btn p-2 rounded-full bg-white shadow" data-audio="${audioId}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
</svg>
</button>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div class="progress-bar bg-green-600 h-2.5 rounded-full" style="width: 0%"></div>
</div>
<span class="text-xs text-gray-500 time-display">0:00</span>
</div>
</div>
<audio id="${audioId}" src="${audioUrl}" preload="auto"></audio>
`;
// Store reference to the last response audio
if (!isUser) {
lastResponseAudio = { audioId, element: document.getElementById(audioId) };
}
} else {
// Text message
bubble.innerHTML = `<p class="text-gray-800">${text}</p>`;
}
messageDiv.appendChild(bubble);
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return bubble;
}
// Auto-play the last response audio
function playLastResponse() {
if (lastResponseAudio && lastResponseAudio.element) {
const audioElement = lastResponseAudio.element;
const playBtn = document.querySelector(`[data-audio="${lastResponseAudio.audioId}"]`);
if (playBtn) {
// Trigger click on the play button to ensure all UI updates are handled
playBtn.click();
} else {
// Fallback to direct play if button not found
audioElement.play();
}
}
}
// Setup audio playback controls
function setupAudioControls() {
document.querySelectorAll('.play-btn').forEach(btn => {
btn.addEventListener('click', function() {
const audioId = this.getAttribute('data-audio');
const audioElement = document.getElementById(audioId);
const progressBar = this.closest('.flex').querySelector('.progress-bar');
const timeDisplay = this.closest('.flex').querySelector('.time-display');
// Stop any currently playing audio
if (activeAudioElement && activeAudioElement !== audioElement) {
activeAudioElement.pause();
activeAudioElement.currentTime = 0;
const activeBtn = document.querySelector(`[data-audio="${activeAudioElement.id}"]`);
if (activeBtn) {
activeBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
</svg>
`;
activeBtn.closest('.flex').querySelector('.progress-bar').style.width = '0%';
activeBtn.closest('.flex').querySelector('.time-display').textContent = '0:00';
}
}
if (audioElement.paused) {
// Play audio
audioElement.play();
activeAudioElement = audioElement;
this.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
`;
// Update progress bar
audioElement.addEventListener('timeupdate', function() {
const progress = (audioElement.currentTime / audioElement.duration) * 100;
progressBar.style.width = `${progress}%`;
// Update time display
const minutes = Math.floor(audioElement.currentTime / 60);
const seconds = Math.floor(audioElement.currentTime % 60).toString().padStart(2, '0');
timeDisplay.textContent = `${minutes}:${seconds}`;
});
// Reset when finished
audioElement.addEventListener('ended', function() {
this.currentTime = 0;
progressBar.style.width = '0%';
timeDisplay.textContent = '0:00';
btn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
</svg>
`;
});
} else {
// Pause audio
audioElement.pause();
this.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
</svg>
`;
}
});
});
}
// Start recording
startBtn.addEventListener('click', async () => {
try {
statusDisplay.textContent = "Starting microphone...";
addMessage("Recording...", null, true);
// Get microphone access
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Create audio context
audioContext = new (window.AudioContext || window.webkitAudioContext)({
sampleRate: config.sampleRate
});
// Create processor node
processor = audioContext.createScriptProcessor(4096, 1, 1);
processor.onaudioprocess = processAudio;
// Create source and connect
const source = audioContext.createMediaStreamSource(mediaStream);
source.connect(processor);
processor.connect(audioContext.destination);
// Setup visualization
setupVisualizer(source);
// Start recording
recording = true;
audioChunks = [];
lastSoundTime = Date.now();
// Start silence detection
silenceTimer = setInterval(checkSilence, 200);
// Start max recording time timer
setTimeout(() => {
if (recording) {
stopRecording();
}
}, config.maxRecordingTime);
// Update UI
startBtn.classList.add('hidden');
stopBtn.classList.remove('hidden', 'opacity-50', 'cursor-not-allowed');
statusDisplay.textContent = "Listening...";
} catch (error) {
console.error('Error:', error);
statusDisplay.textContent = "Error: " + error.message;
addMessage("Error: " + error.message, null, false, true);
}
});
// Process audio data with silence detection
function processAudio(event) {
if (!recording) return;
const inputData = event.inputBuffer.getChannelData(0);
// Check for sound activity
const isSound = isSoundDetected(inputData);
if (isSound) {
lastSoundTime = Date.now();
}
// Convert to 16-bit PCM
const buffer = new ArrayBuffer(inputData.length * 2);
const view = new DataView(buffer);
for (let i = 0, offset = 0; i < inputData.length; i++, offset += 2) {
const s = Math.max(-1, Math.min(1, inputData[i]));
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
audioChunks.push(new Uint8Array(buffer));
}
// Simple sound detection
function isSoundDetected(inputData) {
for (let i = 0; i < inputData.length; i++) {
if (Math.abs(inputData[i]) > config.silenceThreshold) {
return true;
}
}
return false;
}
// Check for silence timeout
function checkSilence() {
if (!recording) return;
const silenceDuration = Date.now() - lastSoundTime;
if (silenceDuration > config.silenceTimeout) {
stopRecording();
}
}
// Stop recording and process
async function stopRecording() {
if (!recording) return;
// Clear silence timer
clearInterval(silenceTimer);
// Stop recording
recording = false;
// Disconnect audio nodes
if (processor) {
processor.disconnect();
processor = null;
}
// Stop media stream
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
}
// Create WAV file
createWavFile();
// Update UI
startBtn.classList.remove('hidden');
stopBtn.classList.add('hidden');
statusDisplay.textContent = "Processing...";
// Send to API
await sendToAPI();
}
// Manual stop button
stopBtn.addEventListener('click', stopRecording);
// Create WAV file
function createWavFile() {
const length = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
const result = new Uint8Array(length);
let offset = 0;
audioChunks.forEach(chunk => {
result.set(chunk, offset);
offset += chunk.length;
});
const wavHeader = createWaveHeader(result.length, config);
const wavData = new Uint8Array(wavHeader.length + result.length);
wavData.set(wavHeader, 0);
wavData.set(result, wavHeader.length);
audioBlob = new Blob([wavData], { type: 'audio/wav' });
}
// Create WAV header
function createWaveHeader(dataLength, config) {
const byteRate = config.sampleRate * config.numChannels * (config.bitDepth / 8);
const blockAlign = config.numChannels * (config.bitDepth / 8);
const buffer = new ArrayBuffer(44);
const view = new DataView(buffer);
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + dataLength, true);
writeString(view, 8, 'WAVE');
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, config.numChannels, true);
view.setUint32(24, config.sampleRate, true);
view.setUint32(28, byteRate, true);
view.setUint16(32, blockAlign, true);
view.setUint16(34, config.bitDepth, true);
writeString(view, 36, 'data');
view.setUint32(40, dataLength, true);
return new Uint8Array(buffer);
}
// Helper to write strings to DataView
function writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
// Send audio to API
async function sendToAPI() {
try {
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.wav');
const response = await fetch('https://vaani-pocndhvgmcnbacgb.siliconpin.com/upload-audio', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('API request failed');
// Check content type to handle both JSON and audio responses
const contentType = response.headers.get('content-type');
let responseText = "Here's what I think about that";
let audioData;
if (contentType.includes('application/json')) {
// Handle JSON response with both text and audio
const result = await response.json();
responseText = result.text || responseText;
console.log('responseText', responseText)
audioData = result.audio;
} else if (contentType.includes('audio/wav')) {
// Handle direct audio response
audioData = await response.arrayBuffer();
} else {
throw new Error('Unexpected response type');
}
const audioUrl = URL.createObjectURL(new Blob([new Uint8Array(audioData)], { type: 'audio/wav' }));
// Update UI with both text and audio
addMessage(responseText, audioUrl, false);
statusDisplay.textContent = "Ready to record";
// Setup playback controls
setupAudioControls();
// Auto-play the response
setTimeout(playLastResponse, 500);
} catch (error) {
console.error('API Error:', error);
statusDisplay.textContent = "Error: " + error.message;
addMessage("Error processing request", null, false, true);
}
}
// Setup audio visualization
function setupVisualizer(source) {
const analyser = audioContext.createAnalyser();
analyser.fftSize = 32;
source.connect(analyser);
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
createVisualizerBars();
const bars = visualizerBars.querySelectorAll('div');
function draw() {
if (!recording) return;
requestAnimationFrame(draw);
analyser.getByteFrequencyData(dataArray);
for (let i = 0; i < bars.length; i++) {
const value = dataArray[i % bufferLength] / 255;
const height = value * 30;
bars[i].style.height = `${Math.max(2, height)}px`;
bars[i].style.backgroundColor = `rgb(${Math.floor(value * 100 + 155)}, ${Math.floor(value * 50 + 50)}, 50)`;
}
}
draw();
}
// Initial setup
createVisualizerBars();
setupAudioControls();
</script>

180
yarn.lock
View File

@ -1234,9 +1234,9 @@
integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==
"@types/prop-types@^15.0.0":
version "15.7.14"
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz"
integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==
version "15.7.15"
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz"
integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
"@types/react-dom@*", "@types/react-dom@^17.0.17 || ^18.0.6 || ^19.0.0":
version "19.0.4"
@ -1314,6 +1314,25 @@
remark-gfm "~3.0.1"
unist-util-visit "^4.1.0"
"@uiw/react-markdown-preview@^5.1.4":
version "5.1.4"
resolved "https://registry.npmjs.org/@uiw/react-markdown-preview/-/react-markdown-preview-5.1.4.tgz"
integrity sha512-6k13WVNHCEaamz3vh54OQ1tseIXneKlir1+E/VFQBPq8PRod+gwLfYtiitDBWu+ZFttoiKPLZ7flgHrVM+JNOg==
dependencies:
"@babel/runtime" "^7.17.2"
"@uiw/copy-to-clipboard" "~1.0.12"
react-markdown "~9.0.1"
rehype-attr "~3.0.1"
rehype-autolink-headings "~7.1.0"
rehype-ignore "^2.0.0"
rehype-prism-plus "2.0.0"
rehype-raw "^7.0.0"
rehype-rewrite "~4.0.0"
rehype-slug "~6.0.0"
remark-gfm "~4.0.0"
remark-github-blockquote-alert "^1.0.0"
unist-util-visit "^5.0.0"
"@uiw/react-md-editor@^3.25.6":
version "3.25.6"
resolved "https://registry.npmjs.org/@uiw/react-md-editor/-/react-md-editor-3.25.6.tgz"
@ -2964,6 +2983,13 @@ hast-util-heading-rank@^2.0.0:
dependencies:
"@types/hast" "^2.0.0"
hast-util-heading-rank@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz"
integrity sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==
dependencies:
"@types/hast" "^3.0.0"
hast-util-is-element@^2.0.0:
version "2.1.3"
resolved "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.3.tgz"
@ -3029,7 +3055,7 @@ hast-util-raw@^9.0.0:
web-namespaces "^2.0.0"
zwitch "^2.0.0"
hast-util-select@^5.0.5:
hast-util-select@^5.0.5, hast-util-select@~5.0.1:
version "5.0.5"
resolved "https://registry.npmjs.org/hast-util-select/-/hast-util-select-5.0.5.tgz"
integrity sha512-QQhWMhgTFRhCaQdgTKzZ5g31GLQ9qRb1hZtDPMqQaOhpLBziWcshUS0uCR5IJ0U1jrK/mxg35fmcq+Dp/Cy2Aw==
@ -3071,27 +3097,6 @@ hast-util-select@^6.0.0:
unist-util-visit "^5.0.0"
zwitch "^2.0.0"
hast-util-select@~5.0.1:
version "5.0.5"
resolved "https://registry.npmjs.org/hast-util-select/-/hast-util-select-5.0.5.tgz"
integrity sha512-QQhWMhgTFRhCaQdgTKzZ5g31GLQ9qRb1hZtDPMqQaOhpLBziWcshUS0uCR5IJ0U1jrK/mxg35fmcq+Dp/Cy2Aw==
dependencies:
"@types/hast" "^2.0.0"
"@types/unist" "^2.0.0"
bcp-47-match "^2.0.0"
comma-separated-tokens "^2.0.0"
css-selector-parser "^1.0.0"
direction "^2.0.0"
hast-util-has-property "^2.0.0"
hast-util-to-string "^2.0.0"
hast-util-whitespace "^2.0.0"
not "^0.1.0"
nth-check "^2.0.0"
property-information "^6.0.0"
space-separated-tokens "^2.0.0"
unist-util-visit "^4.0.0"
zwitch "^2.0.0"
hast-util-to-html@^8.0.0:
version "8.0.4"
resolved "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz"
@ -4561,16 +4566,7 @@ micromark-util-resolve-all@^2.0.0:
dependencies:
micromark-util-types "^2.0.0"
micromark-util-sanitize-uri@^1.0.0:
version "1.2.0"
resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz"
integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-encode "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-sanitize-uri@^1.1.0:
micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0:
version "1.2.0"
resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz"
integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==
@ -5471,6 +5467,22 @@ react-markdown@~8.0.0:
unist-util-visit "^4.0.0"
vfile "^5.0.0"
react-markdown@~9.0.1:
version "9.0.3"
resolved "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.3.tgz"
integrity sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==
dependencies:
"@types/hast" "^3.0.0"
devlop "^1.0.0"
hast-util-to-jsx-runtime "^2.0.0"
html-url-attributes "^3.0.0"
mdast-util-to-hast "^13.0.0"
remark-parse "^11.0.0"
remark-rehype "^11.0.0"
unified "^11.0.0"
unist-util-visit "^5.0.0"
vfile "^6.0.0"
react-qr-code@^2.0.15:
version "2.0.15"
resolved "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.15.tgz"
@ -5641,6 +5653,14 @@ rehype-attr@~2.1.0:
unified "~10.1.1"
unist-util-visit "~4.1.0"
rehype-attr@~3.0.1:
version "3.0.3"
resolved "https://registry.npmjs.org/rehype-attr/-/rehype-attr-3.0.3.tgz"
integrity sha512-Up50Xfra8tyxnkJdCzLBIBtxOcB2M1xdeKe1324U06RAvSjYm7ULSeoM+b/nYPQPVd7jsXJ9+39IG1WAJPXONw==
dependencies:
unified "~11.0.0"
unist-util-visit "~5.0.0"
rehype-autolink-headings@~6.1.1:
version "6.1.1"
resolved "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-6.1.1.tgz"
@ -5654,6 +5674,18 @@ rehype-autolink-headings@~6.1.1:
unified "^10.0.0"
unist-util-visit "^4.0.0"
rehype-autolink-headings@~7.1.0:
version "7.1.0"
resolved "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz"
integrity sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==
dependencies:
"@types/hast" "^3.0.0"
"@ungap/structured-clone" "^1.0.0"
hast-util-heading-rank "^3.0.0"
hast-util-is-element "^3.0.0"
unified "^11.0.0"
unist-util-visit "^5.0.0"
rehype-ignore@^1.0.1:
version "1.0.5"
resolved "https://registry.npmjs.org/rehype-ignore/-/rehype-ignore-1.0.5.tgz"
@ -5663,6 +5695,15 @@ rehype-ignore@^1.0.1:
unified "^10.1.2"
unist-util-visit "^4.1.2"
rehype-ignore@^2.0.0:
version "2.0.2"
resolved "https://registry.npmjs.org/rehype-ignore/-/rehype-ignore-2.0.2.tgz"
integrity sha512-BpAT/3lU9DMJ2siYVD/dSR0A/zQgD6Fb+fxkJd4j+wDVy6TYbYpK+FZqu8eM9EuNKGvi4BJR7XTZ/+zF02Dq8w==
dependencies:
hast-util-select "^6.0.0"
unified "^11.0.0"
unist-util-visit "^5.0.0"
rehype-parse@^8.0.0, rehype-parse@^8.0.2:
version "8.0.5"
resolved "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.5.tgz"
@ -5694,6 +5735,18 @@ rehype-prism-plus@~1.6.1, rehype-prism-plus@1.6.3:
unist-util-filter "^4.0.0"
unist-util-visit "^4.0.0"
rehype-prism-plus@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/rehype-prism-plus/-/rehype-prism-plus-2.0.0.tgz"
integrity sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ==
dependencies:
hast-util-to-string "^3.0.0"
parse-numeric-range "^1.3.0"
refractor "^4.8.0"
rehype-parse "^9.0.0"
unist-util-filter "^5.0.0"
unist-util-visit "^5.0.0"
rehype-raw@^6.1.1:
version "6.1.1"
resolved "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz"
@ -5712,7 +5765,7 @@ rehype-raw@^7.0.0:
hast-util-raw "^9.0.0"
vfile "^6.0.0"
rehype-rewrite@^4.0.2:
rehype-rewrite@^4.0.2, rehype-rewrite@~4.0.0:
version "4.0.2"
resolved "https://registry.npmjs.org/rehype-rewrite/-/rehype-rewrite-4.0.2.tgz"
integrity sha512-rjLJ3z6fIV11phwCqHp/KRo8xuUCO8o9bFJCNw5o6O2wlLk6g8r323aRswdGBQwfXPFYeSuZdAjp4tzo6RGqEg==
@ -5743,6 +5796,17 @@ rehype-slug@~5.1.0:
unified "^10.0.0"
unist-util-visit "^4.0.0"
rehype-slug@~6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz"
integrity sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==
dependencies:
"@types/hast" "^3.0.0"
github-slugger "^2.0.0"
hast-util-heading-rank "^3.0.0"
hast-util-to-string "^3.0.0"
unist-util-visit "^5.0.0"
rehype-stringify@^10.0.0, rehype-stringify@^10.0.1:
version "10.0.1"
resolved "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz"
@ -5781,7 +5845,7 @@ rehype@~12.0.1:
rehype-stringify "^9.0.0"
unified "^10.0.0"
remark-gfm@^4.0.1:
remark-gfm@^4.0.1, remark-gfm@~4.0.0:
version "4.0.1"
resolved "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz"
integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==
@ -5803,6 +5867,13 @@ remark-gfm@~3.0.1:
micromark-extension-gfm "^2.0.0"
unified "^10.0.0"
remark-github-blockquote-alert@^1.0.0:
version "1.3.1"
resolved "https://registry.npmjs.org/remark-github-blockquote-alert/-/remark-github-blockquote-alert-1.3.1.tgz"
integrity sha512-OPNnimcKeozWN1w8KVQEuHOxgN3L4rah8geMOLhA5vN9wITqU4FWD+G26tkEsCGHiOVDbISx+Se5rGZ+D1p0Jg==
dependencies:
unist-util-visit "^5.0.0"
remark-parse@^10.0.0:
version "10.0.2"
resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz"
@ -6701,7 +6772,7 @@ unicode-trie@^2.0.0:
pako "^0.2.5"
tiny-inflate "^1.0.0"
unified@^10.0.0, unified@~10.1.1:
unified@^10.0.0, unified@^10.1.2, unified@~10.1.1:
version "10.1.2"
resolved "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz"
integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==
@ -6714,20 +6785,7 @@ unified@^10.0.0, unified@~10.1.1:
trough "^2.0.0"
vfile "^5.0.0"
unified@^10.1.2:
version "10.1.2"
resolved "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz"
integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==
dependencies:
"@types/unist" "^2.0.0"
bail "^2.0.0"
extend "^3.0.0"
is-buffer "^2.0.0"
is-plain-obj "^4.0.0"
trough "^2.0.0"
vfile "^5.0.0"
unified@^11.0.0, unified@^11.0.3, unified@^11.0.4, unified@^11.0.5:
unified@^11.0.0, unified@^11.0.3, unified@^11.0.4, unified@^11.0.5, unified@~11.0.0:
version "11.0.5"
resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz"
integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==
@ -6749,6 +6807,15 @@ unist-util-filter@^4.0.0:
unist-util-is "^5.0.0"
unist-util-visit-parents "^5.0.0"
unist-util-filter@^5.0.0:
version "5.0.1"
resolved "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-5.0.1.tgz"
integrity sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
unist-util-find-after@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz"
@ -6869,6 +6936,15 @@ unist-util-visit@^5.0.0:
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
unist-util-visit@~5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz"
integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==
dependencies:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz"