feat: add internal API test harness

This commit introduces a new internal API test harness built with React and Vite. This harness provides a user interface for testing various API endpoints, including authentication, core integrations, and entity-related APIs.

The harness includes the following features:

- Firebase authentication integration for secure API testing.
- A modular design with separate components for different API categories.
- Form-based input for API parameters, allowing users to easily configure requests.
- JSON-based response display for clear and readable API results.
- Error handling and display for debugging purposes.
- A navigation system for easy access to different API endpoints.
- Environment-specific configuration for testing in different environments.

This harness will enable developers to quickly and efficiently test API endpoints, ensuring the quality and reliability of the KROW backend services.

The following files were added:

- Makefile: Added targets for installing, developing, building, and deploying the API test harness.
- firebase.json: Added hosting configurations for the API test harness in development and staging environments.
- firebase/internal-launchpad/index.html: Updated with accordion styles and navigation for diagrams and documents.
- internal-api-harness/.env.example: Example environment variables for the API test harness.
- internal-api-harness/.gitignore: Git ignore file for the API test harness.
- internal-api-harness/README.md: README file for the API test harness.
- internal-api-harness/components.json: Configuration file for shadcn-ui components.
- internal-api-harness/eslint.config.js: ESLint configuration file.
- internal-api-harness/index.html: Main HTML file for the API test harness.
- internal-api-harness/jsconfig.json: JSConfig file for the API test harness.
- internal-api-harness/package.json: Package file for the API test harness.
- internal-api-harness/postcss.config.js: PostCSS configuration file.
- internal-api-harness/public/logo.svg: Krow logo.
- internal-api-harness/public/vite.svg: Vite logo.
- internal-api-harness/src/App.css: CSS file for the App component.
- internal-api-harness/src/App.jsx: Main App component.
- internal-api-harness/src/api/client.js: API client for making requests to the backend.
- internal-api-harness/src/api/krowSDK.js: SDK for interacting with Krow APIs.
- internal-api-harness/src/assets/react.svg: React logo.
- internal-api-harness/src/components/ApiResponse.jsx: Component for displaying API responses.
- internal-api-harness/src/components/Layout.jsx: Layout component for the API test harness.
- internal-api-harness/src/components/ServiceTester.jsx: Component for testing individual services.
- internal-api-harness/src/components/ui/button.jsx: Button component.
- internal-api-harness/src/components/ui/card.jsx: Card component.
- internal-api-harness/src/components/ui/collapsible.jsx: Collapsible component.
- internal-api-harness/src/components/ui/input.jsx: Input component.
- internal-api-harness/src/components/ui/label.jsx: Label component.
- internal-api-harness/src/components/ui/select.jsx: Select component.
- internal-api-harness/src/components/ui/textarea.jsx: Textarea component.
- internal-api-harness/src/firebase.js: Firebase configuration file.
- internal-api-harness/src/index.css: Main CSS file.
- internal-api-harness/src/lib/utils.js: Utility functions.
- internal-api-harness/src/main.jsx: Main entry point for the React application.
- internal-api-harness/src/pages/ApiPlaceholder.jsx: Placeholder component for unimplemented APIs.
- internal-api-harness/src/pages/EntityTester.jsx: Component for testing entity APIs.
- internal-api-harness/src/pages/GenerateImage.jsx: Component for testing the Generate Image API.
- internal-api-harness/src/pages/Home.jsx: Home page component.
- internal-api-harness/src/pages/Login.jsx: Login page component.
- internal-api-harness/src/pages/auth/GetMe.jsx: Component for testing the Get Me API.
- internal-api-harness/src/pages/core/CreateSignedUrl.jsx: Component for testing the Create Signed URL API.
- internal-api-harness/src/pages/core/InvokeLLM.jsx: Component for testing the Invoke LLM API.
- internal-api-harness/src/pages/core/SendEmail.jsx: Component for testing the Send Email API.
- internal-api-harness/src/pages/core/UploadFile.jsx: Component for testing the Upload File API.
- internal-api-harness/src/pages/core/UploadPrivateFile.jsx: Component for testing the Upload Private File API.
- internal-api-harness/tailwind.config.js: Tailwind CSS configuration file.
- internal-api-harness/vite.config.js: Vite configuration file.
This commit is contained in:
bwnyasse
2025-11-16 21:45:17 -05:00
parent 831570f2e0
commit f7c2027065
47 changed files with 8707 additions and 154 deletions

View File

@@ -145,6 +145,23 @@ admin-build:
@node scripts/patch-admin-layout-for-env-label.js
@cd admin-web && VITE_APP_ENV=$(ENV) npm run build
# --- API Test Harness ---
harness-install:
@echo "--> Installing API Test Harness dependencies..."
@cd internal-api-harness && npm install
harness-dev:
@echo "--> Starting API Test Harness development server on http://localhost:5175 ..."
@cd internal-api-harness && npm run dev -- --port 5175
harness-build:
@echo "--> Building API Test Harness for production..."
@cd internal-api-harness && npm run build -- --mode $(ENV)
harness-deploy: harness-build
@echo "--> Deploying API Test Harness to [$(ENV)] environment..."
@firebase deploy --only hosting:api-harness-$(ENV) --project=$(FIREBASE_ALIAS)
deploy-admin: admin-build
@echo "--> Building and deploying Admin Console to Cloud Run [$(ENV)]..."
@echo " - Step 1: Building container image..."

View File

