From 0725f393c20d5996f9d226dbc163330420098f5f Mon Sep 17 00:00:00 2001 From: jjiang10 Date: Tue, 3 Jun 2025 20:07:27 -0700 Subject: [PATCH] Web (#58) * init web * start the app * add python func * set project and tempaltes * split data template * split data template * view data sets * use backend apis * clean up * clean up --- poetry.lock | 23 +- pyproject.toml | 1 + readme-web.md | 23 + src/starfish/data_factory/storage/base.py | 20 + .../storage/in_memory/in_memory_storage.py | 16 + .../storage/local/data_handler.py | 26 + .../storage/local/local_storage.py | 14 +- .../storage/local/metadata_handler.py | 37 +- .../data_factory/storage/local/setup.py | 1 + src/starfish/data_factory/storage/models.py | 1 + .../starfish/generate_by_topic/generator.py | 8 +- web/.gitignore | 41 + web/CODE_OF_CONDUCT.md | 4 + web/api/__init__.py | 1 + web/api/api.py | 51 + web/api/main.py | 51 + web/api/routers/__init__.py | 1 + web/api/routers/dataset.py | 96 + web/api/routers/project.py | 130 + web/api/routers/template.py | 104 + web/api/storage.py | 74 + web/components.json | 20 + web/components/app-page.tsx | 339 + web/components/ui/accordion.tsx | 57 + web/components/ui/badge.tsx | 36 + web/components/ui/button.tsx | 57 + web/components/ui/card.tsx | 76 + web/components/ui/collapsible.tsx | 24 + web/components/ui/dialog.tsx | 122 + web/components/ui/input.tsx | 25 + web/components/ui/label.tsx | 26 + web/components/ui/select.tsx | 164 + web/components/ui/table.tsx | 116 + web/components/ui/tabs.tsx | 55 + web/components/ui/textarea.tsx | 24 + web/components/ui/toast.tsx | 129 + web/components/ui/toaster.tsx | 35 + web/components/ui/tooltip.tsx | 32 + web/components/ui/use-toast.ts | 191 + web/hooks/use-toast.ts | 194 + web/package-lock.json | 15875 ++++++++++++++++ web/package.json | 45 + web/postcss.config.js | 6 + web/public/amplify-ui.css | 6938 +++++++ web/public/eval.gif | Bin 0 -> 4733015 bytes web/public/generate.gif | Bin 0 -> 5272408 bytes web/public/microsoft_startups.png | Bin 0 -> 41352 bytes web/public/nvidia.png | Bin 0 -> 59560 bytes web/public/starfish_logo.png | Bin 0 -> 1679788 bytes web/src/app/api/dataset/evaluate/route.ts | 62 + web/src/app/api/dataset/get/route.ts | 12 + web/src/app/api/dataset/list/route.ts | 12 + web/src/app/api/dataset/save/route.ts | 12 + web/src/app/api/projects/create/route.ts | 110 + web/src/app/api/projects/delete/route.ts | 19 + web/src/app/api/projects/get/route.ts | 19 + web/src/app/api/projects/list/route.ts | 71 + web/src/app/api/template/list/route.ts | 55 + web/src/app/api/template/run/route.ts | 62 + web/src/app/api/utils/proxy.ts | 100 + web/src/app/blog/[slug]/page.tsx | 272 + web/src/app/blog/page.tsx | 75 + web/src/app/blog/queries.ts | 116 + web/src/app/blog/styles.module.css | 103 + web/src/app/contactus/page.tsx | 107 + web/src/app/dashboard/DashboardClient.tsx | 44 + web/src/app/dashboard/ExploreTemplates.tsx | 393 + web/src/app/dashboard/ProjectList.tsx | 92 + web/src/app/dashboard/page.tsx | 111 + web/src/app/favicon.ico | Bin 0 -> 15406 bytes web/src/app/layout.tsx | 100 + web/src/app/page.tsx | 11 + .../app/project/[...id]/DatasetPageClient.tsx | 121 + .../[...id]/components/DataFactory.tsx | 297 + .../[...id]/components/DatasetViewer.tsx | 304 + .../components/TemplateConfigureStep.tsx | 342 + .../components/TemplateEvaluateStep.tsx | 218 + .../[...id]/components/TemplateManager.tsx | 263 + .../components/TemplateResultsStep.tsx | 311 + .../components/TemplateRunningStep.tsx | 34 + .../components/TemplateSaveExportStep.tsx | 252 + .../components/TemplateWorkflowSteps.tsx | 67 + web/src/app/project/[...id]/constants.ts | 18 + .../[...id]/hooks/useDatasetVersions.ts | 32 + web/src/app/project/[...id]/loading.tsx | 14 + web/src/app/project/[...id]/page.tsx | 61 + web/src/app/project/[...id]/types.ts | 72 + web/src/app/project/[...id]/utils.ts | 36 + web/src/auth/components/NavBar.tsx | 249 + web/src/components/ui/alert.tsx | 59 + web/src/components/ui/animated-blog-card.tsx | 86 + .../components/ui/animated-blog-header.tsx | 27 + web/src/components/ui/checkbox.tsx | 30 + web/src/middleware.ts | 22 + web/src/sanity/client.ts | 8 + web/styles/globals.css | 67 + web/tailwind.config.js | 86 + web/tsconfig.json | 27 + 98 files changed, 30361 insertions(+), 9 deletions(-) create mode 100644 readme-web.md create mode 100644 web/.gitignore create mode 100644 web/CODE_OF_CONDUCT.md create mode 100644 web/api/__init__.py create mode 100644 web/api/api.py create mode 100644 web/api/main.py create mode 100644 web/api/routers/__init__.py create mode 100644 web/api/routers/dataset.py create mode 100644 web/api/routers/project.py create mode 100644 web/api/routers/template.py create mode 100644 web/api/storage.py create mode 100644 web/components.json create mode 100644 web/components/app-page.tsx create mode 100644 web/components/ui/accordion.tsx create mode 100644 web/components/ui/badge.tsx create mode 100644 web/components/ui/button.tsx create mode 100644 web/components/ui/card.tsx create mode 100644 web/components/ui/collapsible.tsx create mode 100644 web/components/ui/dialog.tsx create mode 100644 web/components/ui/input.tsx create mode 100644 web/components/ui/label.tsx create mode 100644 web/components/ui/select.tsx create mode 100644 web/components/ui/table.tsx create mode 100644 web/components/ui/tabs.tsx create mode 100644 web/components/ui/textarea.tsx create mode 100644 web/components/ui/toast.tsx create mode 100644 web/components/ui/toaster.tsx create mode 100644 web/components/ui/tooltip.tsx create mode 100644 web/components/ui/use-toast.ts create mode 100644 web/hooks/use-toast.ts create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/postcss.config.js create mode 100644 web/public/amplify-ui.css create mode 100644 web/public/eval.gif create mode 100644 web/public/generate.gif create mode 100644 web/public/microsoft_startups.png create mode 100644 web/public/nvidia.png create mode 100644 web/public/starfish_logo.png create mode 100644 web/src/app/api/dataset/evaluate/route.ts create mode 100644 web/src/app/api/dataset/get/route.ts create mode 100644 web/src/app/api/dataset/list/route.ts create mode 100644 web/src/app/api/dataset/save/route.ts create mode 100644 web/src/app/api/projects/create/route.ts create mode 100644 web/src/app/api/projects/delete/route.ts create mode 100644 web/src/app/api/projects/get/route.ts create mode 100644 web/src/app/api/projects/list/route.ts create mode 100644 web/src/app/api/template/list/route.ts create mode 100644 web/src/app/api/template/run/route.ts create mode 100644 web/src/app/api/utils/proxy.ts create mode 100644 web/src/app/blog/[slug]/page.tsx create mode 100644 web/src/app/blog/page.tsx create mode 100644 web/src/app/blog/queries.ts create mode 100644 web/src/app/blog/styles.module.css create mode 100644 web/src/app/contactus/page.tsx create mode 100644 web/src/app/dashboard/DashboardClient.tsx create mode 100644 web/src/app/dashboard/ExploreTemplates.tsx create mode 100644 web/src/app/dashboard/ProjectList.tsx create mode 100644 web/src/app/dashboard/page.tsx create mode 100644 web/src/app/favicon.ico create mode 100644 web/src/app/layout.tsx create mode 100644 web/src/app/page.tsx create mode 100644 web/src/app/project/[...id]/DatasetPageClient.tsx create mode 100644 web/src/app/project/[...id]/components/DataFactory.tsx create mode 100644 web/src/app/project/[...id]/components/DatasetViewer.tsx create mode 100644 web/src/app/project/[...id]/components/TemplateConfigureStep.tsx create mode 100644 web/src/app/project/[...id]/components/TemplateEvaluateStep.tsx create mode 100644 web/src/app/project/[...id]/components/TemplateManager.tsx create mode 100644 web/src/app/project/[...id]/components/TemplateResultsStep.tsx create mode 100644 web/src/app/project/[...id]/components/TemplateRunningStep.tsx create mode 100644 web/src/app/project/[...id]/components/TemplateSaveExportStep.tsx create mode 100644 web/src/app/project/[...id]/components/TemplateWorkflowSteps.tsx create mode 100644 web/src/app/project/[...id]/constants.ts create mode 100644 web/src/app/project/[...id]/hooks/useDatasetVersions.ts create mode 100644 web/src/app/project/[...id]/loading.tsx create mode 100644 web/src/app/project/[...id]/page.tsx create mode 100644 web/src/app/project/[...id]/types.ts create mode 100644 web/src/app/project/[...id]/utils.ts create mode 100644 web/src/auth/components/NavBar.tsx create mode 100644 web/src/components/ui/alert.tsx create mode 100644 web/src/components/ui/animated-blog-card.tsx create mode 100644 web/src/components/ui/animated-blog-header.tsx create mode 100644 web/src/components/ui/checkbox.tsx create mode 100644 web/src/middleware.ts create mode 100644 web/src/sanity/client.ts create mode 100644 web/styles/globals.css create mode 100644 web/tailwind.config.js create mode 100644 web/tsconfig.json diff --git a/poetry.lock b/poetry.lock index bea0e26..433f228 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1076,6 +1076,27 @@ files = [ [package.extras] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] +[[package]] +name = "fastapi" +version = "0.115.12" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, + {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.47.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + [[package]] name = "fastjsonschema" version = "2.21.1" @@ -6538,4 +6559,4 @@ youtube = ["pytube", "youtube-transcript-api"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "22f8c37a59d1fc1f5113cfcad17b6e522733a1d2d48382bdaa550103bba7f228" +content-hash = "cbc83990953f3451539e0850d1a2c3ff91c28e068fe6e8ddaeea85ba358d02fc" diff --git a/pyproject.toml b/pyproject.toml index dfee193..0a6c1e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ packages = [ [tool.poetry.dependencies] python = ">=3.10,<4.0" litellm = ">=1.65.1,<2.0.0" +fastapi = ">=0.95.0" loguru = ">=0.7.3,<0.8.0" cachetools = ">=5.5.2,<6.0.0" ollama = ">=0.4.7,<0.5.0" diff --git a/readme-web.md b/readme-web.md new file mode 100644 index 0000000..7080def --- /dev/null +++ b/readme-web.md @@ -0,0 +1,23 @@ + +#### Step 2: Start the Backend + +```bash +# Install Python dependencies +pip install -r api/requirements.txt + +# Start the API server +python -m web.api.main +``` + +#### Step 3: Start the Frontend + +```bash +NODE_OPTIONS='--inspect' +npm run dev +``` + +#### Step 4: Debug the Frontend + +```bash +NODE_OPTIONS='--inspect' npm run dev +``` diff --git a/src/starfish/data_factory/storage/base.py b/src/starfish/data_factory/storage/base.py index 87fc494..49daa4c 100644 --- a/src/starfish/data_factory/storage/base.py +++ b/src/starfish/data_factory/storage/base.py @@ -79,6 +79,11 @@ async def list_projects(self, limit: Optional[int] = None, offset: Optional[int] """List available projects.""" pass + @abstractmethod + async def delete_project(self, project_id: str) -> None: + """Delete a project.""" + pass + # Master Job methods @abstractmethod async def log_master_job_start(self, job_data: GenerationMasterJob) -> None: @@ -173,6 +178,21 @@ async def list_execution_jobs_by_master_id_and_config_hash(self, master_job_id: """Retrieve execution job details by master job id and config hash.""" pass + @abstractmethod + async def save_dataset(self, project_id: str, dataset_name: str, dataset_data: Dict[str, Any]) -> str: + """Save a dataset.""" + pass + + @abstractmethod + async def get_dataset(self, project_id: str, dataset_name: str) -> Dict[str, Any]: + """Get a dataset.""" + pass + + @abstractmethod + async def list_datasets(self, project_id: str) -> List[Dict[str, Any]]: + """List datasets for a project.""" + pass + from .registry import Registry diff --git a/src/starfish/data_factory/storage/in_memory/in_memory_storage.py b/src/starfish/data_factory/storage/in_memory/in_memory_storage.py index e300e32..3833f60 100644 --- a/src/starfish/data_factory/storage/in_memory/in_memory_storage.py +++ b/src/starfish/data_factory/storage/in_memory/in_memory_storage.py @@ -130,3 +130,19 @@ async def list_record_metadata(self, master_job_uuid: str, job_uuid: str) -> Lis async def list_execution_jobs_by_master_id_and_config_hash(self, master_job_id: str, config_hash: str, job_status: str) -> Optional[GenerationJob]: """Retrieve execution job details by master job id and config hash.""" pass + + async def save_dataset(self, project_id: str, dataset_name: str, dataset_data: Dict[str, Any]) -> str: + """Save a dataset.""" + pass + + async def get_dataset(self, project_id: str, dataset_name: str) -> Dict[str, Any]: + """Get a dataset.""" + pass + + async def list_datasets(self, project_id: str) -> List[Dict[str, Any]]: + """List datasets for a project.""" + pass + + async def delete_project(self, project_id: str) -> None: + """Delete a project.""" + pass diff --git a/src/starfish/data_factory/storage/local/data_handler.py b/src/starfish/data_factory/storage/local/data_handler.py index 60a4b13..ef29447 100644 --- a/src/starfish/data_factory/storage/local/data_handler.py +++ b/src/starfish/data_factory/storage/local/data_handler.py @@ -18,6 +18,7 @@ CONFIGS_DIR = "configs" DATA_DIR = "data" ASSOCIATIONS_DIR = "associations" +DATASETS_DIR = "datasets" class FileSystemDataHandler: @@ -31,6 +32,7 @@ def __init__(self, data_base_path: str): self.config_path = os.path.join(self.data_base_path, CONFIGS_DIR) self.record_data_path = os.path.join(self.data_base_path, DATA_DIR) self.assoc_path = os.path.join(self.data_base_path, ASSOCIATIONS_DIR) + self.datasets_path = os.path.join(self.data_base_path, DATASETS_DIR) # TODO: Consider locks if implementing JSONL appends for associations async def ensure_base_dirs(self): @@ -85,6 +87,30 @@ def generate_request_config_path_impl(self, master_job_id: str) -> str: path = os.path.join(self.config_path, f"{master_job_id}.request.json") return path # Return absolute path as the reference + async def save_dataset_impl(self, project_id: str, dataset_name: str, dataset_data: Dict[str, Any]): + path = os.path.join(self.datasets_path, project_id, f"{dataset_name}.json") + await self._save_json_file(path, dataset_data) + return path + + async def get_dataset_impl(self, project_id: str, dataset_name: str) -> Dict[str, Any]: + path = os.path.join(self.datasets_path, project_id, f"{dataset_name}.json") + return await self._read_json_file(path) + + async def list_datasets_impl(self, project_id: str) -> list[Dict[str, Any]]: + path = os.path.join(self.datasets_path, project_id) + files = await aio_os.listdir(path) + datasets = [] + + for i in range(len(files)): + file = files[i] + if file.endswith(".json"): + dataset = await self._read_json_file(os.path.join(path, file)) + + datasets.append( + {"id": i, "name": file[:-5], "created_at": file.split("__")[1][:-5], "record_count": len(dataset), "data": dataset, "status": "completed"} + ) + return datasets + async def get_request_config_impl(self, config_ref: str) -> Dict[str, Any]: return await self._read_json_file(config_ref) # Assumes ref is absolute path diff --git a/src/starfish/data_factory/storage/local/local_storage.py b/src/starfish/data_factory/storage/local/local_storage.py index 4b88dc5..38865db 100644 --- a/src/starfish/data_factory/storage/local/local_storage.py +++ b/src/starfish/data_factory/storage/local/local_storage.py @@ -90,8 +90,11 @@ async def save_project(self, project_data: Project) -> None: async def get_project(self, project_id: str) -> Optional[Project]: return await self._metadata_handler.get_project_impl(project_id) + async def delete_project(self, project_id: str) -> None: + await self._metadata_handler.delete_project_impl(project_id) + async def list_projects(self, limit: Optional[int] = None, offset: Optional[int] = None) -> List[Project]: - return await self._metadata_handler.list_projects_impl(limit, offset) + return await self._metadata_handler.list_projects_impl_data_template(limit, offset) async def log_master_job_start(self, job_data: GenerationMasterJob) -> None: await self._metadata_handler.log_master_job_start_impl(job_data) @@ -154,6 +157,15 @@ async def list_execution_jobs_by_master_id_and_config_hash(self, master_job_id: async def list_record_metadata(self, master_job_uuid: str, job_uuid: str) -> List[Record]: return await self._metadata_handler.list_record_metadata_impl(master_job_uuid, job_uuid) + async def save_dataset(self, project_id: str, dataset_name: str, dataset_data: Dict[str, Any]) -> str: + return await self._data_handler.save_dataset_impl(project_id, dataset_name, dataset_data) + + async def get_dataset(self, project_id: str, dataset_name: str) -> Dict[str, Any]: + return await self._data_handler.get_dataset_impl(project_id, dataset_name) + + async def list_datasets(self, project_id: str) -> List[Dict[str, Any]]: + return await self._data_handler.list_datasets_impl(project_id) + @register_storage("local") def create_local_storage(storage_uri: str, data_storage_uri_override: Optional[str] = None) -> LocalStorage: diff --git a/src/starfish/data_factory/storage/local/metadata_handler.py b/src/starfish/data_factory/storage/local/metadata_handler.py index 8fd9039..0b13987 100644 --- a/src/starfish/data_factory/storage/local/metadata_handler.py +++ b/src/starfish/data_factory/storage/local/metadata_handler.py @@ -225,10 +225,17 @@ async def batch_save_execution_jobs(self, jobs: List[GenerationJob]): async def save_project_impl(self, project_data: Project): sql = """ - INSERT OR REPLACE INTO Projects (project_id, name, description, created_when, updated_when) - VALUES (?, ?, ?, ?, ?); + INSERT OR REPLACE INTO Projects (project_id, name, template_name, description, created_when, updated_when) + VALUES (?, ?, ?, ?, ?,?); """ - params = (project_data.project_id, project_data.name, project_data.description, project_data.created_when, project_data.updated_when) + params = ( + project_data.project_id, + project_data.name, + project_data.template_name, + project_data.description, + project_data.created_when, + project_data.updated_when, + ) await self._execute_sql(sql, params) async def get_project_impl(self, project_id: str) -> Optional[Project]: @@ -236,6 +243,10 @@ async def get_project_impl(self, project_id: str) -> Optional[Project]: row = await self._fetchone_sql(sql, (project_id,)) return _row_to_pydantic(Project, row) + async def delete_project_impl(self, project_id: str) -> None: + sql = "DELETE FROM Projects WHERE project_id = ?" + await self._execute_sql(sql, (project_id,)) + async def list_projects_impl(self, limit: Optional[int], offset: Optional[int]) -> List[Project]: sql = "SELECT * FROM Projects ORDER BY name" params: List[Any] = [] @@ -256,6 +267,26 @@ async def list_projects_impl(self, limit: Optional[int], offset: Optional[int]) rows = await self._fetchall_sql(sql, tuple(params)) return [_row_to_pydantic(Project, row) for row in rows] + async def list_projects_impl_data_template(self, limit: Optional[int], offset: Optional[int]) -> List[Project]: + sql = "SELECT * FROM Projects WHERE template_name IS NOT NULL ORDER BY name" + params: List[Any] = [] + + # SQLite requires LIMIT when using OFFSET + if offset is not None: + if limit is not None: + sql += " LIMIT ? OFFSET ?" + params.extend([limit, offset]) + else: + # If no explicit limit but with offset, use a high limit + sql += " LIMIT 1000 OFFSET ?" + params.append(offset) + elif limit is not None: + sql += " LIMIT ?" + params.append(limit) + + rows = await self._fetchall_sql(sql, tuple(params)) + return [_row_to_pydantic(Project, row) for row in rows] + async def log_master_job_start_impl(self, job_data: GenerationMasterJob): data_dict = _serialize_pydantic_for_db(job_data) cols = ", ".join(data_dict.keys()) diff --git a/src/starfish/data_factory/storage/local/setup.py b/src/starfish/data_factory/storage/local/setup.py index 83b5410..ef357cf 100644 --- a/src/starfish/data_factory/storage/local/setup.py +++ b/src/starfish/data_factory/storage/local/setup.py @@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS Projects ( project_id TEXT PRIMARY KEY, name TEXT NOT NULL, + template_name TEXT, description TEXT, created_when TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_when TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP diff --git a/src/starfish/data_factory/storage/models.py b/src/starfish/data_factory/storage/models.py index e2da034..75bcf25 100644 --- a/src/starfish/data_factory/storage/models.py +++ b/src/starfish/data_factory/storage/models.py @@ -35,6 +35,7 @@ class Project(BaseModel): project_id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique project identifier.") name: str = Field(..., description="User-friendly project name.") + template_name: Optional[str] = Field(None, description="template name.") description: Optional[str] = Field(None, description="Optional description.") created_when: datetime.datetime = Field(default_factory=utc_now) updated_when: datetime.datetime = Field(default_factory=utc_now) diff --git a/src/starfish/data_gen_template/templates/starfish/generate_by_topic/generator.py b/src/starfish/data_gen_template/templates/starfish/generate_by_topic/generator.py index a13d059..6a1405b 100644 --- a/src/starfish/data_gen_template/templates/starfish/generate_by_topic/generator.py +++ b/src/starfish/data_gen_template/templates/starfish/generate_by_topic/generator.py @@ -43,17 +43,17 @@ class GenerateByTopicInput(BaseModel): dependencies=[], input_example="""{ "user_instruction": "Generate Q&A pairs about machine learning concepts", - "num_records": 100, - "records_per_topic": 5, + "num_records": 4, + "records_per_topic": 2, "topics": [ "supervised learning", "unsupervised learning", {"reinforcement learning": 3}, # This means generate 3 records for this topic "neural networks", ], - "topic_model_name": "openai/gpt-4", + "topic_model_name": "openai/gpt-4.1-mini", "topic_model_kwargs": {"temperature": 0.7}, - "generation_model_name": "openai/gpt-4", + "generation_model_name": "openai/gpt-4.1-mini", "generation_model_kwargs": {"temperature": 0.8, "max_tokens": 200}, "output_schema": [ {"name": "question", "type": "str"}, diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..0bfcbb2 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# amplify +.amplify +amplify_outputs* +amplifyconfiguration* \ No newline at end of file diff --git a/web/CODE_OF_CONDUCT.md b/web/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5b627cf --- /dev/null +++ b/web/CODE_OF_CONDUCT.md @@ -0,0 +1,4 @@ +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/web/api/__init__.py b/web/api/__init__.py new file mode 100644 index 0000000..9bac60b --- /dev/null +++ b/web/api/__init__.py @@ -0,0 +1 @@ +# Empty file to make this a package diff --git a/web/api/api.py b/web/api/api.py new file mode 100644 index 0000000..29d4ed5 --- /dev/null +++ b/web/api/api.py @@ -0,0 +1,51 @@ +import os +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from contextlib import asynccontextmanager + +from starfish.common.logger import get_logger + +logger = get_logger(__name__) + +from starfish.common.env_loader import load_env_file +from web.api.storage import setup_storage, close_storage + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup + await setup_storage() + yield + # Shutdown (if needed) + await close_storage() + + +# Import routers +from .routers import template, dataset, project + +current_dir = os.path.dirname(os.path.abspath(__file__)) +root_dir = os.path.normpath(os.path.join(current_dir, "..", "..")) # Go up two levels from web/api/ +env_path = os.path.join(root_dir, ".env") +load_env_file(env_path=env_path) + +# Initialize FastAPI app +app = FastAPI(title="Streaming API", lifespan=lifespan, description="API for streaming chat completions") + +# Configure CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Allows all origins + allow_credentials=True, + allow_methods=["*"], # Allows all methods + allow_headers=["*"], # Allows all headers +) + +# Include routers +app.include_router(template.router) +app.include_router(dataset.router) +app.include_router(project.router) + + +# Helper function to get adalflow root path +def get_adalflow_default_root_path(): + return os.path.expanduser(os.path.join("~", ".adalflow")) diff --git a/web/api/main.py b/web/api/main.py new file mode 100644 index 0000000..b8cfdcd --- /dev/null +++ b/web/api/main.py @@ -0,0 +1,51 @@ +import uvicorn +import os +import sys +import logging +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +# --- Unified Logging Configuration --- +# Determine the project's base directory (assuming main.py is in 'api' subdirectory) +# Adjust if your structure is different, e.g., if main.py is at the root. +# This assumes 'api/main.py', so logs will be in 'api/logs/application.log' +LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") +os.makedirs(LOG_DIR, exist_ok=True) +LOG_FILE_PATH = os.path.join(LOG_DIR, "application.log") + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(lineno)d %(filename)s:%(funcName)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler(LOG_FILE_PATH), + logging.StreamHandler(), # Also keep logging to console + ], + force=True, # Ensure this configuration takes precedence and clears any existing handlers +) + +# Get a logger for this main module (optional, but good practice) +logger = logging.getLogger(__name__) + +# Add the current directory to the path so we can import the api package +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Check for required environment variables +required_env_vars = ["GOOGLE_API_KEY", "OPENAI_API_KEY"] +missing_vars = [var for var in required_env_vars if not os.environ.get(var)] +if missing_vars: + logger.warning(f"Missing environment variables: {', '.join(missing_vars)}") + logger.warning("Some functionality may not work correctly without these variables.") + +if __name__ == "__main__": + # Get port from environment variable or use default + # port = int(os.environ.get("PORT", 8001)) + port = 8002 + # Import the app here to ensure environment variables are set first + from api.api import app + + logger.info(f"Starting Streaming API on port {port}") + + # Run the FastAPI app with uvicorn + uvicorn.run("api.api:app", host="0.0.0.0", port=port, reload=True) diff --git a/web/api/routers/__init__.py b/web/api/routers/__init__.py new file mode 100644 index 0000000..9bac60b --- /dev/null +++ b/web/api/routers/__init__.py @@ -0,0 +1 @@ +# Empty file to make this a package diff --git a/web/api/routers/dataset.py b/web/api/routers/dataset.py new file mode 100644 index 0000000..9dea6a2 --- /dev/null +++ b/web/api/routers/dataset.py @@ -0,0 +1,96 @@ +from fastapi import APIRouter, HTTPException + +from starfish.common.logger import get_logger +from starfish import StructuredLLM, data_factory +from web.api.storage import save_dataset, list_datasets_from_storage, get_dataset_from_storage + +logger = get_logger(__name__) + +router = APIRouter(prefix="/dataset", tags=["dataset"]) + + +@data_factory() +async def default_eval(input_data): + if not isinstance(input_data, str): + input_data = input_data + + eval_llm = StructuredLLM( + prompt="Given input data {{input_data}} please give it score from 1 to 10", + output_schema=[{"name": "quality_score", "type": "int"}], + model_name="gpt-4o-mini", + ) + + eval_response = await eval_llm.run(input_data=input_data) + return eval_response.data + + +@router.post("/evaluate") +async def evaluate_dataset(request: dict): + """ + Evaluate a dataset with the given inputs. + + This endpoint evaluates a dataset with the given inputs and returns the output. + + Returns: + The result of evaluating the dataset + """ + try: + # logger.info(f"Evaluating dataset: {request}") + result = request["evaluatedData"] + input_data = [] + for item in result: + input_data.append(str(item)) + processed_data = default_eval.run(input_data=input_data) + processed_data_index = default_eval.get_index_completed() + for i in range(len(processed_data_index)): + result[processed_data_index[i]]["quality_score"] = processed_data[i]["quality_score"] + + # for item in result: + # item["quality_score"] = 6 + return result + + except Exception as e: + logger.error(f"Error evaluating dataset: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error evaluating dataset: {str(e)}") + + +@router.post("/save") +async def save_dataset_api(request: dict): + """ + Save a dataset with the given inputs. + + This endpoint saves a dataset with the given inputs and returns the output. + """ + try: + # logger.info(f"Saving dataset: {request}") + await save_dataset(request["projectId"], request["datasetName"], request["data"]) + return request + except Exception as e: + logger.error(f"Error saving dataset: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error saving dataset: {str(e)}") + + +@router.post("/list") +async def list_datasets(request: dict): + """ + List all datasets for a given project. + """ + try: + datasets = await list_datasets_from_storage(request["projectId"], request["datasetType"]) + return datasets + except Exception as e: + logger.error(f"Error listing datasets: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error listing datasets: {str(e)}") + + +@router.post("/get") +async def get_dataset(request: dict): + """ + Get a dataset with the given inputs. + """ + try: + dataset = await get_dataset_from_storage(request["projectId"], request["datasetName"]) + return dataset + except Exception as e: + logger.error(f"Error getting dataset: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error getting dataset: {str(e)}") diff --git a/web/api/routers/project.py b/web/api/routers/project.py new file mode 100644 index 0000000..847eeb8 --- /dev/null +++ b/web/api/routers/project.py @@ -0,0 +1,130 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from typing import Optional + +from starfish.common.logger import get_logger +from starfish.data_factory.storage.models import Project +from web.api.storage import save_project, get_project, list_projects, delete_project + +logger = get_logger(__name__) + +router = APIRouter(prefix="/project", tags=["project"]) + + +class ProjectCreateRequest(BaseModel): + name: str + template_name: str + description: Optional[str] = None + + +@router.post("/create") +async def create_project(request: ProjectCreateRequest): + """ + Create a new project. + + This endpoint creates a new project with the given details and saves it using Local Storage. + + Args: + request: Project creation request containing name, description, and metadata + + Returns: + The created project details + """ + try: + logger.info(f"Creating project: {request.name}") + + # Create project instance + project = Project( + project_id=request.name, + name=request.name, + template_name=request.template_name, + description=request.description, + ) + + # Save project using local storage + await save_project(project) + + logger.info(f"Project created successfully: {project.project_id}") + return {"id": project.project_id, "name": project.name, "description": project.description, "created_at": project.created_when} + + except Exception as e: + logger.error(f"Error creating project: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error creating project: {str(e)}") + + +@router.get("/get") +async def get_project_endpoint(id: str): + """ + Get a project by ID. + + Args: + project_id: The ID of the project to retrieve + + Returns: + The project details + """ + try: + project = await get_project(id) + + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + return { + "id": project.project_id, + "name": project.name, + "template_name": project.template_name, + "description": project.description, + "created_at": project.created_when, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error retrieving project: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error retrieving project: {str(e)}") + + +@router.delete("/delete") +async def delete_project_endpoint(id: str): + """ + Delete a project by ID. + + Args: + id: The ID of the project to delete + + Returns: + The deleted project details + """ + try: + await delete_project(id) + return {"message": "Project deleted successfully"} + except Exception as e: + logger.error(f"Error deleting project: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error deleting project: {str(e)}") + + +@router.post("/list") +async def list_projects_endpoint(request: dict): + """ + List all projects. + + Returns: + List of all projects + """ + try: + projects = await list_projects() + + return [ + { + "id": project.project_id, + "name": project.name, + "template_name": project.template_name, + "description": project.description, + "created_at": project.created_when, + } + for project in projects + ] + + except Exception as e: + logger.error(f"Error listing projects: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error listing projects: {str(e)}") diff --git a/web/api/routers/template.py b/web/api/routers/template.py new file mode 100644 index 0000000..4c46ba5 --- /dev/null +++ b/web/api/routers/template.py @@ -0,0 +1,104 @@ +import ast +from fastapi import APIRouter, HTTPException +from typing import List, Optional, Type +from pydantic import BaseModel + +from starfish.common.logger import get_logger +from starfish import data_gen_template + +logger = get_logger(__name__) + +router = APIRouter(prefix="/template", tags=["template"]) + + +class TemplateRegister(BaseModel): + name: str = "starfish/generate_by_topic" + input_schema: Type[BaseModel] = None + output_schema: Optional[Type[BaseModel]] = None + description: str = """Generates diverse synthetic data across multiple topics based on user instructions. + Automatically creates relevant topics if not provided and handles deduplication across generated content. + """ + author: str = "Wendao Liu" + starfish_version: str = "0.1.3" + dependencies: List[str] = [] + input_example: str + + +class TemplateRunRequest(BaseModel): + templateName: str + inputs: dict + + +@router.get("/list") +async def get_template_list(): + """ + Get available model providers and their models. + + This endpoint returns the configuration of available model providers and their + respective models that can be used throughout the application. + + Returns: + List[TemplateRegister]: A list of template configurations + """ + try: + logger.info("Fetching model configurations") + templates = data_gen_template.list(is_detail=True) + for template in templates: + try: + input_example_str = template["input_example"] + if isinstance(input_example_str, str): + # Remove any leading/trailing whitespace and quotes + input_example_str = input_example_str.strip() + # If it starts and ends with triple quotes, remove them + if input_example_str.startswith('"""') and input_example_str.endswith('"""'): + input_example_str = input_example_str[3:-3].strip() + + # Try ast.literal_eval first (safest) + try: + template["input_example"] = ast.literal_eval(input_example_str) + except (ValueError, SyntaxError): + # If that fails, try eval with restricted globals (less safe but sometimes necessary) + logger.warning("Using eval() for complex expression - this should be avoided in production") + # Create a restricted environment for eval + safe_dict = {"__builtins__": {}} + template["input_example"] = eval(input_example_str, safe_dict) + + elif isinstance(input_example_str, dict): + # Already a dict, no conversion needed + pass + except Exception as err: + logger.error(f"Failed to parse input_example for template: {err}") + logger.error(f"Problematic string (first 500 chars): {str(input_example_str)[:500]}") + # Keep the original string if parsing fails + template["input_example"] = input_example_str + return templates + + except Exception as e: + logger.error(f"Error creating model configuration: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error fetching templates: {str(e)}") + + +@router.post("/run") +async def run_template(request: TemplateRunRequest): + """ + Run a template with the given inputs. + + This endpoint runs a template with the given inputs and returns the output. + + Returns: + The result of running the template + """ + try: + logger.info(f"Running template: {request.templateName}") + + data_gen_template.list() + template = data_gen_template.get(request.templateName) + result = await template.run(**request.inputs) + # data_factory.run(result) + for i in range(len(result)): + result[i]["id"] = i + return result + + except Exception as e: + logger.error(f"Error running template: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error running template: {str(e)}") diff --git a/web/api/storage.py b/web/api/storage.py new file mode 100644 index 0000000..e68e7d1 --- /dev/null +++ b/web/api/storage.py @@ -0,0 +1,74 @@ +# Move storage-related functions here +from typing import Dict, Any +from starfish.data_factory.storage.models import Project +from starfish.data_factory.constants import ( + LOCAL_STORAGE_URI, +) +from starfish.data_factory.storage.local.local_storage import create_local_storage +from starfish.common.logger import get_logger + +logger = get_logger(__name__) +# Create storage instance +storage = create_local_storage(LOCAL_STORAGE_URI) + + +async def setup_storage(): + """Setup storage - to be called during app startup""" + await storage.setup() + logger.info("Storage setup completed") + + +async def close_storage(): + """Close storage - to be called during app shutdown""" + await storage.close() + logger.info("Storage closed") + + +async def save_project(project: Project): + # Implementation here + await storage.save_project(project) + + +async def get_project(project_id: str): + # Implementation here + return await storage.get_project(project_id) + + +async def list_projects(): + # Implementation here + return await storage.list_projects() + + +async def delete_project(project_id: str): + # Implementation here + await storage.delete_project(project_id) + + +async def save_dataset(project_name: str, dataset_name: str, dataset_data: Dict[str, Any]): + # Implementation here + await storage.save_dataset(project_name, dataset_name, dataset_data) + + +async def get_dataset(project_name: str, dataset_name: str): + # Implementation here + return await storage.get_dataset(project_name, dataset_name) + + +async def list_datasets(project_name: str): + # Implementation here + return await storage.list_datasets(project_name) + + +async def list_datasets_from_storage(project_id: str, dataset_type: str): + # Implementation here + if dataset_type == "factory": + return [] + elif dataset_type == "template": + return await storage.list_datasets(project_id) + else: + raise ValueError(f"Invalid dataset type: {dataset_type}") + + +async def get_dataset_from_storage(project_id: str, dataset_name: str): + # Implementation here + return await storage.get_dataset(project_id, dataset_name) diff --git a/web/components.json b/web/components.json new file mode 100644 index 0000000..db71365 --- /dev/null +++ b/web/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "styles/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/web/components/app-page.tsx b/web/components/app-page.tsx new file mode 100644 index 0000000..cd05ec2 --- /dev/null +++ b/web/components/app-page.tsx @@ -0,0 +1,339 @@ +'use client'; + +import Image from 'next/image' +import { Button } from "@/components/ui/button" +import { useRouter } from 'next/navigation' +import { Database, Cpu, Share2, BarChart3, ChevronDown, Plus, Minus, PlayCircle, Github, Copy, Calendar } from 'lucide-react' +import { motion, AnimatePresence } from 'framer-motion' +import { useState } from 'react' +import Link from 'next/link' + +const colorScheme = { + primary: "#DB2777", // Main pink color for buttons and primary elements + hover: "#BE185D", // Darker pink for hover states + text: { + primary: "#DB2777", // Pink for primary text + secondary: "#6B7280", // Gray for secondary text + white: "#FFFFFF" // White text + }, + background: "#FDF2F8" // Light pink background +}; + +export default function HomePage() { + const router = useRouter(); + const [openFaqIndex, setOpenFaqIndex] = useState(null); + const [copied, setCopied] = useState(false); + + const copyCommand = () => { + navigator.clipboard.writeText('pip install starfish-core'); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const faqs = [ + { + question: "What is Starfishdata's mission in healthcare?", + answer: "Starfishdata is dedicated to solving the data bottleneck in Healthcare AI by providing high-quality, privacy-preserving synthetic data for research, development, and deployment of AI solutions." + }, + { + question: "How does Starfishdata ensure patient privacy?", + answer: "We use advanced generative models and strict privacy-preserving techniques to ensure that no real patient data is ever exposed or re-identifiable in our synthetic datasets." + }, + { + question: "Who can benefit from Starfishdata's solutions?", + answer: "Healthcare AI startups, hospitals, research institutions, and any organization facing data scarcity or privacy challenges in healthcare can benefit from our synthetic data platform." + }, + { + question: "What makes Starfishdata different from other synthetic data providers?", + answer: "Our exclusive focus on healthcare, deep expertise in generative AI, and commitment to regulatory compliance set us apart. We partner closely with clients to deliver data that accelerates innovation while protecting patient privacy." + } + ]; + + const toggleFaq = (index: number) => { + setOpenFaqIndex(openFaqIndex === index ? null : index); + }; + + const scrollToVideo = () => { + const videoSection = document.getElementById('youtube-video') + if (videoSection) { + videoSection.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }) + } + } + + return ( +
+ +
+ + Starfish Logo + + + Starfishdata + + + Solving the data bottleneck in Healthcare AI + + {/* Social Links (X, Discord, Hugging Face) */} +
+ + + + + + + + + + + + + + + + + + +
+ {/* Schedule a Call with Us - Main CTA */} + + + Schedule a Call with Us + + {/* Divider below CTA */} +
+ {/* Code block for pip install */} +
+
Try our open source library
+
+ pip install starfish-core + +
+
+ {/* Consistent Secondary Buttons under code block */} +
+ + + Star on GitHub + + +
+ {/* Divider before Supported by section */} +
+ {/* Supported by Section (social proof) */} +
+

Supported by

+
+ NVIDIA Inception Partner + Microsoft for Startups +
+
+
+
+ + {/* Scroll Indicator Section */} + +
+ Scroll to explore + +
+
+ + {/* FAQ Section */} +
+ + Frequently Asked Questions + + +
+ {faqs.map((faq, index) => ( + + + + {openFaqIndex === index && ( + +
+

{faq.answer}

+
+
+ )} +
+
+ ))} +
+
+ + {/* Footer CTA */} + +

+ Ready to Get Started? +

+

+ Join us in revolutionizing AI development with high-quality synthetic data +

+ {/* Add social row above the main footer CTA */} +
+ + + + + + + + + + + + + +
+ +
+
+ ) +} \ No newline at end of file diff --git a/web/components/ui/accordion.tsx b/web/components/ui/accordion.tsx new file mode 100644 index 0000000..8dcf9b6 --- /dev/null +++ b/web/components/ui/accordion.tsx @@ -0,0 +1,57 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/web/components/ui/badge.tsx b/web/components/ui/badge.tsx new file mode 100644 index 0000000..e87d62b --- /dev/null +++ b/web/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/web/components/ui/button.tsx b/web/components/ui/button.tsx new file mode 100644 index 0000000..0270f64 --- /dev/null +++ b/web/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/web/components/ui/card.tsx b/web/components/ui/card.tsx new file mode 100644 index 0000000..77e9fb7 --- /dev/null +++ b/web/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/web/components/ui/collapsible.tsx b/web/components/ui/collapsible.tsx new file mode 100644 index 0000000..85efe61 --- /dev/null +++ b/web/components/ui/collapsible.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +CollapsibleContent.displayName = CollapsiblePrimitive.CollapsibleContent.displayName + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } \ No newline at end of file diff --git a/web/components/ui/dialog.tsx b/web/components/ui/dialog.tsx new file mode 100644 index 0000000..95b0d38 --- /dev/null +++ b/web/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/web/components/ui/input.tsx b/web/components/ui/input.tsx new file mode 100644 index 0000000..a92b8e0 --- /dev/null +++ b/web/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/web/components/ui/label.tsx b/web/components/ui/label.tsx new file mode 100644 index 0000000..5341821 --- /dev/null +++ b/web/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/web/components/ui/select.tsx b/web/components/ui/select.tsx new file mode 100644 index 0000000..ac2a8f2 --- /dev/null +++ b/web/components/ui/select.tsx @@ -0,0 +1,164 @@ +"use client" + +import * as React from "react" +import { + CaretSortIcon, + CheckIcon, + ChevronDownIcon, + ChevronUpIcon, +} from "@radix-ui/react-icons" +import * as SelectPrimitive from "@radix-ui/react-select" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/web/components/ui/table.tsx b/web/components/ui/table.tsx new file mode 100644 index 0000000..fbfd2b5 --- /dev/null +++ b/web/components/ui/table.tsx @@ -0,0 +1,116 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} \ No newline at end of file diff --git a/web/components/ui/tabs.tsx b/web/components/ui/tabs.tsx new file mode 100644 index 0000000..0f4caeb --- /dev/null +++ b/web/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/web/components/ui/textarea.tsx b/web/components/ui/textarea.tsx new file mode 100644 index 0000000..d1258e4 --- /dev/null +++ b/web/components/ui/textarea.tsx @@ -0,0 +1,24 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface TextareaProps + extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +