From 5cd2a1b6dc8873e82e82a2c4403666ef176f7e29 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 9 Dec 2025 23:46:28 +0000 Subject: [PATCH] init commit --- .gitignore | 24 ++ App.tsx | 556 ++++++++++++++++++++++++++++++++++ README.md | 20 ++ components/ProcessTracker.tsx | 80 +++++ components/VideoGallery.tsx | 69 +++++ index.html | 83 +++++ index.tsx | 15 + metadata.json | 5 + package.json | 25 ++ services/apiService.ts | 79 +++++ services/azureStorage.ts | 56 ++++ tsconfig.json | 29 ++ types.ts | 49 +++ vite.config.ts | 23 ++ 14 files changed, 1113 insertions(+) create mode 100644 .gitignore create mode 100644 App.tsx create mode 100644 README.md create mode 100644 components/ProcessTracker.tsx create mode 100644 components/VideoGallery.tsx create mode 100644 index.html create mode 100644 index.tsx create mode 100644 metadata.json create mode 100644 package.json create mode 100644 services/apiService.ts create mode 100644 services/azureStorage.ts create mode 100644 tsconfig.json create mode 100644 types.ts create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..cf99bf5 --- /dev/null +++ b/App.tsx @@ -0,0 +1,556 @@ + +import React, { useState, useRef, useEffect } from 'react'; +import { MarketingFormData, VideoJob, JobStatus, ProcessingStep, VideoHistoryItem } from './types'; +import { uploadImagesToAzure } from './services/azureStorage'; +import { sendImageManifest, submitJobMetadata, fetchTableData, fetchVideoHistory } from './services/apiService'; +import { VideoGallery } from './components/VideoGallery'; +import { ProcessTracker } from './components/ProcessTracker'; +import { Sparkles, MapPin, Building2, ImagePlus, X, UploadCloud, ArrowRight, Music2, Plus, Mic2, Gauge, Smile } from 'lucide-react'; +import { v7 as uuidv7 } from 'uuid'; + +// Attribute Options Configuration +const ATTRIBUTE_OPTIONS = { + genres: [ + { label: 'K-Pop', value: 'k-pop' }, + { label: 'Pop', value: 'pop' }, + { label: 'Indie Pop', value: 'indie pop' }, + { label: 'Dance', value: 'dance' }, + { label: 'Electronic', value: 'electronic' }, + { label: 'City Pop', value: 'city pop' }, + { label: 'Hip Hop', value: 'hiphop' }, + { label: 'R&B', value: 'rnb' }, + ], + vocals: [ + { label: '허스키한', value: 'raspy' }, + { label: '맑은', value: 'clear' }, + { label: '따뜻한', value: 'warm' }, + { label: '밝은', value: 'bright' }, + ], + tempos: [ + { label: 'Indie Pop BPM 110', value: '110 bpm' }, + { label: 'Korean Ballad BPM 75', value: '75 bpm' }, + { label: 'K-Pop (댄스) BPM 130', value: '130 bpm' }, + ], + moods: [ + { label: '기쁜', value: 'happy' }, + { label: '명량한', value: 'jubilant' }, + { label: '강렬한', value: 'aggressive' }, + { label: '파워풀한', value: 'power' }, + { label: '드라마틱한', value: 'dramatic' }, + ] +}; + +const App: React.FC = () => { + // Form State + const [formData, setFormData] = useState({ + customer_name: '', + region: '', + detail_region_info: '', + attribute: { + genre: 'k-pop', + vocal: 'clear', + tempo: '130 bp', + mood: 'joyful' + }, + imageUrls: [] + }); + + const [selectedFiles, setSelectedFiles] = useState([]); + const [previewUrls, setPreviewUrls] = useState([]); + + // App Logic State + const [isUploading, setIsUploading] = useState(false); + const [submissionStep, setSubmissionStep] = useState(''); + + // Job Management State + const [jobs, setJobs] = useState([]); + const [activeJobId, setActiveJobId] = useState(null); + + // History State + const [videoHistory, setVideoHistory] = useState([]); + + // Active job is derived from the jobs array + const activeJob = jobs.find(j => j.id === activeJobId) || null; + + // Fetch History Function + const loadHistory = async () => { + const history = await fetchVideoHistory(); + // Sort by ID descending (newest first) + setVideoHistory(history.sort((a, b) => b.id - a.id)); + }; + + // Initial Load + useEffect(() => { + loadHistory(); + }, []); + + // --- POLLING LOGIC --- + useEffect(() => { + // Only poll if there are jobs that are PROCESSING + const processingJobs = jobs.filter(j => j.status === JobStatus.PROCESSING); + + if (processingJobs.length === 0) return; + + const intervalId = setInterval(async () => { + // Iterate through all processing jobs + for (const job of processingJobs) { + try { + // STATE MACHINE: Check status based on current step + switch (job.currentStep) { + case ProcessingStep.LYRICS: { + const lyricsData = await fetchTableData(job.id, 'lyrics'); + console.log(lyricsData) + console.log(lyricsData && lyricsData.status == 'completed') + if (lyricsData && lyricsData.status == 'completed') { + updateJobState(job.id, { + currentStep: ProcessingStep.MUSIC, + lyrics: lyricsData.text || lyricsData.content + }); + } + break; + } + + case ProcessingStep.MUSIC: { + const songData = await fetchTableData(job.id, 'song'); + console.log(songData) + console.log(songData && songData.status == 'completed') + if (songData && songData.status == 'completed') { + updateJobState(job.id, { + currentStep: ProcessingStep.VIDEO + }); + } + break; + } + + case ProcessingStep.VIDEO: { + const videoData = await fetchTableData(job.id, 'creatomate_result_url'); + console.log(videoData) + console.log(videoData && videoData.status == 'completed') + if (videoData && videoData.status == 'completed') { + const resultUrl = typeof videoData.url === 'string' ? videoData.url : (videoData.result || videoData.output); + + updateJobState(job.id, { + status: JobStatus.COMPLETED, + currentStep: ProcessingStep.DONE, + resultUrl: resultUrl + }); + + // Job Completed: Refresh History + loadHistory(); + } + break; + } + + default: + break; + } + } catch (e) { + console.error(`Polling error for job ${job.id}`, e); + } + } + }, 60000); // Poll every 30 seconds + + return () => clearInterval(intervalId); + }, [jobs]); // Re-run effect if jobs array changes (e.g. status updates) + + const updateJobState = (id: string, updates: Partial) => { + setJobs(prevJobs => prevJobs.map(job => + job.id === id ? { ...job, ...updates } : job + )); + }; + + const fileInputRef = useRef(null); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const handleAttributeChange = (category: keyof MarketingFormData['attribute'], value: string) => { + setFormData(prev => ({ + ...prev, + attribute: { + ...prev.attribute, + [category]: value + } + })); + }; + + const handleFileSelect = (e: React.ChangeEvent) => { + if (e.target.files) { + const newFiles = Array.from(e.target.files); + setSelectedFiles(prev => [...prev, ...newFiles]); + + const newPreviews = newFiles.map(file => URL.createObjectURL(file)); + setPreviewUrls(prev => [...prev, ...newPreviews]); + } + }; + + const removeFile = (index: number) => { + setSelectedFiles(prev => prev.filter((_, i) => i !== index)); + setPreviewUrls(prev => { + URL.revokeObjectURL(prev[index]); + return prev.filter((_, i) => i !== index); + }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.customer_name) return; + + try { + setIsUploading(true); + + // Step 1: Generate Task ID + setSubmissionStep('Initializing Task ID...'); + const task_id = uuidv7(); + + // Step 2: Upload Images to Azure + setSubmissionStep('Uploading Images to Azure...'); + let uploadedUrls: string[] = []; + if (selectedFiles.length > 0) { + uploadedUrls = await uploadImagesToAzure(selectedFiles, task_id); + } + + // Step 3: Send Image Manifest to API 2 + setSubmissionStep('Syncing Image Data...'); + const manifest = uploadedUrls.map(url => ({ + task_id: task_id, + img_url: url + })); + + if (manifest.length > 0) { + await sendImageManifest(manifest); + } + + // Step 4: Submit Metadata to API 3 + setSubmissionStep('Finalizing Submission...'); + const payload = { + customer_name: formData.customer_name, + region: formData.region, + detail_region_info: formData.detail_region_info, + task_id: task_id, + attribute: formData.attribute + }; + + await submitJobMetadata(payload); + + // Refresh history immediately after submit to confirm operation + loadHistory(); + + // Step 5: Start Tracking + // Create new job object + const newJob: VideoJob = { + id: task_id, + createdAt: Date.now(), + data: { ...formData, imageUrls: uploadedUrls }, + status: JobStatus.PROCESSING, + currentStep: ProcessingStep.LYRICS // Start at Lyrics since upload is done + }; + + setJobs(prev => [newJob, ...prev]); + setActiveJobId(task_id); + setIsUploading(false); + + } catch (err) { + console.error("Submission error", err); + setIsUploading(false); + setSubmissionStep('Error occurred.'); + alert("Failed to submit job. Check console for details."); + } + }; + + const handleCreateNew = () => { + setActiveJobId(null); + setFormData({ + customer_name: '', + region: '', + detail_region_info: '', + attribute: { + genre: 'k-pop', + vocal: 'clear', + tempo: '130 bp', + mood: 'joyful' + }, + imageUrls: [] + }); + setSelectedFiles([]); + setPreviewUrls([]); + setSubmissionStep(''); + }; + + return ( +
+ {/* Background */} +
+
+ + {/* Main Layout */} +
+ + {/* Header */} +
+
+ +
+

Castad

+

Next-Gen Marketing Video Generator

+
+ + {/* View Switcher: Form OR Tracker */} + {activeJob ? ( +
+ + +
+ +
+
+ ) : ( +
+ {/* Business Info */} +
+

Business Information

+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ + {/* Attributes Section */} +
+

Vibe & Style

+ + {/* 1. Genre */} +
+ +
+ {ATTRIBUTE_OPTIONS.genres.map((opt) => ( + + ))} +
+
+ + {/* 2. Vocal */} +
+ +
+ {ATTRIBUTE_OPTIONS.vocals.map((opt) => ( + + ))} +
+
+ + {/* 3. Tempo */} +
+ +
+ {ATTRIBUTE_OPTIONS.tempos.map((opt) => ( + + ))} +
+
+ + {/* 4. Mood */} +
+ +
+ {ATTRIBUTE_OPTIONS.moods.map((opt) => ( + + ))} +
+
+
+ + {/* Images */} +
+ + +
fileInputRef.current?.click()} + > + +
+ +
+

Click to upload images

+
+ + {previewUrls.length > 0 && ( +
+ {previewUrls.map((url, idx) => ( +
+ preview + +
+ ))} +
+ )} +
+ + {/* Submit Button */} + +
+ )} + + {/* Gallery Section - Always Visible */} +
+ +
+
+
+ ); +}; + +export default App; diff --git a/README.md b/README.md new file mode 100644 index 0000000..db96b33 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1LbQRtjumcLDp4knwfpkaxKoHbZPbn_zE + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/components/ProcessTracker.tsx b/components/ProcessTracker.tsx new file mode 100644 index 0000000..1353968 --- /dev/null +++ b/components/ProcessTracker.tsx @@ -0,0 +1,80 @@ + +import React from 'react'; +import { ProcessingStep, VideoJob } from '../types'; +import { Loader2, CheckCircle2, Music, Video, FileText, UploadCloud } from 'lucide-react'; + +interface ProcessTrackerProps { + job: VideoJob; +} + +const steps = [ + { id: ProcessingStep.UPLOAD, label: "Upload", icon: UploadCloud }, + { id: ProcessingStep.LYRICS, label: "Lyrics", icon: FileText }, + { id: ProcessingStep.MUSIC, label: "Music", icon: Music }, + { id: ProcessingStep.VIDEO, label: "Video", icon: Video }, +]; + +export const ProcessTracker: React.FC = ({ job }) => { + const getCurrentStepIndex = () => { + if (job.currentStep === ProcessingStep.DONE) return steps.length; + return steps.findIndex(s => s.id === job.currentStep); + }; + + const currentIndex = getCurrentStepIndex(); + + return ( +
+

+ Creating Magic for {job.data.customer_name} +

+ +
+ {/* Connecting Line */} +
+
+
+ + {steps.map((step, index) => { + const isActive = index === currentIndex; + const isCompleted = index < currentIndex; + const Icon = step.icon; + + return ( +
+
+ {isCompleted ? : isActive ? : } +
+ + {step.label} + +
+ ); + })} +
+ +
+

+ {job.currentStep !== ProcessingStep.DONE ? `Current Step: ${job.currentStep}` : 'Finalizing output...'} +

+
+
+ ); +}; diff --git a/components/VideoGallery.tsx b/components/VideoGallery.tsx new file mode 100644 index 0000000..76cf947 --- /dev/null +++ b/components/VideoGallery.tsx @@ -0,0 +1,69 @@ + +import React from 'react'; +import { VideoHistoryItem } from '../types'; +import { Calendar, Film } from 'lucide-react'; + +interface VideoGalleryProps { + videos: VideoHistoryItem[]; +} + +export const VideoGallery: React.FC = ({ videos }) => { + return ( +
+

+ Your Creations +

+ + {videos.length === 0 ? ( +
+

No videos found. Create your first campaign above!

+
+ ) : ( +
+ {videos.map((video) => ( +
+ {/* Video Thumbnail Area */} +
+ {video.result_movie_url ? ( +
+ + {/* Content Area */} +
+
+
+ Created At +
+ + {video.created_at} +
+
+ {video.status && ( +
+ {video.status} +
+ )} +
+
+
+ ))} +
+ )} +
+ ); +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..cb8b470 --- /dev/null +++ b/index.html @@ -0,0 +1,83 @@ + + + + + + Castad - Marketing Generator + + + + + + + + +
+ + + \ No newline at end of file diff --git a/index.tsx b/index.tsx new file mode 100644 index 0000000..6ca5361 --- /dev/null +++ b/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); \ No newline at end of file diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..5ac5967 --- /dev/null +++ b/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Castad", + "description": "Castad - Generate stunning AI marketing videos for your business using lyrics and music.", + "requestFramePermissions": [] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..caf9344 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "castad", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "lucide-react": "^0.555.0", + "@google/genai": "^1.31.0", + "react-dom": "^19.2.1", + "react": "^19.2.1", + "uuid": "11.0.3", + "@azure/storage-blob": "12.17.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/services/apiService.ts b/services/apiService.ts new file mode 100644 index 0000000..982f691 --- /dev/null +++ b/services/apiService.ts @@ -0,0 +1,79 @@ + +import { VideoHistoryItem } from '../types'; + +const API_ENDPOINT_1="https://o2odev.app.n8n.cloud/webhook/get_video_list" +const API_ENDPOINT_2="https://o2odev.app.n8n.cloud/webhook/upload_image_path" +const API_ENDPOINT_3="https://o2odev.app.n8n.cloud/webhook/ado3" +const API_ENDPOINT_4="https://o2odev.app.n8n.cloud/webhook/mysql-api" + +export const fetchVideoHistory = async (): Promise => { + try { + const response = await fetch(API_ENDPOINT_1); + if (!response.ok) { + console.error("Failed to fetch history"); + return []; + } + const data = await response.json(); + return Array.isArray(data) ? data : []; + } catch (error) { + console.error("Error fetching video history:", error); + return []; + } +}; + +export const sendImageManifest = async (manifest: { task_id: string; img_url: string }[]) => { + const url = API_ENDPOINT_2; + + console.log(manifest) + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(manifest) + }); + + if (!response.ok) { + throw new Error(`Failed to send image manifest: ${response.statusText}`); + } + + return response.json(); +}; + +export const submitJobMetadata = async (data: any) => { + const url = API_ENDPOINT_3; + + console.log(data) + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + + if (!response.ok) { + throw new Error(`Failed to submit job metadata: ${response.statusText}`); + } + + return true; +}; + +export const fetchTableData = async (task_id: string, tableName: 'lyrics' | 'song' | 'creatomate_result_url') => { + const url = API_ENDPOINT_4; + + try { + // Calls API 4 with ?task_id=UUID&table=TABLE_NAME + const response = await fetch(`${url}?task_id=${task_id}&table=${tableName}`, { + method: 'GET' + }); + + if (!response.ok) { + // If 404, it likely means that step isn't done yet + return null; + } + var result = await response.json() + console.log(result[0]) + // The API seems to return an array, grab the first item if so + return Array.isArray(result) ? result[0] : result; + } catch (error) { + //console.error(`Error fetching table ${tableName}:`, error); + return []; + } +}; diff --git a/services/azureStorage.ts b/services/azureStorage.ts new file mode 100644 index 0000000..2a3a7a3 --- /dev/null +++ b/services/azureStorage.ts @@ -0,0 +1,56 @@ + +import { BlobServiceClient, BlockBlobClient} from '@azure/storage-blob'; + +export const uploadImagesToAzure = async (files: File[], task_idx: string): Promise => { + // const sasUrl = process.env.AZURE_STORAGE_SAS_URL; + // // Default to 'castad-data' if not set, or read from env + // const rootPath = process.env.AZURE_UPLOAD_ROOT || "castad-data"; + // const containerName = "castad-uploads"; // Ensure this matches your SAS token permissions or container + + // if (!sasUrl) { + // console.warn("AZURE_STORAGE_SAS_URL is missing in .env. Returning mock URLs."); + // return files.map((_, i) => `https://mock-azure.com/${rootPath}/${task_idx}/img/${i}.jpg`); + // } + const AZURE_SAS_TOKEN="sv=2024-11-04&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2027-03-12T17:32:56Z&st=2025-12-08T09:17:56Z&spr=https,http&sig=J0uF91mTBpSUVSk0O%2FG2IXmebCDRMYp1%2BNOtBVpcOKE%3D" + const AZURE_SERVICE_PATH="https://ado2mediastoragepublic.blob.core.windows.net/" + // // const rootPath = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx" + // // const blobPath = + try { + + // const sas_url = `https://ado2mediastoragepublic.blob.core.windows.net/?sv=2024-11-04&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2027-03-12T17:32:56Z&st=2025-12-08T09:17:56Z&spr=https,http&sig=J0uF91mTBpSUVSk0O%2FG2IXmebCDRMYp1%2BNOtBVpcOKE%3D`; + // const blobServiceClient = new BlobServiceClient(AZURE_SERVICE_PATH,); + // const blobContainerClient = blobServiceClient.getContainerClient("ado2-media-public-access") + // const blockBlobClient = new BlockBlobClient + + const uploadPromises = files.map(async (file, index) => { + const extension = file.name.split('.').pop() || 'jpg'; + // Path format: ROOT/task_id/img/uploadNumber.extension + + const blobName = `${AZURE_SERVICE_PATH}ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/${task_idx}-img-${index}.${extension}`; + const sasUrl = `${blobName}?${AZURE_SAS_TOKEN}` + // // console.log(`\nBlobUrl = ${sasUrl}\n`); + // const blockBlobClient = blobContainerClient.getBlockBlobClient(blobName); + + // await blockBlobClient.uploadData(file, { + // blobHTTPHeaders: { blobContentType: file.type } + // }); + // console.log(blockBlobClient.url) + // return blockBlobClient.url; + // var data = new FormData() + // data.append('file', file) + fetch(sasUrl, { + headers: {"Content-Type": file.type, + "x-ms-blob-type" : "BlockBlob"}, + method: 'PUT', + body: file // FormData 객체를 직접 body에 전달하면 Content-Type이 자동으로 설정됨 (multipart/form-data) + }) + console.log(blobName) + return blobName + }); + return await Promise.all(uploadPromises); + } + catch (error) { + console.error("Azure Upload Error:", error); + throw new Error("Failed to upload images to Azure."); + } +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c6eed5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "types": [ + "node" + ], + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "./*" + ] + }, + "allowImportingTsExtensions": true, + "noEmit": true + } +} \ No newline at end of file diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..bde8650 --- /dev/null +++ b/types.ts @@ -0,0 +1,49 @@ + +export enum JobStatus { + PENDING = 'PENDING', + PROCESSING = 'PROCESSING', + COMPLETED = 'COMPLETED', + FAILED = 'FAILED' +} + +export enum ProcessingStep { + UPLOAD = 'Uploading Assets', + LYRICS = 'Generating Lyrics', + MUSIC = 'Composing Music', + VIDEO = 'Synthesizing Video', + DONE = 'Finalizing' +} + +export interface MarketingFormData { + task_idx?: string; + customer_name: string; // Maps to businessName + region: string; + detail_region_info: string; + attribute: { + genre: string; + vocal: string; + tempo: string; + mood: string; + }; + imageUrls: string[]; +} + +export interface VideoJob { + id: string; + createdAt: number; + data: MarketingFormData; + status: JobStatus; + currentStep: ProcessingStep; + resultUrl?: string; + lyrics?: string; +} + +export interface VideoHistoryItem { + id: number; + input_history_id: number; + song_id: number; + task_id: string; + status: string; + result_movie_url: string; + created_at: string; +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ee5fb8d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import path from 'path'; +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); + return { + server: { + port: 3000, + host: '0.0.0.0', + }, + plugins: [react()], + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + } + } + }; +});