@@ -44,6 +44,38 @@
"destination": "/index.html"
}
]
},
{
"target": "api-harness-dev",
"public": "internal-api-harness/dist",
"site": "krow-api-harness-dev",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
},
{
"target": "api-harness-staging",
"public": "internal-api-harness/dist",
"site": "krow-api-harness-staging",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
],
"emulators": {

View File

@@ -94,6 +94,35 @@
cursor: grabbing;
}
/* Accordion styles */
.accordion-button {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.75rem 1rem;
font-size: 0.875rem;
color: #4b5563; /* text-gray-600 */
text-align: left;
background-color: #f9fafb; /* bg-gray-50 */
border-radius: 0.5rem; /* rounded-lg */
transition: background-color 0.2s ease;
}
.accordion-button:hover {
background-color: #f3f4f6; /* bg-gray-100 */
}
.accordion-button .chevron {
transition: transform 0.2s ease;
}
.accordion-button[aria-expanded="true"] .chevron {
transform: rotate(90deg);
}
.accordion-panel {
overflow: hidden;
transition: max-height 0.3s ease-out;
max-height: 0;
}
/* Modal styles */
.modal-overlay {
backdrop-filter: blur(4px);
@@ -566,150 +595,96 @@
});
// Build hierarchical structure from paths
function buildDiagramHierarchy(diagrams) {
const hierarchy = {};
function buildHierarchy(items, pathPrefix) {
const hierarchy = { _root: { _items: [], _children: {} } };
diagrams.forEach(diagram => {
const parts = diagram.path.split('/');
const relevantParts = parts.slice(2, -1); // Remove 'assets/diagrams/' and filename
let current = hierarchy;
relevantParts.forEach(part => {
if (!current[part]) {
current[part] = { _items: [], _children: {} };
}
current = current[part]._children;
});
// Add the item to appropriate level
if (relevantParts.length > 0) {
let parent = hierarchy[relevantParts[0]];
for (let i = 1; i < relevantParts.length; i++) {
parent = parent._children[relevantParts[i]];
}
parent._items.push(diagram);
} else {
// Root level diagrams
if (!hierarchy._root) {
hierarchy._root = { _items: [], _children: {} };
}
hierarchy._root._items.push(diagram);
items.forEach(item => {
let relativePath = item.path;
if (relativePath.startsWith('./')) {
relativePath = relativePath.substring(2);
}
if (relativePath.startsWith(pathPrefix)) {
relativePath = relativePath.substring(pathPrefix.length);
}
});
const parts = relativePath.split('/');
const relevantParts = parts.slice(0, -1); // remove filename
let current = hierarchy._root;
relevantParts.forEach(part => {
if (!current._children[part]) {
current._children[part] = { _items: [], _children: {} };
}
current = current._children[part];
});
current._items.push(item);
});
return hierarchy;
}
// Build hierarchical structure from paths (for documents)
function buildDocumentHierarchy(documents) {
const hierarchy = {};
// Generic function to create accordion navigation
function createAccordionNavigation(hierarchy, parentElement, createLinkFunction, sectionTitle) {
const createAccordion = (title, items, children) => {
const container = document.createElement('div');
container.className = 'mb-1';
documents.forEach(doc => {
const parts = doc.path.split('/');
const relevantParts = parts.slice(2, -1); // Remove 'assets/documents/' and filename
const button = document.createElement('button');
button.className = 'accordion-button';
button.setAttribute('aria-expanded', 'false');
button.innerHTML = `
<span class="font-medium">${title.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</span>
<svg class="chevron w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
`;
let current = hierarchy;
relevantParts.forEach(part => {
if (!current[part]) {
current[part] = { _items: [], _children: {} };
}
current = current[part]._children;
});
const panel = document.createElement('div');
panel.className = 'accordion-panel pl-4 pt-1';
// Add the item to appropriate level
if (relevantParts.length > 0) {
let parent = hierarchy[relevantParts[0]];
for (let i = 1; i < relevantParts.length; i++) {
parent = parent._children[relevantParts[i]];
}
parent._items.push(doc);
} else {
// Root level documents
if (!hierarchy._root) {
hierarchy._root = { _items: [], _children: {} };
}
hierarchy._root._items.push(doc);
if (items) {
items.forEach(item => createLinkFunction(item, panel, 1));
}
});
return hierarchy;
}
// Create navigation from hierarchy
function createNavigation(hierarchy, parentElement, level = 0) {
// First, show root level items if any
if (hierarchy._root && hierarchy._root._items.length > 0) {
const mainHeading = document.createElement('div');
mainHeading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-6 mb-3';
mainHeading.textContent = 'Diagrams';
parentElement.appendChild(mainHeading);
hierarchy._root._items.forEach(diagram => {
createDiagramLink(diagram, parentElement, 0);
});
}
// Then process nested categories
Object.keys(hierarchy).forEach(key => {
if (key === '_items' || key === '_children' || key === '_root') return;
const section = hierarchy[key];
const heading = document.createElement('div');
heading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider ' +
(level === 0 ? 'mt-6 mb-3' : 'mt-4 mb-2 pl-8');
heading.textContent = key.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
parentElement.appendChild(heading);
// Add items in this section
if (section._items && section._items.length > 0) {
section._items.forEach(diagram => {
createDiagramLink(diagram, parentElement, level);
if (children) {
Object.keys(children).forEach(childKey => {
const childSection = children[childKey];
const childHeading = document.createElement('div');
childHeading.className = 'px-4 pt-2 pb-1 text-xs font-semibold text-gray-400 uppercase tracking-wider';
childHeading.textContent = childKey.replace(/-/g, ' ');
panel.appendChild(childHeading);
childSection._items.forEach(item => createLinkFunction(item, panel, 2));
});
}
// Recursively add children
if (section._children && Object.keys(section._children).length > 0) {
createNavigation(section._children, parentElement, level + 1);
}
});
}
// Create document navigation from hierarchy
function createDocumentNavigation(hierarchy, parentElement, level = 0) {
// First, show root level items if any
if (hierarchy._root && hierarchy._root._items.length > 0) {
const mainHeading = document.createElement('div');
mainHeading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-6 mb-3';
mainHeading.textContent = 'Documentation';
parentElement.appendChild(mainHeading);
hierarchy._root._items.forEach(doc => {
createDocumentLink(doc, parentElement, 0);
button.addEventListener('click', () => {
const isExpanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', !isExpanded);
if (!isExpanded) {
panel.style.maxHeight = panel.scrollHeight + 'px';
} else {
panel.style.maxHeight = '0px';
}
});
container.appendChild(button);
container.appendChild(panel);
return container;
};
const heading = document.createElement('div');
heading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-6 mb-3';
heading.textContent = sectionTitle;
parentElement.appendChild(heading);
// Process root items first
if (hierarchy._root && hierarchy._root._items.length > 0) {
hierarchy._root._items.forEach(item => createLinkFunction(item, parentElement, 0));
}
// Then process nested categories
Object.keys(hierarchy).forEach(key => {
if (key === '_items' || key === '_children' || key === '_root') return;
const section = hierarchy[key];
const heading = document.createElement('div');
heading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider ' +
(level === 0 ? 'mt-6 mb-3' : 'mt-4 mb-2 pl-8');
heading.textContent = key.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
parentElement.appendChild(heading);
// Add items in this section
if (section._items && section._items.length > 0) {
section._items.forEach(doc => {
createDocumentLink(doc, parentElement, level);
});
}
// Recursively add children
if (section._children && Object.keys(section._children).length > 0) {
createDocumentNavigation(section._children, parentElement, level + 1);
}
// Process categories as accordions
Object.keys(hierarchy._root._children).forEach(key => {
if (key.startsWith('_')) return;
const section = hierarchy._root._children[key];
const accordion = createAccordion(key, section._items, section._children);
parentElement.appendChild(accordion);
});
}
@@ -717,8 +692,8 @@
function createDocumentLink(doc, parentElement, level) {
const link = document.createElement('a');
link.href = '#';
link.className = 'nav-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-gray-50 mb-1' +
(level > 0 ? ' pl-8' : '');
link.className = 'nav-item flex items-center space-x-3 px-4 py-2.5 rounded-lg text-gray-600 hover:bg-gray-100 text-sm mb-1' +
(level > 0 ? ' ' : '');
link.onclick = (e) => {
e.preventDefault();
showView('document', link, doc.path, doc.title);
@@ -728,7 +703,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>`;
link.innerHTML = `${iconSvg}<span class="text-sm">${doc.title}</span>`;
link.innerHTML = `${iconSvg}<span class="truncate">${doc.title}</span>`;
parentElement.appendChild(link);
}
@@ -736,26 +711,18 @@
function createDiagramLink(diagram, parentElement, level) {
const link = document.createElement('a');
link.href = '#';
link.className = 'nav-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-gray-50 mb-1' +
(level > 0 ? ' pl-8' : '');
link.className = 'nav-item flex items-center space-x-3 px-4 py-2.5 rounded-lg text-gray-600 hover:bg-gray-100 text-sm mb-1' +
(level > 0 ? ' ' : '');
link.onclick = (e) => {
e.preventDefault();
showView('diagram', link, diagram.path, diagram.title, diagram.type);
};
// Get icon based on type or custom icon
let iconSvg = '';
if (diagram.type === 'svg') {
iconSvg = `<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>`;
} else {
iconSvg = `<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>`;
}
const iconSvg = `<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4l2 2h4a2 2 0 012 2v12a2 2 0 01-2 2h-4l-2-2H7z"></path>
</svg>`;
link.innerHTML = iconSvg + '<span class="text-sm">' + diagram.title + '</span>';
link.innerHTML = `${iconSvg}<span class="truncate">${diagram.title}</span>`;
parentElement.appendChild(link);
}
@@ -770,12 +737,11 @@
}
const text = await response.text();
console.log('Loaded config:', text);
allDiagrams = JSON.parse(text);
if (allDiagrams && allDiagrams.length > 0) {
const hierarchy = buildDiagramHierarchy(allDiagrams);
createNavigation(hierarchy, dynamicSection);
const hierarchy = buildHierarchy(allDiagrams, 'assets/diagrams/');
createAccordionNavigation(hierarchy, dynamicSection, createDiagramLink, 'Diagrams');
}
} catch (error) {
console.error('Error loading diagrams configuration:', error);
@@ -801,12 +767,11 @@
}
const text = await response.text();
console.log('Loaded documents config:', text);
allDocuments = JSON.parse(text);
if (allDocuments && allDocuments.length > 0) {
const hierarchy = buildDocumentHierarchy(allDocuments);
createDocumentNavigation(hierarchy, documentationSection);
const hierarchy = buildHierarchy(allDocuments, 'assets/documents/');
createAccordionNavigation(hierarchy, documentationSection, createDocumentLink, 'Documentation');
}
} catch (error) {
console.error('Error loading documents configuration:', error);

View File

@@ -0,0 +1,8 @@
VITE_HARNESS_FIREBASE_API_KEY="your-api-key"
VITE_HARNESS_FIREBASE_AUTH_DOMAIN="your-auth-domain"
VITE_HARNESS_FIREBASE_PROJECT_ID="your-project-id"
VITE_HARNESS_FIREBASE_STORAGE_BUCKET="your-storage-bucket"
VITE_HARNESS_FIREBASE_MESSAGING_SENDER_ID="your-messaging-sender-id"
VITE_HARNESS_FIREBASE_APP_ID="your-app-id"
VITE_HARNESS_ENVIRONMENT="dev"
VITE_API_BASE_URL="http://localhost:8080"

24
internal-api-harness/.gitignore vendored Normal file
View File

@@ -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?

View File

@@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

View File

@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>internal-api-harness</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
}
}

6611
internal-api-harness/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
{
"name": "internal-api-harness",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"firebase": "^12.6.0",
"lucide-react": "^0.553.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-firebase-hooks": "^5.1.1",
"react-router-dom": "^7.9.6",
"shadcn-ui": "^0.9.5",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"autoprefixer": "^10.4.22",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"vite": "^7.2.2"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="200 180 280 140" style="enable-background:new 0 0 500 500;" xml:space="preserve">
<style type="text/css">
.st0{fill:#24303B;}
.st1{fill:#002FE3;}
</style>
<g>
<path class="st1" d="M459.81,202.55c-5.03,0.59-9.08,4.49-10.36,9.38l-15.99,59.71l-16.24-56.3
c-1.68-5.92-6.22-10.86-12.19-12.34c-1.58-0.39-3.11-0.54-4.64-0.49h-0.15c-1.53-0.05-3.11,0.1-4.64,0.49
c-5.97,1.48-10.51,6.42-12.24,12.34l-3.6,12.53l-11.35,39.38l-7.9-27.54c-10.76-37.5-48.56-62.23-88.38-55.32
c-33.26,5.82-57.05,35.68-56.99,69.48v0.79c0,4.34,0.39,8.73,1.13,13.18c0.18,1.02,0.37,2.03,0.6,3.03
c1.84,8.31,10.93,12.73,18.49,8.8v0c5.36-2.79,7.84-8.89,6.42-14.77c-0.85-3.54-1.28-7.23-1.23-11.03
c0-25.02,20.48-45.5,45.55-45.2c7.6,0.1,15.59,2.07,23.59,6.37c13.52,7.3,23.15,20.18,27.34,34.94l13.32,46.34
c1.73,5.97,6.22,11,12.24,12.58c9.62,2.62,19-3.06,21.51-12.04l16.09-56.7l0.2-0.1l16.09,56.85c1.63,5.68,5.87,10.41,11.55,11.99
c9.13,2.57,18.11-2.66,20.67-11.2l24.13-79.6C475.35,209.85,468.64,201.56,459.81,202.55z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -0,0 +1,45 @@
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Layout from "./components/Layout";
import Login from "./pages/Login";
import Home from "./pages/Home";
import GenerateImage from "./pages/GenerateImage";
import ApiPlaceholder from "./pages/ApiPlaceholder";
import GetMe from "./pages/auth/GetMe";
import SendEmail from "./pages/core/SendEmail";
import EntityTester from "./pages/EntityTester";
import InvokeLLM from "./pages/core/InvokeLLM";
import UploadFile from "./pages/core/UploadFile";
import UploadPrivateFile from "./pages/core/UploadPrivateFile";
import CreateSignedUrl from "./pages/core/CreateSignedUrl";
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
{/* Auth APIs */}
<Route path="/auth/me" element={<GetMe />} />
<Route path="/auth/update-me" element={<ApiPlaceholder title="Update Me" />} />
{/* Core APIs */}
<Route path="/core/send-email" element={<SendEmail />} />
<Route path="/core/invoke-llm" element={<InvokeLLM />} />
<Route path="/core/upload-file" element={<UploadFile />} />
<Route path="/core/upload-private-file" element={<UploadPrivateFile />} />
<Route path="/core/create-signed-url" element={<CreateSignedUrl />} />
<Route path="/core/extract-data" element={<ApiPlaceholder title="Extract Data from File" />} />
<Route path="/core/generate-image" element={<GenerateImage />} />
{/* Entity APIs */}
<Route path="/entities" element={<EntityTester />} />
</Route>
</Routes>
</Router>
);
}
export default App;

View File

@@ -0,0 +1,22 @@
import axios from "axios";
import { auth } from "../firebase";
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // You will need to add this to your .env file
});
apiClient.interceptors.request.use(
async (config) => {
const user = auth.currentUser;
if (user) {
const token = await user.getIdToken();
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
export default apiClient;

View File

@@ -0,0 +1,147 @@
import apiClient from './client';
import { auth } from '../firebase';
import { signOut } from 'firebase/auth';
// --- Auth Module ---
const authModule = {
/**
* Fetches the currently authenticated user's profile from the backend.
* @returns {Promise<object>} The user profile.
*/
me: async () => {
const { data } = await apiClient.get('/auth/me');
return data;
},
/**
* Logs the user out.
* @param {string} [redirectUrl] - Optional URL to redirect to after logout.
*/
logout: async (redirectUrl) => {
await signOut(auth);
if (redirectUrl) {
window.location.href = redirectUrl;
}
},
/**
* Checks if a user is currently authenticated.
* @returns {boolean} True if a user is authenticated.
*/
isAuthenticated: () => {
return !!auth.currentUser;
},
};
// --- Core Integrations Module ---
const coreIntegrationsModule = {
/**
* Sends an email.
* @param {object} params - { to, subject, body }
* @returns {Promise<object>} API response.
*/
SendEmail: async (params) => {
const { data } = await apiClient.post('/sendEmail', params);
return data;
},
/**
* Invokes a large language model.
* @param {object} params - { prompt, response_json_schema, file_urls }
* @returns {Promise<object>} API response.
*/
InvokeLLM: async (params) => {
const { data } = await apiClient.post('/invokeLLM', params);
return data;
},
/**
* Uploads a public file.
* @param {File} file - The file to upload.
* @returns {Promise<object>} API response with file_url.
*/
UploadFile: async ({ file }) => {
const formData = new FormData();
formData.append('file', file);
const { data } = await apiClient.post('/uploadFile', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return data;
},
/**
* Uploads a private file.
* @param {File} file - The file to upload.
* @returns {Promise<object>} API response with file_uri.
*/
UploadPrivateFile: async ({ file }) => {
const formData = new FormData();
formData.append('file', file);
const { data } = await apiClient.post('/uploadPrivateFile', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return data;
},
/**
* Creates a temporary signed URL for a private file.
* @param {object} params - { file_uri, expires_in }
* @returns {Promise<object>} API response with signed_url.
*/
CreateFileSignedUrl: async (params) => {
const { data } = await apiClient.post('/createSignedUrl', params);
return data;
},
};
// --- Entities Module ---
// Based on docs/07-reference-base44-api-export.md
const entityNames = [
"User", "Event", "Staff", "Vendor", "VendorRate", "Invoice", "Business",
"Certification", "Team", "Conversation", "Message", "ActivityLog",
"Enterprise", "Sector", "Partner", "Order", "Shift"
];
const entitiesModule = {};
entityNames.forEach(entityName => {
// This factory creates a standard set of CRUD-like methods for each entity.
// It assumes a consistent RESTful endpoint structure: /entities/{EntityName}/{method}
entitiesModule[entityName] = {
get: async (params) => {
const { data } = await apiClient.get(`/entities/${entityName}/get`, { params });
return data;
},
create: async (params) => {
const { data } = await apiClient.post(`/entities/${entityName}/create`, params);
return data;
},
update: async (params) => {
const { data } = await apiClient.post(`/entities/${entityName}/update`, params);
return data;
},
delete: async (params) => {
const { data } = await apiClient.post(`/entities/${entityName}/delete`, params);
return data;
},
filter: async (params) => {
const { data } = await apiClient.post(`/entities/${entityName}/filter`, params);
return data;
},
list: async () => {
// Assuming a 'filter' with no params can act as 'list'
const { data } = await apiClient.post(`/entities/${entityName}/filter`, {});
return data;
}
};
});
// --- Main SDK Export ---
export const krowSDK = {
auth: authModule,
integrations: {
Core: coreIntegrationsModule,
},
entities: entitiesModule,
};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,38 @@
const ApiResponse = ({ response, error, loading }) => {
if (loading) {
return (
<div className="mt-4">
<h3 className="text-lg font-semibold text-slate-800 mb-2">Response</h3>
<div className="bg-slate-100 border border-slate-200 rounded-lg p-4">
<p className="text-slate-500">Loading...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="mt-4">
<h3 className="text-lg font-semibold text-red-800 mb-2">Error</h3>
<pre className="bg-red-50 border border-red-200 text-red-700 rounded-lg p-4 text-sm whitespace-pre-wrap">
{JSON.stringify(error, null, 2)}
</pre>
</div>
);
}
if (response) {
return (
<div className="mt-4">
<h3 className="text-lg font-semibold text-slate-800 mb-2">Response</h3>
<pre className="bg-slate-100 border border-slate-200 rounded-lg p-4 text-sm whitespace-pre-wrap">
{JSON.stringify(response, null, 2)}
</pre>
</div>
);
}
return null;
};
export default ApiResponse;

View File

@@ -0,0 +1,125 @@
import { Link, Navigate, Outlet, useLocation } from "react-router-dom";
import { useAuthState } from "react-firebase-hooks/auth";
import { auth } from "../firebase";
import { krowSDK } from "@/api/krowSDK";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown } from "lucide-react";
import { useState } from "react";
const navLinks = {
auth: [
{ path: "/auth/me", title: "Get Me" },
{ path: "/auth/update-me", title: "Update Me" },
],
core: [
{ path: "/core/send-email", title: "Send Email" },
{ path: "/core/invoke-llm", title: "Invoke LLM" },
{ path: "/core/upload-file", title: "Upload File" },
{ path: "/core/upload-private-file", title: "Upload Private File" },
{ path: "/core/create-signed-url", title: "Create Signed URL" },
{ path: "/core/extract-data", title: "Extract Data from File" },
{ path: "/core/generate-image", title: "Generate Image" },
],
entities: [
{ path: "/entities", title: "Entity API Tester" }
]
};
const NavSection = ({ title, links }) => {
const location = useLocation();
const [isOpen, setIsOpen] = useState(true);
const navLinkClasses = (path) =>
`flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors ${
location.pathname === path
? "bg-slate-200 text-slate-900"
: "text-slate-600 hover:bg-slate-100"
}`;
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger className="w-full">
<div className="flex items-center justify-between px-3 py-2">
<h2 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
{title}
</h2>
<ChevronDown className={`w-4 h-4 text-slate-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</div>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-1">
{links.map((api) => (
<Link key={api.path} to={api.path} className={navLinkClasses(api.path)}>
{api.title}
</Link>
))}
</CollapsibleContent>
</Collapsible>
);
}
const Layout = () => {
const [user, loading] = useAuthState(auth);
const location = useLocation();
if (loading) {
return (
<div className="flex items-center justify-center h-screen">
<div>Loading...</div>
</div>
);
}
if (!user) {
return <Navigate to="/login" />;
}
const handleLogout = () => {
krowSDK.auth.logout();
};
return (
<div className="flex h-screen bg-slate-50">
{/* Sidebar */}
<aside className="w-72 bg-white border-r border-slate-200 flex flex-col">
<div className="p-6 border-b border-slate-200">
<div className="flex items-center space-x-3">
<img src="/logo.svg" alt="Krow Logo" className="h-12 w-12" />
<div>
<h1 className="text-xl font-bold text-slate-900">KROW</h1>
<p className="text-xs text-slate-500">API Test Harness ({import.meta.env.VITE_HARNESS_ENVIRONMENT})</p>
</div>
</div>
</div>
<nav className="flex-1 overflow-y-auto p-4 space-y-2">
<NavSection title="Auth" links={navLinks.auth} />
<NavSection title="Core Integrations" links={navLinks.core} />
<NavSection title="Entities" links={navLinks.entities} />
</nav>
<div className="p-4 border-t border-slate-200">
<div className="text-sm text-slate-600 truncate mb-2" title={user.email}>{user.email}</div>
<Button onClick={handleLogout} className="w-full">
Logout
</Button>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 overflow-y-auto">
<div className="p-8">
<Outlet />
</div>
</main>
</div>
);
};
export default Layout;

View File

@@ -0,0 +1,120 @@
import { useState } from "react";
import apiClient from "../api/client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
const ServiceTester = ({ serviceName, serviceDescription, endpoint, fields }) => {
const [formData, setFormData] = useState({});
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const handleChange = (e) => {
const { name, value, files } = e.target;
if (files) {
setFormData({ ...formData, [name]: files[0] });
} else {
setFormData({ ...formData, [name]: value });
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
setResponse(null);
const data = new FormData();
for (const key in formData) {
data.append(key, formData[key]);
}
try {
const res = await apiClient.post(endpoint, data);
setResponse(res.data);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-slate-900">{serviceName}</h1>
<p className="text-slate-600 mt-1">{serviceDescription}</p>
</div>
<form onSubmit={handleSubmit}>
<Card>
<CardHeader>
<CardTitle>Parameters</CardTitle>
<CardDescription>
Fill in the required parameters to execute the service.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{fields.map((field) => (
<div key={field.name} className="grid w-full max-w-sm items-center gap-1.5">
<Label htmlFor={field.name}>{field.label}</Label>
{field.type === "textarea" ? (
<Textarea
id={field.name}
name={field.name}
onChange={handleChange}
placeholder={field.placeholder}
/>
) : (
<Input
id={field.name}
name={field.name}
type={field.type}
onChange={handleChange}
placeholder={field.placeholder}
/>
)}
</div>
))}
</CardContent>
<CardFooter>
<Button type="submit" disabled={loading}>
{loading ? "Executing..." : "Execute"}
</Button>
</CardFooter>
</Card>
</form>
{response && (
<div className="mt-8">
<h2 className="text-xl font-bold text-slate-900 mb-2">Response</h2>
<Card>
<CardContent className="p-4">
<pre className="text-sm overflow-x-auto bg-slate-900 text-white p-4 rounded-md">
{JSON.stringify(response, null, 2)}
</pre>
</CardContent>
</Card>
</div>
)}
{error && (
<div className="mt-8">
<h2 className="text-xl font-bold text-red-600 mb-2">Error</h2>
<Card className="border-red-500">
<CardContent className="p-4">
<pre className="text-sm overflow-x-auto bg-red-50 text-red-800 p-4 rounded-md">
{JSON.stringify(error, null, 2)}
</pre>
</CardContent>
</Card>
</div>
)}
</div>
);
};
export default ServiceTester;

View File

@@ -0,0 +1,48 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
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",
},
}
)
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props} />
);
})
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,50 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
{...props} />
))
Card.displayName = "Card"
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props} />
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props} />
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props} />
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@@ -0,0 +1,19 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props} />
);
})
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,16 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva } 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(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -0,0 +1,105 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
}

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props} />
);
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@@ -0,0 +1,17 @@
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: import.meta.env.VITE_HARNESS_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_HARNESS_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_HARNESS_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_HARNESS_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_HARNESS_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_HARNESS_FIREBASE_APP_ID
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);

View File

@@ -0,0 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,6 @@
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,13 @@
const ApiPlaceholder = ({ title }) => {
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">{title}</h1>
<div className="bg-slate-100 border border-slate-200 rounded-lg p-8 text-center">
<p className="text-slate-500">This page is a placeholder for the "{title}" API test harness.</p>
<p className="text-slate-500 mt-2">Implementation is pending.</p>
</div>
</div>
);
};
export default ApiPlaceholder;

View File

@@ -0,0 +1,190 @@
import { useState, useMemo } from "react";
import { krowSDK } from "@/api/krowSDK";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import ApiResponse from "@/components/ApiResponse";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
const entityNames = Object.keys(krowSDK.entities).sort();
const getPrettifiedJSON = (entity, method) => {
// Basic placeholder payloads. A more advanced SDK could provide detailed examples.
const payloads = {
get: { id: "some-id" },
create: { data: { property: "value" } },
update: { id: "some-id", data: { property: "new-value" } },
delete: { id: "some-id" },
filter: { where: { property: { _eq: "value" } } },
list: {}
};
return JSON.stringify(payloads[method] || {}, null, 2);
};
const EntityTester = () => {
const [selectedEntity, setSelectedEntity] = useState(null);
const [selectedMethod, setSelectedMethod] = useState(null);
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [jsonInput, setJsonInput] = useState("");
const [jsonError, setJsonError] = useState(null);
const availableMethods = useMemo(() => {
if (!selectedEntity) return [];
return Object.keys(krowSDK.entities[selectedEntity]);
}, [selectedEntity]);
const handleEntityChange = (entity) => {
setSelectedEntity(entity);
setSelectedMethod(null);
setJsonInput("");
setJsonError(null);
setResponse(null);
setError(null);
};
const handleMethodSelect = (method) => {
setSelectedMethod(method);
setJsonInput(getPrettifiedJSON(selectedEntity, method));
setJsonError(null);
setResponse(null);
setError(null);
};
const handleJsonInputChange = (e) => {
setJsonInput(e.target.value);
try {
JSON.parse(e.target.value);
setJsonError(null);
} catch (err) {
setJsonError("Invalid JSON format");
}
};
const executeApi = async () => {
if (!selectedEntity || !selectedMethod || jsonError) return;
setLoading(true);
setResponse(null);
setError(null);
try {
const params = JSON.parse(jsonInput);
const sdkMethod = krowSDK.entities[selectedEntity][selectedMethod];
const res = await sdkMethod(params);
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
const renderMethodForm = () => {
if (!selectedMethod) {
return (
<div className="mt-4 p-4 text-center text-slate-500 bg-slate-50 rounded-lg">
<p>Select a method to begin.</p>
</div>
);
}
return (
<div className="space-y-4 mt-6">
<h3 className="text-lg font-semibold text-slate-800">
Parameters for <code className="bg-slate-100 p-1 rounded text-sm">/{selectedEntity}/{selectedMethod}</code>
</h3>
{/*
This is a textarea for JSON input. A more advanced implementation could
dynamically generate a form based on the expected parameters of each
SDK method, but that requires metadata about each method's signature
which is not currently available in the mock client.
*/}
<div className="space-y-2">
<Label htmlFor="params">JSON Payload</Label>
<Textarea
id="params"
name="params"
value={jsonInput}
onChange={handleJsonInputChange}
rows={8}
className={`font-mono text-sm ${jsonError ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
/>
{jsonError && <p className="text-xs text-red-600">{jsonError}</p>}
</div>
<Button onClick={executeApi} disabled={loading || !!jsonError}>
{loading ? "Executing..." : "Execute"}
</Button>
</div>
);
};
return (
<div>
<header className="mb-8">
<h1 className="text-3xl font-bold text-slate-900">Entity API Tester</h1>
<p className="text-slate-600 mt-1">
Select an entity and method, provide the required parameters in JSON format, and execute the API call.
</p>
</header>
<Card className="shadow-sm">
<CardHeader>
<CardTitle>API Configuration</CardTitle>
<CardDescription>Choose a Base44 entity and method to interact with.</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-start">
<div className="col-span-1">
<Label>Entity</Label>
<Select onValueChange={handleEntityChange} value={selectedEntity || ""}>
<SelectTrigger>
<SelectValue placeholder="Select an entity" />
</SelectTrigger>
<SelectContent>
{entityNames.map(entity => (
<SelectItem key={entity} value={entity}>{entity}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedEntity && (
<div className="col-span-2">
<Label>Method</Label>
<div className="flex flex-wrap items-center gap-2 pt-2">
{availableMethods.map(method => (
<Button
key={method}
variant={selectedMethod === method ? "default" : "outline"}
size="sm"
onClick={() => handleMethodSelect(method)}
>
{method}
</Button>
))}
</div>
</div>
)}
</div>
{renderMethodForm()}
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
export default EntityTester;

View File

@@ -0,0 +1,28 @@
import ServiceTester from "@/components/ServiceTester";
const GenerateImage = () => {
const fields = [
{
name: "prompt",
label: "Prompt",
type: "textarea",
placeholder: "Enter a prompt for the image",
},
{
name: "file",
label: "File",
type: "file",
},
];
return (
<ServiceTester
serviceName="Generate Image"
serviceDescription="Test the Generate Image service"
endpoint="/generate-image"
fields={fields}
/>
);
};
export default GenerateImage;

View File

@@ -0,0 +1,31 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
const Home = () => {
return (
<div className="max-w-4xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-slate-900">Welcome to KROW API Test Harness</h1>
<p className="text-slate-600 mt-1">
Your dedicated tool for rapid and authenticated testing of KROW backend services.
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Get Started</CardTitle>
<CardDescription>
Use the sidebar navigation to select an API category and then choose a specific endpoint to test.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-slate-700">
This tool automatically handles Firebase authentication, injecting the necessary ID tokens into your API requests.
Simply log in, select an API, provide the required parameters, and execute to see the raw JSON response.
</p>
</CardContent>
</Card>
</div>
);
};
export default Home;

View File

@@ -0,0 +1,37 @@
import { GoogleAuthProvider, signInWithPopup, setPersistence, browserLocalPersistence } from "firebase/auth";
import { useAuthState } from "react-firebase-hooks/auth";
import { Navigate } from "react-router-dom";
import { auth } from "../firebase";
import { Button } from "@/components/ui/button";
const Login = () => {
const [user, loading] = useAuthState(auth);
const handleGoogleLogin = async () => {
const provider = new GoogleAuthProvider();
try {
await setPersistence(auth, browserLocalPersistence);
await signInWithPopup(auth, provider);
} catch (error) {
console.error("Error signing in with Google", error);
}
};
if (loading) {
return <div>Loading...</div>;
}
// If user is logged in, redirect to the home page
if (user) {
return <Navigate to="/" />;
}
// If no user, show the login button
return (
<div className="flex items-center justify-center h-screen">
<Button onClick={handleGoogleLogin}>Sign in with Google</Button>
</div>
);
};
export default Login;

View File

@@ -0,0 +1,49 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import ApiResponse from "@/components/ApiResponse";
import { krowSDK } from "@/api/krowSDK";
const GetMe = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const handleGetMe = async () => {
setLoading(true);
setError(null);
setResponse(null);
try {
const res = await krowSDK.auth.me();
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">Get Me</h1>
<p className="text-slate-600 mb-6">Fetches the currently authenticated user's profile.</p>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Test `/auth/me`</CardTitle>
</CardHeader>
<CardContent>
<Button onClick={handleGetMe} disabled={loading}>
{loading ? "Loading..." : "Execute"}
</Button>
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
// We need to re-import Card components because they are not globally available.
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
export default GetMe;

View File

@@ -0,0 +1,74 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import ApiResponse from "@/components/ApiResponse";
import { krowSDK } from "@/api/krowSDK";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
const CreateSignedUrl = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
file_uri: "gs://your-bucket/private-file.pdf",
expires_in: 3600,
});
const handleChange = (e) => {
const { id, value } = e.target;
setFormData(prev => ({ ...prev, [id]: value }));
};
const handleCreateUrl = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
setResponse(null);
try {
const params = {
...formData,
expires_in: parseInt(formData.expires_in, 10),
};
const res = await krowSDK.integrations.Core.CreateFileSignedUrl(params);
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">Create Signed URL</h1>
<p className="text-slate-600 mb-6">Tests the `integrations.Core.CreateFileSignedUrl` endpoint.</p>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Test `/createSignedUrl`</CardTitle>
<CardDescription>Creates a temporary access URL for a private file stored in GCS.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleCreateUrl} className="space-y-4">
<div>
<Label htmlFor="file_uri">File URI</Label>
<Input id="file_uri" value={formData.file_uri} onChange={handleChange} />
</div>
<div>
<Label htmlFor="expires_in">Expires In (seconds)</Label>
<Input id="expires_in" type="number" value={formData.expires_in} onChange={handleChange} />
</div>
<Button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create Signed URL"}
</Button>
</form>
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
export default CreateSignedUrl;

View File

@@ -0,0 +1,81 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import ApiResponse from "@/components/ApiResponse";
import { krowSDK } from "@/api/krowSDK";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
const InvokeLLM = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
prompt: "Extract the total amount from the attached invoice.",
response_json_schema: JSON.stringify({ type: "object", properties: { total_amount: { type: "number" } } }, null, 2),
file_urls: "https://example.com/invoice.pdf",
});
const handleChange = (e) => {
const { id, value } = e.target;
setFormData(prev => ({ ...prev, [id]: value }));
};
const handleInvokeLLM = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
setResponse(null);
try {
const params = {
...formData,
response_json_schema: JSON.parse(formData.response_json_schema),
file_urls: formData.file_urls.split(',').map(url => url.trim()),
};
const res = await krowSDK.integrations.Core.InvokeLLM(params);
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">Invoke LLM</h1>
<p className="text-slate-600 mb-6">Tests the `integrations.Core.InvokeLLM` endpoint.</p>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Test `/invokeLLM`</CardTitle>
<CardDescription>Calls a large language model (e.g., Vertex AI).</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleInvokeLLM} className="space-y-4">
<div>
<Label htmlFor="prompt">Prompt</Label>
<Textarea id="prompt" value={formData.prompt} onChange={handleChange} rows={4} />
</div>
<div>
<Label htmlFor="response_json_schema">Response JSON Schema</Label>
<Textarea id="response_json_schema" value={formData.response_json_schema} onChange={handleChange} rows={6} className="font-mono" />
</div>
<div>
<Label htmlFor="file_urls">File URLs (comma-separated)</Label>
<Input id="file_urls" value={formData.file_urls} onChange={handleChange} />
</div>
<Button type="submit" disabled={loading}>
{loading ? "Invoking..." : "Invoke LLM"}
</Button>
</form>
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
export default InvokeLLM;

View File

@@ -0,0 +1,75 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import ApiResponse from "@/components/ApiResponse";
import { krowSDK } from "@/api/krowSDK";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
const SendEmail = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
to: "test@example.com",
subject: "Test Email from Harness",
body: "This is a test email.",
});
const handleChange = (e) => {
const { id, value } = e.target;
setFormData(prev => ({ ...prev, [id]: value }));
};
const handleSendEmail = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
setResponse(null);
try {
const res = await krowSDK.integrations.Core.SendEmail(formData);
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">Send Email</h1>
<p className="text-slate-600 mb-6">Tests the `integrations.Core.SendEmail` endpoint.</p>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Test `/sendEmail`</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSendEmail} className="space-y-4">
<div>
<Label htmlFor="to">To</Label>
<Input id="to" type="email" value={formData.to} onChange={handleChange} />
</div>
<div>
<Label htmlFor="subject">Subject</Label>
<Input id="subject" value={formData.subject} onChange={handleChange} />
</div>
<div>
<Label htmlFor="body">Body</Label>
<Textarea id="body" value={formData.body} onChange={handleChange} rows={5} />
</div>
<Button type="submit" disabled={loading}>
{loading ? "Sending..." : "Send Email"}
</Button>
</form>
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
export default SendEmail;

View File

@@ -0,0 +1,67 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import ApiResponse from "@/components/ApiResponse";
import { krowSDK } from "@/api/krowSDK";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
const UploadFile = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [file, setFile] = useState(null);
const handleFileChange = (e) => {
setFile(e.target.files[0]);
};
const handleUpload = async (e) => {
e.preventDefault();
if (!file) {
setError({ message: "Please select a file to upload." });
return;
}
setLoading(true);
setError(null);
setResponse(null);
try {
const res = await krowSDK.integrations.Core.UploadFile({ file });
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">Upload Public File</h1>
<p className="text-slate-600 mb-6">Tests the `integrations.Core.UploadFile` endpoint for public files.</p>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Test `/uploadFile`</CardTitle>
<CardDescription>Handles multipart/form-data upload to GCS and returns a public URL.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleUpload} className="space-y-4">
<div>
<Label htmlFor="file">File</Label>
<Input id="file" type="file" onChange={handleFileChange} />
</div>
<Button type="submit" disabled={loading || !file}>
{loading ? "Uploading..." : "Upload File"}
</Button>
</form>
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
export default UploadFile;

View File

@@ -0,0 +1,67 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import ApiResponse from "@/components/ApiResponse";
import { krowSDK } from "@/api/krowSDK";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
const UploadPrivateFile = () => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [file, setFile] = useState(null);
const handleFileChange = (e) => {
setFile(e.target.files[0]);
};
const handleUpload = async (e) => {
e.preventDefault();
if (!file) {
setError({ message: "Please select a file to upload." });
return;
}
setLoading(true);
setError(null);
setResponse(null);
try {
const res = await krowSDK.integrations.Core.UploadPrivateFile({ file });
setResponse(res);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">Upload Private File</h1>
<p className="text-slate-600 mb-6">Tests the `integrations.Core.UploadPrivateFile` endpoint.</p>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Test `/uploadPrivateFile`</CardTitle>
<CardDescription>Handles multipart/form-data upload to GCS and returns a secure gs:// URI.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleUpload} className="space-y-4">
<div>
<Label htmlFor="file">File</Label>
<Input id="file" type="file" onChange={handleFileChange} />
</div>
<Button type="submit" disabled={loading || !file}>
{loading ? "Uploading..." : "Upload File"}
</Button>
</form>
</CardContent>
</Card>
<ApiResponse response={response} error={error} loading={loading} />
</div>
);
};
export default UploadPrivateFile;

View File

@@ -0,0 +1,96 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
},
plugins: [require("tailwindcss-animate")],
}

View File

@@ -0,0 +1,17 @@
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
server: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin-allow-popups",
},
},
})