refactor: replace raw HTML elements with design system components
- Replace <select>, <input>, <label>, <textarea> with UI components - Update various dashboard and docs pages - Simplify unsandbox package.json - Add DESIGN_SYSTEM.md documentation - Add Claude skills configuration
This commit is contained in:
parent
7d9321d9c6
commit
23d5159b28
26 changed files with 1280 additions and 335 deletions
102
.claude/commands/blocks-develop.md
Normal file
102
.claude/commands/blocks-develop.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
---
|
||||
description: Develop and validate TPMJS tools using the blocks CLI
|
||||
---
|
||||
|
||||
Help the user develop new tools for the TPMJS registry using the blocks CLI. This workflow covers defining tools in blocks.yml, implementing them with AI SDK v6, validating with the blocks CLI, and publishing to npm.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Define Tool in blocks.yml
|
||||
|
||||
Add tool definition to `packages/tools/official/blocks.yml`:
|
||||
|
||||
```yaml
|
||||
blocks:
|
||||
category.toolName:
|
||||
type: utility
|
||||
description: "Clear description for LLMs"
|
||||
path: "tool-directory-name"
|
||||
domain_rules:
|
||||
- id: rule_name
|
||||
description: "Implementation requirement"
|
||||
inputs:
|
||||
- name: paramName
|
||||
type: string
|
||||
description: "Parameter description"
|
||||
outputs:
|
||||
- name: result
|
||||
type: ResultType
|
||||
description: "Output description"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
```
|
||||
|
||||
### 2. Create Package Structure
|
||||
|
||||
```
|
||||
packages/tools/official/tool-name/
|
||||
├── package.json # npm package with tpmjs field
|
||||
├── tsconfig.json # Extends @tpmjs/tsconfig
|
||||
├── tsup.config.ts # Build config
|
||||
├── block.ts # REQUIRED by validator
|
||||
├── index.ts # Re-export from src
|
||||
└── src/index.ts # Main implementation
|
||||
```
|
||||
|
||||
### 3. Implement with AI SDK v6
|
||||
|
||||
```typescript
|
||||
import { jsonSchema, tool } from 'ai';
|
||||
|
||||
export const myTool = tool({
|
||||
description: 'Description for LLMs',
|
||||
parameters: jsonSchema<InputType>({
|
||||
type: 'object',
|
||||
properties: { /* ... */ },
|
||||
required: ['field1'],
|
||||
}),
|
||||
async execute(input): Promise<OutputType> {
|
||||
// REAL implementation - no stubs
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
export default myTool;
|
||||
```
|
||||
|
||||
### 4. Run Validation
|
||||
|
||||
```bash
|
||||
cd packages/tools/official
|
||||
pnpm blocks run tool-name # Validate single tool
|
||||
pnpm blocks run tool-name --force # Force full validation
|
||||
pnpm blocks run --all # Validate all tools
|
||||
```
|
||||
|
||||
### 5. Build and Publish
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
npm publish --access public
|
||||
|
||||
# Trigger sync to tpmjs.com
|
||||
source apps/web/.env.local
|
||||
curl -X POST https://tpmjs.com/api/sync/keyword -H "Authorization: Bearer $CRON_SECRET"
|
||||
```
|
||||
|
||||
## Valid Categories
|
||||
|
||||
For `tpmjs.category` in package.json: `research`, `web`, `data`, `documentation`, `engineering`, `security`, `statistics`, `ops`, `agent`, `sandbox`, `utilities`, `html`, `compliance`
|
||||
|
||||
## Required Files
|
||||
|
||||
- **block.ts** at root: `export const block = { name: 'tool-name', tools: { myTool } };`
|
||||
- **index.ts** at root: `export * from './src/index.js';`
|
||||
- Both are required for the validator to find the tool
|
||||
|
||||
## Common Issues
|
||||
|
||||
- "invalid tpmjs field" during sync = Invalid category or missing tools array
|
||||
- "Tool not found in exports" = Export name must match blocks.yml
|
||||
- "Required file not found" = Need index.ts and block.ts at package root
|
||||
|
||||
When helping the user, read the full skill documentation at `.claude/skills/blocks-develop.md` for comprehensive details on entities, measures, and multi-tool packages.
|
||||
357
.claude/skills/blocks-develop.md
Normal file
357
.claude/skills/blocks-develop.md
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
# TPMJS Tool Development with Blocks CLI
|
||||
|
||||
Use this skill when developing new tools for the TPMJS registry. This covers the full workflow from defining a tool in blocks.yml through implementation, validation, and publishing.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Navigate to official tools directory
|
||||
cd packages/tools/official
|
||||
|
||||
# Run validation on a specific tool
|
||||
pnpm blocks run <block-name>
|
||||
|
||||
# Run validation on all tools
|
||||
pnpm blocks run --all
|
||||
|
||||
# Force full validation (ignore cache)
|
||||
pnpm blocks run <block-name> --force
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Define the Tool Block in blocks.yml
|
||||
|
||||
Add your tool definition to `packages/tools/official/blocks.yml` in the `blocks:` section:
|
||||
|
||||
```yaml
|
||||
blocks:
|
||||
# Category.toolName format
|
||||
sandbox.myTool:
|
||||
type: utility
|
||||
description: "Clear, LLM-friendly description of what the tool does"
|
||||
path: "my-tool" # Directory name under packages/tools/official/
|
||||
domain_rules:
|
||||
- id: rule_name
|
||||
description: "What this implementation must do"
|
||||
inputs:
|
||||
- name: inputName
|
||||
type: string
|
||||
description: "Description for LLMs"
|
||||
- name: optionalInput
|
||||
type: number
|
||||
optional: true
|
||||
description: "Optional parameter"
|
||||
outputs:
|
||||
- name: result
|
||||
type: MyResultType
|
||||
description: "What the tool returns"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
```
|
||||
|
||||
**Key Fields:**
|
||||
- `type`: Usually `utility` for single-shot tools
|
||||
- `path`: Directory name (kebab-case)
|
||||
- `domain_rules`: Implementation requirements the validator checks
|
||||
- `inputs/outputs`: Schema for validation
|
||||
- `measures`: Quality constraints from the domain section
|
||||
|
||||
### 2. Create the Tool Package
|
||||
|
||||
Create the directory structure:
|
||||
|
||||
```
|
||||
packages/tools/official/my-tool/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── tsup.config.ts
|
||||
├── block.ts # Required by validator
|
||||
├── index.ts # Re-export from src
|
||||
└── src/
|
||||
└── index.ts # Main implementation
|
||||
```
|
||||
|
||||
**package.json:**
|
||||
```json
|
||||
{
|
||||
"name": "@tpmjs/tools-my-tool",
|
||||
"version": "0.1.0",
|
||||
"description": "Short description for npm",
|
||||
"type": "module",
|
||||
"keywords": ["tpmjs", "category-name", "ai"],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": ["dist"],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tpmjs/tsconfig": "workspace:*",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "6.0.23"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tpmjs/tpmjs.git",
|
||||
"directory": "packages/tools/official/my-tool"
|
||||
},
|
||||
"homepage": "https://tpmjs.com",
|
||||
"license": "MIT",
|
||||
"tpmjs": {
|
||||
"category": "sandbox",
|
||||
"frameworks": ["vercel-ai"],
|
||||
"tools": [
|
||||
{
|
||||
"name": "myTool",
|
||||
"description": "Clear description (20+ chars) of what this tool does."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Valid categories for tpmjs.category:**
|
||||
- `research`, `web`, `data`, `documentation`, `engineering`
|
||||
- `security`, `statistics`, `ops`, `agent`, `sandbox`, `utilities`
|
||||
- `html`, `compliance`
|
||||
|
||||
**tsconfig.json:**
|
||||
```json
|
||||
{
|
||||
"extends": "@tpmjs/tsconfig/react-library.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
**tsup.config.ts:**
|
||||
```typescript
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
target: 'es2022',
|
||||
});
|
||||
```
|
||||
|
||||
**block.ts (Required by validator):**
|
||||
```typescript
|
||||
import { myTool } from './src/index.js';
|
||||
|
||||
export const block = {
|
||||
name: 'my-tool',
|
||||
description: 'Short description',
|
||||
tools: { myTool },
|
||||
};
|
||||
|
||||
export default block;
|
||||
```
|
||||
|
||||
**index.ts (Root re-export):**
|
||||
```typescript
|
||||
export * from './src/index.js';
|
||||
export { default } from './src/index.js';
|
||||
```
|
||||
|
||||
### 3. Implement the Tool
|
||||
|
||||
**src/index.ts:**
|
||||
```typescript
|
||||
import { jsonSchema, tool } from 'ai';
|
||||
|
||||
// Define input/output types
|
||||
interface MyToolInput {
|
||||
param1: string;
|
||||
param2?: number;
|
||||
}
|
||||
|
||||
interface MyToolResult {
|
||||
data: string;
|
||||
metadata: {
|
||||
processedAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Export the tool using AI SDK v6 pattern
|
||||
export const myTool = tool({
|
||||
description: 'Clear description for LLMs explaining what this tool does and when to use it.',
|
||||
parameters: jsonSchema<MyToolInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: {
|
||||
type: 'string',
|
||||
description: 'Description of param1',
|
||||
},
|
||||
param2: {
|
||||
type: 'number',
|
||||
description: 'Optional description of param2',
|
||||
},
|
||||
},
|
||||
required: ['param1'],
|
||||
}),
|
||||
async execute(input): Promise<MyToolResult> {
|
||||
// REAL implementation - no stubs, no TODOs
|
||||
const result = await doSomething(input.param1);
|
||||
|
||||
return {
|
||||
data: result,
|
||||
metadata: {
|
||||
processedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Default export for compatibility
|
||||
export default myTool;
|
||||
```
|
||||
|
||||
### 4. Run Validation
|
||||
|
||||
```bash
|
||||
cd packages/tools/official
|
||||
|
||||
# Validate your tool
|
||||
pnpm blocks run my-tool
|
||||
|
||||
# The validator runs 3 stages:
|
||||
# 1. schema - Validates inputs/outputs match blocks.yml
|
||||
# 2. shape - Verifies exports and structure
|
||||
# 3. domain - Checks domain rules are satisfied
|
||||
```
|
||||
|
||||
**Common validation errors:**
|
||||
- `Required file "index.ts" not found` - Need index.ts at package root
|
||||
- `Required file "block.ts" not found` - Need block.ts at package root
|
||||
- `Tool "myTool" not found in exports` - Export name must match blocks.yml
|
||||
- `invalid tpmjs field` - Category must be valid, tools array required
|
||||
|
||||
### 5. Build and Publish
|
||||
|
||||
```bash
|
||||
# Build the package
|
||||
pnpm build
|
||||
|
||||
# Publish to npm
|
||||
npm publish --access public
|
||||
|
||||
# Trigger sync to tpmjs.com
|
||||
source apps/web/.env.local
|
||||
curl -X POST https://tpmjs.com/api/sync/keyword \
|
||||
-H "Authorization: Bearer $CRON_SECRET"
|
||||
```
|
||||
|
||||
## Multi-Tool Packages
|
||||
|
||||
For packages with multiple tools (like unsandbox):
|
||||
|
||||
**blocks.yml:**
|
||||
```yaml
|
||||
blocks:
|
||||
sandbox.executeCodeAsync:
|
||||
type: utility
|
||||
path: "unsandbox" # Same path for all tools in package
|
||||
# ...
|
||||
|
||||
sandbox.getJob:
|
||||
type: utility
|
||||
path: "unsandbox" # Same path
|
||||
# ...
|
||||
```
|
||||
|
||||
**block.ts:**
|
||||
```typescript
|
||||
import { executeCodeAsync, getJob, listJobs } from './src/index.js';
|
||||
|
||||
export const block = {
|
||||
name: 'unsandbox',
|
||||
tools: { executeCodeAsync, getJob, listJobs },
|
||||
};
|
||||
|
||||
export default block;
|
||||
```
|
||||
|
||||
**package.json tpmjs field:**
|
||||
```json
|
||||
{
|
||||
"tpmjs": {
|
||||
"category": "sandbox",
|
||||
"frameworks": ["vercel-ai"],
|
||||
"tools": [
|
||||
{ "name": "executeCodeAsync", "description": "..." },
|
||||
{ "name": "getJob", "description": "..." },
|
||||
{ "name": "listJobs", "description": "..." }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Philosophy (from blocks.yml)
|
||||
|
||||
- Every tool MUST be a working, production-ready implementation - no stubs, no TODOs
|
||||
- Tools use AI SDK v6 `tool()` + `jsonSchema()` pattern exclusively
|
||||
- Each tool does ONE thing exceptionally well (single-shot, one call in, one result out)
|
||||
- Tools return structured, typed outputs that agents can reliably parse
|
||||
- Error handling is explicit - throw meaningful errors, never silently fail
|
||||
- Dependencies are minimal and production-stable
|
||||
|
||||
## Domain Entities
|
||||
|
||||
When defining outputs, reference existing entities from blocks.yml:
|
||||
|
||||
```yaml
|
||||
# Example entities available:
|
||||
url: [href, domain, protocol, path, query, fragment]
|
||||
webpage: [url, title, html, text, metadata]
|
||||
text_content: [raw, sentences, paragraphs, wordCount]
|
||||
claim: [statement, confidence, needsCitation, category]
|
||||
timeline: [events, dateRange, gaps, eventCount]
|
||||
```
|
||||
|
||||
Or define new entities in the `domain.entities` section if needed.
|
||||
|
||||
## Quality Measures
|
||||
|
||||
Reference these in your tool's `measures` array:
|
||||
|
||||
- `working_implementation` - No stubs, TODOs, or placeholders
|
||||
- `valid_output_structure` - Returns correct typed object
|
||||
- `proper_error_handling` - Throws descriptive errors
|
||||
- `ai_sdk_compliance` - Uses tool() and jsonSchema()
|
||||
- `npm_publishable` - Valid package.json with tpmjs field
|
||||
- `readme_documentation` - Has README with examples
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```bash
|
||||
# Force rebuild without cache
|
||||
pnpm blocks run my-tool --force --no-cache
|
||||
|
||||
# See JSON output for debugging
|
||||
pnpm blocks run my-tool --json
|
||||
|
||||
# Check if validator finds your package
|
||||
ls packages/tools/official/my-tool/
|
||||
# Must have: index.ts, block.ts at root level
|
||||
```
|
||||
30
CLAUDE.md
30
CLAUDE.md
|
|
@ -26,7 +26,37 @@ This project uses a Turborepo monorepo architecture with the following structure
|
|||
|
||||
### Architecture Principles
|
||||
|
||||
#### 1. Design System First
|
||||
|
||||
**Always use `@tpmjs/ui` components instead of raw HTML elements.** This ensures visual consistency, accessibility, and maintainability across the application.
|
||||
|
||||
```typescript
|
||||
// Good - use design system components
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Input } from '@tpmjs/ui/Input/Input';
|
||||
import { Table, TableRow, TableCell } from '@tpmjs/ui/Table/Table';
|
||||
|
||||
<Button onClick={handleClick}>Submit</Button>
|
||||
<Input value={value} onChange={onChange} />
|
||||
|
||||
// Bad - raw HTML elements
|
||||
<button onClick={handleClick}>Submit</button>
|
||||
<input value={value} onChange={onChange} />
|
||||
```
|
||||
|
||||
**When to create/update UI components:**
|
||||
- If a pattern is used in 2+ places, create a reusable component in `@tpmjs/ui`
|
||||
- If existing component styling doesn't match the design, update the component (not the usage site)
|
||||
- If you need virtualization (e.g., `react-virtuoso`), match the design system's styling classes
|
||||
|
||||
**Common components to use:**
|
||||
- `Button` - all clickable actions
|
||||
- `Input`, `Select`, `Textarea` - form inputs
|
||||
- `Table`, `TableRow`, `TableCell` - data tables
|
||||
- `Card` - content containers
|
||||
- `Badge` - status indicators
|
||||
- `Icon` - all icons (not inline SVGs)
|
||||
- `Spinner`, `Skeleton` - loading states
|
||||
|
||||
#### 2. No Barrel Exports
|
||||
|
||||
|
|
|
|||
559
DESIGN_SYSTEM.md
Normal file
559
DESIGN_SYSTEM.md
Normal file
|
|
@ -0,0 +1,559 @@
|
|||
# TPMJS Design System Specification
|
||||
|
||||
> A technical, precise design system inspired by [turbopuffer.com](https://turbopuffer.com) - warm, monospace-driven, with generous whitespace and fieldset-style containers.
|
||||
|
||||
---
|
||||
|
||||
## Brand Direction
|
||||
|
||||
### Mood & Personality
|
||||
- **Technical & Precise** - Engineering-focused, trustworthy, developer-first
|
||||
- **Warm & Distinctive** - Not cold/corporate, the copper accent adds warmth
|
||||
- **Confident & Minimal** - Let the content speak, reduce visual noise
|
||||
|
||||
### Reference Sites
|
||||
- [turbopuffer.com](https://turbopuffer.com) - Primary inspiration
|
||||
- Linear, Vercel - Secondary references for technical clarity
|
||||
|
||||
---
|
||||
|
||||
## Color Palette
|
||||
|
||||
### Primary Accent
|
||||
```css
|
||||
--color-accent: #A6592D; /* Copper/terracotta - primary brand color */
|
||||
--color-accent-hover: #8B4A26; /* Darker copper for hover states */
|
||||
--color-accent-light: #D4A574; /* Light copper for backgrounds/highlights */
|
||||
```
|
||||
|
||||
### Gradient Header
|
||||
```css
|
||||
/* Warm gradient for top bar/hero sections */
|
||||
--gradient-header: linear-gradient(135deg, #D4732A 0%, #8B3D1A 50%, #2D1810 100%);
|
||||
```
|
||||
|
||||
### Neutral Palette
|
||||
```css
|
||||
/* Backgrounds */
|
||||
--color-bg-primary: #FFFFFF; /* Main background */
|
||||
--color-bg-secondary: #FAFAFA; /* Subtle sections */
|
||||
--color-bg-elevated: #FFFFFF; /* Cards, elevated surfaces */
|
||||
|
||||
/* Text */
|
||||
--color-text-primary: #1A1A1A; /* Primary text - near black */
|
||||
--color-text-secondary: #666666; /* Secondary/muted text */
|
||||
--color-text-tertiary: #999999; /* Placeholder, hints */
|
||||
|
||||
/* Borders */
|
||||
--color-border: #E5E5E5; /* Default borders */
|
||||
--color-border-strong: #CCCCCC; /* Emphasized borders */
|
||||
--color-border-focus: #A6592D; /* Focus state - uses accent */
|
||||
```
|
||||
|
||||
### Semantic Colors
|
||||
```css
|
||||
--color-success: #22C55E;
|
||||
--color-error: #EF4444;
|
||||
--color-warning: #F59E0B;
|
||||
--color-info: #3B82F6;
|
||||
```
|
||||
|
||||
### Dark Mode (Future)
|
||||
```css
|
||||
/* Dark mode should invert while keeping the warm accent */
|
||||
--color-bg-primary-dark: #0D0D0D;
|
||||
--color-bg-secondary-dark: #1A1A1A;
|
||||
--color-text-primary-dark: #F5F5F5;
|
||||
--color-border-dark: #333333;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Stack
|
||||
|
||||
**Headings & Code: Monospace**
|
||||
```css
|
||||
--font-mono: 'JetBrains Mono', 'IBM Plex Mono', 'Fira Code', monospace;
|
||||
```
|
||||
|
||||
**Body Text: Sans-serif (for longer reading)**
|
||||
```css
|
||||
--font-sans: 'Inter', 'IBM Plex Sans', system-ui, sans-serif;
|
||||
```
|
||||
|
||||
### Type Scale
|
||||
|
||||
| Element | Font | Size | Weight | Line Height | Letter Spacing |
|
||||
|---------|------|------|--------|-------------|----------------|
|
||||
| H1 | Mono | 48px (3rem) | 600 | 1.1 | -0.02em |
|
||||
| H2 | Mono | 36px (2.25rem) | 600 | 1.2 | -0.01em |
|
||||
| H3 | Mono | 24px (1.5rem) | 600 | 1.3 | 0 |
|
||||
| H4 | Mono | 20px (1.25rem) | 600 | 1.4 | 0 |
|
||||
| Body Large | Sans | 18px (1.125rem) | 400 | 1.7 | 0 |
|
||||
| Body | Sans | 16px (1rem) | 400 | 1.7 | 0 |
|
||||
| Body Small | Sans | 14px (0.875rem) | 400 | 1.6 | 0 |
|
||||
| Caption | Sans | 12px (0.75rem) | 400 | 1.5 | 0.01em |
|
||||
| Code | Mono | 14px (0.875rem) | 400 | 1.6 | 0 |
|
||||
|
||||
### Typography Rules
|
||||
1. **Headings are lowercase** - "pricing", "faq", "tools" (not "Pricing", "FAQ", "Tools")
|
||||
2. **Generous line-height** - Minimum 1.6 for body text, 1.7 preferred
|
||||
3. **Bold sparingly** - Use weight 600 for emphasis, not 700+
|
||||
4. **Monospace for data** - Numbers, metrics, technical values always in mono
|
||||
|
||||
### CSS Variables
|
||||
```css
|
||||
/* Font families */
|
||||
--font-heading: var(--font-mono);
|
||||
--font-body: var(--font-sans);
|
||||
--font-code: var(--font-mono);
|
||||
|
||||
/* Font sizes */
|
||||
--text-xs: 0.75rem; /* 12px */
|
||||
--text-sm: 0.875rem; /* 14px */
|
||||
--text-base: 1rem; /* 16px */
|
||||
--text-lg: 1.125rem; /* 18px */
|
||||
--text-xl: 1.25rem; /* 20px */
|
||||
--text-2xl: 1.5rem; /* 24px */
|
||||
--text-3xl: 2.25rem; /* 36px */
|
||||
--text-4xl: 3rem; /* 48px */
|
||||
|
||||
/* Line heights */
|
||||
--leading-tight: 1.2;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.7;
|
||||
|
||||
/* Font weights */
|
||||
--font-normal: 400;
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spacing
|
||||
|
||||
### Spacing Scale
|
||||
```css
|
||||
--space-0: 0;
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-5: 1.25rem; /* 20px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
--space-10: 2.5rem; /* 40px */
|
||||
--space-12: 3rem; /* 48px */
|
||||
--space-16: 4rem; /* 64px */
|
||||
--space-20: 5rem; /* 80px */
|
||||
--space-24: 6rem; /* 96px */
|
||||
```
|
||||
|
||||
### Spacing Philosophy
|
||||
- **Generous whitespace** - When in doubt, add more space
|
||||
- **Vertical rhythm** - Use consistent spacing between sections (typically `--space-16` to `--space-24`)
|
||||
- **Component padding** - Cards and containers use `--space-6` to `--space-8`
|
||||
- **Text spacing** - Paragraphs separated by `--space-4` to `--space-6`
|
||||
|
||||
---
|
||||
|
||||
## Borders & Containers
|
||||
|
||||
### Border Radius
|
||||
```css
|
||||
--radius-none: 0; /* DEFAULT - sharp corners */
|
||||
--radius-sm: 2px; /* Use sparingly for special cases */
|
||||
--radius-md: 4px; /* Use sparingly for special cases */
|
||||
```
|
||||
|
||||
**Rule: Default to 0 border-radius. Sharp corners are the brand.**
|
||||
|
||||
### Border Styles
|
||||
|
||||
**Dashed (Primary)**
|
||||
```css
|
||||
border: 1px dashed var(--color-border);
|
||||
```
|
||||
|
||||
**Solid (Emphasis)**
|
||||
```css
|
||||
border: 2px solid var(--color-text-primary); /* Featured items */
|
||||
```
|
||||
|
||||
### Fieldset-Style Containers
|
||||
|
||||
The signature container style with a label that "cuts into" the border:
|
||||
|
||||
```html
|
||||
<fieldset class="fieldset-container">
|
||||
<legend>section title</legend>
|
||||
<!-- content -->
|
||||
</fieldset>
|
||||
```
|
||||
|
||||
```css
|
||||
.fieldset-container {
|
||||
border: 1px dashed var(--color-border);
|
||||
padding: var(--space-6);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fieldset-container legend {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
padding: 0 var(--space-2);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
```
|
||||
|
||||
### Container Variants
|
||||
|
||||
| Variant | Border | Background | Use Case |
|
||||
|---------|--------|------------|----------|
|
||||
| Default | 1px dashed | transparent | Most containers |
|
||||
| Elevated | 1px dashed | white | Cards on gray bg |
|
||||
| Featured | 2px solid | white | Highlighted item |
|
||||
| Ghost | none | transparent | Minimal grouping |
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### Buttons
|
||||
|
||||
**Primary Button (Accent)**
|
||||
```css
|
||||
.btn-primary {
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
```
|
||||
|
||||
**Secondary Button (Outline)**
|
||||
```css
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
cursor: pointer;
|
||||
transition: border-color 150ms ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--color-text-primary);
|
||||
}
|
||||
```
|
||||
|
||||
**Button Sizes**
|
||||
| Size | Padding | Font Size |
|
||||
|------|---------|-----------|
|
||||
| sm | `--space-2` `--space-4` | `--text-xs` |
|
||||
| md | `--space-3` `--space-6` | `--text-sm` |
|
||||
| lg | `--space-4` `--space-8` | `--text-base` |
|
||||
|
||||
### Links
|
||||
|
||||
```css
|
||||
a {
|
||||
color: var(--color-text-primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
```
|
||||
|
||||
**Rule: Links are underlined, not colored.** Use underline as the primary affordance.
|
||||
|
||||
### Inputs
|
||||
|
||||
```css
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-none);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-base);
|
||||
background: white;
|
||||
transition: border-color 150ms ease;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
```
|
||||
|
||||
### Cards
|
||||
|
||||
```css
|
||||
.card {
|
||||
border: 1px dashed var(--color-border);
|
||||
padding: var(--space-6);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.card--featured {
|
||||
border: 2px solid var(--color-text-primary);
|
||||
}
|
||||
|
||||
.card__title {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
text-transform: lowercase;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.card__description {
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-base);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: var(--leading-relaxed);
|
||||
}
|
||||
```
|
||||
|
||||
### Badges
|
||||
|
||||
```css
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
border: 1px solid currentColor;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.badge--default { color: var(--color-text-secondary); }
|
||||
.badge--success { color: var(--color-success); }
|
||||
.badge--error { color: var(--color-error); }
|
||||
.badge--warning { color: var(--color-warning); }
|
||||
```
|
||||
|
||||
### Tables
|
||||
|
||||
```css
|
||||
.table-container {
|
||||
border: 1px dashed var(--color-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.table th {
|
||||
text-align: left;
|
||||
padding: var(--space-4);
|
||||
border-bottom: 1px dashed var(--color-border);
|
||||
font-weight: var(--font-semibold);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: var(--space-4);
|
||||
border-bottom: 1px dashed var(--color-border);
|
||||
}
|
||||
|
||||
.table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout
|
||||
|
||||
### Container Widths
|
||||
```css
|
||||
--container-sm: 640px;
|
||||
--container-md: 768px;
|
||||
--container-lg: 1024px;
|
||||
--container-xl: 1280px;
|
||||
```
|
||||
|
||||
### Page Structure
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Gradient Header Bar (announcement) │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Navigation (sticky, white bg) │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Hero Section │
|
||||
│ (generous padding: --space-24) │
|
||||
│ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Content Sections │
|
||||
│ (separated by --space-16 to --space-24) │
|
||||
│ │
|
||||
│ ┌─ fieldset container ─────────────────┐ │
|
||||
│ │ section title │ │
|
||||
│ │ │ │
|
||||
│ │ Content with generous padding │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interactions
|
||||
|
||||
### Hover States
|
||||
- **Buttons**: Background color change (accent → darker)
|
||||
- **Links**: Opacity reduction to 0.7
|
||||
- **Cards**: Border color change (border → border-strong)
|
||||
- **No transforms** - Avoid scale/translate on hover (too playful)
|
||||
|
||||
### Focus States
|
||||
```css
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
### Transitions
|
||||
```css
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 200ms ease;
|
||||
--transition-slow: 300ms ease;
|
||||
```
|
||||
|
||||
**Rule: Keep transitions subtle and fast. No bouncy/spring animations.**
|
||||
|
||||
---
|
||||
|
||||
## Special Elements
|
||||
|
||||
### Gradient Header Bar
|
||||
```css
|
||||
.header-bar {
|
||||
background: var(--gradient-header);
|
||||
color: white;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
### Technical Diagrams
|
||||
Use ASCII-style box diagrams with monospace font:
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ client │─────▶│ API │
|
||||
└─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
### Code Blocks
|
||||
```css
|
||||
.code-block {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px dashed var(--color-border);
|
||||
padding: var(--space-4);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
overflow-x: auto;
|
||||
}
|
||||
```
|
||||
|
||||
### Sliders/Range Inputs
|
||||
Custom styled with accent color, monospace tooltips showing values.
|
||||
|
||||
---
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### Do
|
||||
- Use lowercase for headings
|
||||
- Use dashed borders for containers
|
||||
- Use generous whitespace
|
||||
- Use monospace for technical content
|
||||
- Use underlines for links
|
||||
- Keep interactions subtle and fast
|
||||
- Use the copper accent sparingly but confidently
|
||||
|
||||
### Don't
|
||||
- Don't use rounded corners (except for special cases)
|
||||
- Don't use drop shadows
|
||||
- Don't use gradients (except header bar)
|
||||
- Don't use icons where text works
|
||||
- Don't use colored links
|
||||
- Don't use bouncy animations
|
||||
- Don't use multiple accent colors
|
||||
|
||||
---
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
### Phase 1: Foundation
|
||||
1. Update CSS variables (colors, spacing, typography)
|
||||
2. Install fonts (JetBrains Mono, Inter)
|
||||
3. Update base styles (reset, typography)
|
||||
|
||||
### Phase 2: Core Components
|
||||
1. Button variants
|
||||
2. Input/Form elements
|
||||
3. Card/Container styles
|
||||
4. Badge variants
|
||||
|
||||
### Phase 3: Layout
|
||||
1. Fieldset-style containers
|
||||
2. Page layouts with generous spacing
|
||||
3. Navigation updates
|
||||
4. Gradient header bar
|
||||
|
||||
### Phase 4: Polish
|
||||
1. Table styles
|
||||
2. Code blocks
|
||||
3. Interactive elements (sliders, toggles)
|
||||
4. Transitions and hover states
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **Turbopuffer**: https://turbopuffer.com - Primary design inspiration
|
||||
- **JetBrains Mono**: https://www.jetbrains.com/lp/mono/
|
||||
- **Inter**: https://rsms.me/inter/
|
||||
|
||||
---
|
||||
|
||||
*Last updated: January 2025*
|
||||
*Version: 1.0*
|
||||
2
apps/web/next-env.d.ts
vendored
2
apps/web/next-env.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import { Input } from '@tpmjs/ui/Input/Input';
|
||||
import { Label } from '@tpmjs/ui/Label/Label';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { signIn } from '~/lib/auth-client';
|
||||
|
|
@ -77,9 +78,7 @@ export default function SignInPage() {
|
|||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-foreground mb-1">
|
||||
Email
|
||||
</label>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
|
|
@ -92,9 +91,7 @@ export default function SignInPage() {
|
|||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
||||
Password
|
||||
</label>
|
||||
<Label htmlFor="password" className="mb-0">Password</Label>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-sm text-foreground-secondary hover:text-foreground hover:underline"
|
||||
|
|
@ -112,14 +109,16 @@ export default function SignInPage() {
|
|||
placeholder="Enter your password"
|
||||
className="pr-10"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground-tertiary hover:text-foreground transition-colors"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0"
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
<Icon icon={showPassword ? 'eyeOff' : 'eye'} size="sm" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export default function PrettyAgentDetailPage(): React.ReactElement {
|
|||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-red-500">{error}</p>
|
||||
<p className="text-error">{error}</p>
|
||||
</div>
|
||||
) : agent ? (
|
||||
<div className="space-y-8">
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ export default function PrettyCollectionDetailPage(): React.ReactElement {
|
|||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-red-500">{error}</p>
|
||||
<p className="text-error">{error}</p>
|
||||
</div>
|
||||
) : collection ? (
|
||||
<div className="space-y-8">
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export default function UserProfilePage(): React.ReactElement {
|
|||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-red-500">{error}</p>
|
||||
<p className="text-error">{error}</p>
|
||||
</div>
|
||||
) : profile ? (
|
||||
<div className="space-y-8">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { AIProvider } from '@tpmjs/types/agent';
|
|||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
|
||||
import Link from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
|
@ -102,10 +103,10 @@ function ToolCallCard({
|
|||
onToggle: () => void;
|
||||
}) {
|
||||
const statusColors = {
|
||||
pending: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
|
||||
running: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
||||
success: 'bg-green-500/20 text-green-400 border-green-500/30',
|
||||
error: 'bg-red-500/20 text-red-400 border-red-500/30',
|
||||
pending: 'bg-warning/10 text-warning border-warning/30',
|
||||
running: 'bg-info/10 text-info border-info/30',
|
||||
success: 'bg-success/10 text-success border-success/30',
|
||||
error: 'bg-error/10 text-error border-error/30',
|
||||
};
|
||||
|
||||
const statusIcons: Record<ToolCall['status'], 'loader' | 'check' | 'alertCircle' | 'info'> = {
|
||||
|
|
@ -126,10 +127,10 @@ function ToolCallCard({
|
|||
return (
|
||||
<div className="rounded-lg border border-border bg-surface-secondary/50 overflow-hidden font-mono text-xs">
|
||||
{/* Header */}
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onToggle}
|
||||
className="w-full flex items-center gap-3 p-3 hover:bg-surface-secondary/80 transition-colors"
|
||||
className="w-full flex items-center gap-3 p-3 h-auto justify-start rounded-none hover:bg-surface-secondary/80"
|
||||
>
|
||||
<div className={`p-1.5 rounded ${statusColors[toolCall.status]}`}>
|
||||
<Icon
|
||||
|
|
@ -151,7 +152,7 @@ function ToolCallCard({
|
|||
size="xs"
|
||||
className={`text-foreground-tertiary transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
|
|
@ -180,7 +181,7 @@ function ToolCallCard({
|
|||
</span>
|
||||
<div className="flex-1 h-px bg-border/50" />
|
||||
</div>
|
||||
<pre className="text-[11px] text-green-400 overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto">
|
||||
<pre className="text-[11px] text-success overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto">
|
||||
{formatJson(toolCall.output)}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
@ -661,14 +662,14 @@ export default function PublicAgentChatPage(): React.ReactElement {
|
|||
<div className="border-b border-border bg-surface/50 px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="p-2 hover:bg-surface-secondary rounded-lg transition-colors"
|
||||
title={sidebarOpen ? 'Hide sidebar' : 'Show sidebar'}
|
||||
>
|
||||
<Icon icon="menu" size="sm" />
|
||||
</button>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-foreground">{agent.name}</h1>
|
||||
<p className="text-sm text-foreground-tertiary">
|
||||
|
|
@ -683,28 +684,20 @@ export default function PublicAgentChatPage(): React.ReactElement {
|
|||
</div>
|
||||
{/* View Mode Tabs */}
|
||||
<div className="flex gap-1 mt-3">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant={viewMode === 'chat' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('chat')}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
viewMode === 'chat'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-foreground-secondary hover:text-foreground hover:bg-surface-secondary'
|
||||
}`}
|
||||
>
|
||||
Chat
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'debug' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('debug')}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
viewMode === 'debug'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-foreground-secondary hover:text-foreground hover:bg-surface-secondary'
|
||||
}`}
|
||||
>
|
||||
Debug JSON
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -776,13 +769,14 @@ export default function PublicAgentChatPage(): React.ReactElement {
|
|||
<span className="text-sm">Loading older messages...</span>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={loadMoreMessages}
|
||||
className="text-sm text-primary hover:text-primary/80 transition-colors"
|
||||
className="text-primary hover:text-primary/80"
|
||||
>
|
||||
Load older messages
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : null,
|
||||
|
|
@ -950,22 +944,23 @@ export default function PublicAgentChatPage(): React.ReactElement {
|
|||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="px-4 py-2 bg-red-50 dark:bg-red-900/20 border-t border-red-200 dark:border-red-800">
|
||||
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
<div className="px-4 py-2 bg-error/10 border-t border-error/20">
|
||||
<p className="text-sm text-error">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="border-t border-border p-4">
|
||||
<div className="flex items-end gap-2">
|
||||
<textarea
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message..."
|
||||
rows={1}
|
||||
className="flex-1 px-4 py-3 bg-background border border-border rounded-lg text-foreground placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary resize-none min-h-[48px] max-h-[200px]"
|
||||
resize="none"
|
||||
className="flex-1 min-h-[48px] max-h-[200px]"
|
||||
style={{
|
||||
height: 'auto',
|
||||
minHeight: '48px',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import type { AIProvider } from '@tpmjs/types/agent';
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
|
||||
import Link from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
|
@ -87,14 +88,10 @@ function ToolCallCard({
|
|||
const effectiveStatus = hasError ? 'error' : toolCall.status;
|
||||
|
||||
const statusColors = {
|
||||
pending:
|
||||
'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400 border-yellow-300 dark:border-yellow-500/30',
|
||||
running:
|
||||
'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 border-blue-300 dark:border-blue-500/30',
|
||||
success:
|
||||
'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400 border-green-300 dark:border-green-500/30',
|
||||
error:
|
||||
'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400 border-red-300 dark:border-red-500/30',
|
||||
pending: 'bg-warning/10 text-warning border-warning/30',
|
||||
running: 'bg-info/10 text-info border-info/30',
|
||||
success: 'bg-success/10 text-success border-success/30',
|
||||
error: 'bg-error/10 text-error border-error/30',
|
||||
};
|
||||
|
||||
const statusIcons: Record<ToolCall['status'], 'loader' | 'check' | 'alertCircle' | 'info'> = {
|
||||
|
|
@ -114,13 +111,13 @@ function ToolCallCard({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-lg border overflow-hidden font-mono text-xs ${hasError ? 'border-red-500/50 bg-red-50 dark:bg-red-500/5' : 'border-border bg-slate-50 dark:bg-surface-secondary/50'}`}
|
||||
className={`rounded-lg border overflow-hidden font-mono text-xs ${hasError ? 'border-error/50 bg-error/5' : 'border-border bg-surface-secondary/50'}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onToggle}
|
||||
className="w-full flex items-center gap-3 p-3 hover:bg-slate-100 dark:hover:bg-surface-secondary/80 transition-colors"
|
||||
className="w-full flex items-center gap-3 p-3 h-auto justify-start rounded-none hover:bg-surface-secondary/80"
|
||||
>
|
||||
<div className={`p-1.5 rounded ${statusColors[effectiveStatus]}`}>
|
||||
<Icon
|
||||
|
|
@ -136,14 +133,14 @@ function ToolCallCard({
|
|||
{toolCall.toolCallId.slice(0, 8)}...
|
||||
</span>
|
||||
{hasError && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-red-100 dark:bg-red-500/20 text-red-600 dark:text-red-400">
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-error/10 text-error">
|
||||
ERROR
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Show error message preview in header */}
|
||||
{hasError && errorMessage && !isExpanded && (
|
||||
<div className="text-red-600 dark:text-red-400 text-[10px] mt-1 truncate max-w-[300px]">
|
||||
<div className="text-error text-[10px] mt-1 truncate max-w-[300px]">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -153,7 +150,7 @@ function ToolCallCard({
|
|||
size="xs"
|
||||
className={`text-foreground-tertiary transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
|
|
@ -175,14 +172,14 @@ function ToolCallCard({
|
|||
|
||||
{/* Error Message Section */}
|
||||
{hasError && errorMessage && (
|
||||
<div className="p-3 bg-red-100 dark:bg-red-500/10 border-b border-red-200 dark:border-red-500/20">
|
||||
<div className="p-3 bg-error/10 border-b border-error/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Icon icon="alertCircle" size="xs" className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-[10px] uppercase tracking-wider text-red-600 dark:text-red-400 font-semibold">
|
||||
<Icon icon="alertCircle" size="xs" className="text-error" />
|
||||
<span className="text-[10px] uppercase tracking-wider text-error font-semibold">
|
||||
Error
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-red-600 dark:text-red-400">{errorMessage}</p>
|
||||
<p className="text-[11px] text-error">{errorMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -196,7 +193,7 @@ function ToolCallCard({
|
|||
<div className="flex-1 h-px bg-border/50" />
|
||||
</div>
|
||||
<pre
|
||||
className={`text-[11px] overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto ${hasError ? 'text-red-600 dark:text-red-300' : 'text-emerald-700 dark:text-green-400'}`}
|
||||
className={`text-[11px] overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto ${hasError ? 'text-error' : 'text-success'}`}
|
||||
>
|
||||
{formatJson(toolCall.output)}
|
||||
</pre>
|
||||
|
|
@ -573,19 +570,21 @@ export default function AgentChatPage(): React.ReactElement {
|
|||
</p>
|
||||
) : (
|
||||
conversations.map((conv) => (
|
||||
<button
|
||||
<Button
|
||||
key={conv.id}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => handleSelectConversation(conv.slug)}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg mb-1 transition-colors ${
|
||||
className={`w-full text-left h-auto py-2 px-3 justify-start mb-1 ${
|
||||
chatId === conv.slug
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-foreground-secondary hover:bg-surface-secondary'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col items-start">
|
||||
<p className="text-sm font-medium truncate">{conv.title || 'Untitled Chat'}</p>
|
||||
<p className="text-xs text-foreground-tertiary">{conv.messageCount} messages</p>
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -738,22 +737,23 @@ export default function AgentChatPage(): React.ReactElement {
|
|||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="px-4 py-2 bg-red-50 dark:bg-red-900/20 border-t border-red-200 dark:border-red-800">
|
||||
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
<div className="px-4 py-2 bg-error/10 border-t border-error/20">
|
||||
<p className="text-sm text-error">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="border-t border-border p-4">
|
||||
<div className="flex items-end gap-2">
|
||||
<textarea
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message..."
|
||||
rows={1}
|
||||
className="flex-1 px-4 py-3 bg-surface border border-border rounded-lg text-foreground placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary resize-none min-h-[48px] max-h-[200px]"
|
||||
resize="none"
|
||||
className="flex-1 min-h-[48px] max-h-[200px]"
|
||||
style={{
|
||||
height: 'auto',
|
||||
minHeight: '48px',
|
||||
|
|
|
|||
|
|
@ -7,8 +7,12 @@ import { Badge } from '@tpmjs/ui/Badge/Badge';
|
|||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { CodeBlock } from '@tpmjs/ui/CodeBlock/CodeBlock';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import { Input } from '@tpmjs/ui/Input/Input';
|
||||
import { Label } from '@tpmjs/ui/Label/Label';
|
||||
import { Select } from '@tpmjs/ui/Select/Select';
|
||||
import { Spinner } from '@tpmjs/ui/Spinner/Spinner';
|
||||
import { Switch } from '@tpmjs/ui/Switch/Switch';
|
||||
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -259,17 +263,12 @@ conv = resp.json() # conv['data']['messages']`,
|
|||
onTabChange={setActiveSection}
|
||||
size="sm"
|
||||
/>
|
||||
<select
|
||||
<Select
|
||||
value={effectiveLang}
|
||||
onChange={(e) => setActiveLang(e.target.value)}
|
||||
className="px-2 py-1 text-sm bg-surface border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>
|
||||
{langOptions.map((opt) => (
|
||||
<option key={opt.id} value={opt.id}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
size="sm"
|
||||
options={langOptions.map((opt) => ({ value: opt.id, label: opt.label }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
|
|
@ -1067,121 +1066,82 @@ export default function AgentDetailPage(): React.ReactElement {
|
|||
<div className="space-y-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-foreground mb-1">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="uid" className="block text-sm font-medium text-foreground mb-1">
|
||||
UID
|
||||
</label>
|
||||
<input
|
||||
<Label htmlFor="uid">UID</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="uid"
|
||||
name="uid"
|
||||
value={formData.uid}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground font-mono text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="block text-sm font-medium text-foreground mb-1"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary resize-none"
|
||||
resize="none"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="provider"
|
||||
className="block text-sm font-medium text-foreground mb-1"
|
||||
>
|
||||
Provider
|
||||
</label>
|
||||
<select
|
||||
<Label htmlFor="provider">Provider</Label>
|
||||
<Select
|
||||
id="provider"
|
||||
name="provider"
|
||||
value={formData.provider}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
>
|
||||
{SUPPORTED_PROVIDERS.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{PROVIDER_DISPLAY_NAMES[p]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
options={SUPPORTED_PROVIDERS.map((p) => ({
|
||||
value: p,
|
||||
label: PROVIDER_DISPLAY_NAMES[p],
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="modelId"
|
||||
className="block text-sm font-medium text-foreground mb-1"
|
||||
>
|
||||
Model
|
||||
</label>
|
||||
<select
|
||||
<Label htmlFor="modelId">Model</Label>
|
||||
<Select
|
||||
id="modelId"
|
||||
name="modelId"
|
||||
value={formData.modelId}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
>
|
||||
{models.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
options={models.map((m) => ({ value: m.id, label: m.name }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="systemPrompt"
|
||||
className="block text-sm font-medium text-foreground mb-1"
|
||||
>
|
||||
System Prompt
|
||||
</label>
|
||||
<textarea
|
||||
<Label htmlFor="systemPrompt">System Prompt</Label>
|
||||
<Textarea
|
||||
id="systemPrompt"
|
||||
name="systemPrompt"
|
||||
value={formData.systemPrompt}
|
||||
onChange={handleChange}
|
||||
rows={6}
|
||||
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground font-mono text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary resize-none"
|
||||
resize="none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="temperature"
|
||||
className="block text-sm font-medium text-foreground mb-1"
|
||||
>
|
||||
Temperature
|
||||
</label>
|
||||
<input
|
||||
<Label htmlFor="temperature">Temperature</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="temperature"
|
||||
name="temperature"
|
||||
|
|
@ -1190,17 +1150,11 @@ export default function AgentDetailPage(): React.ReactElement {
|
|||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="maxToolCallsPerTurn"
|
||||
className="block text-sm font-medium text-foreground mb-1"
|
||||
>
|
||||
Max Tool Calls
|
||||
</label>
|
||||
<input
|
||||
<Label htmlFor="maxToolCallsPerTurn">Max Tool Calls</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="maxToolCallsPerTurn"
|
||||
name="maxToolCallsPerTurn"
|
||||
|
|
@ -1208,17 +1162,11 @@ export default function AgentDetailPage(): React.ReactElement {
|
|||
onChange={handleChange}
|
||||
min={1}
|
||||
max={100}
|
||||
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="maxMessagesInContext"
|
||||
className="block text-sm font-medium text-foreground mb-1"
|
||||
>
|
||||
Context Messages
|
||||
</label>
|
||||
<input
|
||||
<Label htmlFor="maxMessagesInContext">Context Messages</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="maxMessagesInContext"
|
||||
name="maxMessagesInContext"
|
||||
|
|
@ -1226,7 +1174,6 @@ export default function AgentDetailPage(): React.ReactElement {
|
|||
onChange={handleChange}
|
||||
min={1}
|
||||
max={100}
|
||||
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import { Input } from '@tpmjs/ui/Input/Input';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -277,16 +278,13 @@ export default function TpmjsApiKeysPage(): React.ReactElement {
|
|||
<div className="bg-surface border border-border rounded-lg p-6 mb-6">
|
||||
<h2 className="text-lg font-medium text-foreground mb-4">Create API Key</h2>
|
||||
{createError && <p className="text-error text-sm mb-3">{createError}</p>}
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
|
||||
placeholder="Key name (e.g., Production Server, CI/CD)"
|
||||
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-foreground text-sm placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
<Button onClick={handleCreate} disabled={creating || !newKeyName.trim()}>
|
||||
{creating ? 'Creating...' : 'Create Key'}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import { Select } from '@tpmjs/ui/Select/Select';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
||||
|
|
@ -107,17 +108,16 @@ export default function UsagePage(): React.ReactElement {
|
|||
title="Usage"
|
||||
subtitle="Monitor your API usage and costs"
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
<Select
|
||||
value={period}
|
||||
onChange={(e) => setPeriod(e.target.value as Period)}
|
||||
className="px-3 py-1.5 bg-surface border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>
|
||||
<option value="hourly">Hourly</option>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
</select>
|
||||
</div>
|
||||
size="sm"
|
||||
options={[
|
||||
{ value: 'hourly', label: 'Hourly' },
|
||||
{ value: 'daily', label: 'Daily' },
|
||||
{ value: 'monthly', label: 'Monthly' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@ export default function AuthenticationPage(): React.ReactElement {
|
|||
<li>Copy your key and store it securely</li>
|
||||
</ol>
|
||||
|
||||
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4">
|
||||
<p className="text-amber-600 dark:text-amber-400 font-medium">Important</p>
|
||||
<div className="bg-warning/10 border border-warning/20 rounded-lg p-4">
|
||||
<p className="text-warning font-medium">Important</p>
|
||||
<p className="text-foreground-secondary text-sm mt-1">
|
||||
Your API key is displayed only once when created. Make sure to copy and store it
|
||||
securely. If you lose it, you'll need to generate a new one.
|
||||
|
|
|
|||
|
|
@ -120,10 +120,10 @@ function EndpointCard({
|
|||
children?: React.ReactNode;
|
||||
}) {
|
||||
const methodColors = {
|
||||
GET: 'bg-green-500/10 text-green-500 border-green-500/30',
|
||||
POST: 'bg-blue-500/10 text-blue-500 border-blue-500/30',
|
||||
PUT: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30',
|
||||
DELETE: 'bg-red-500/10 text-red-500 border-red-500/30',
|
||||
GET: 'bg-success/10 text-success border-success/30',
|
||||
POST: 'bg-info/10 text-info border-info/30',
|
||||
PUT: 'bg-warning/10 text-warning border-warning/30',
|
||||
DELETE: 'bg-error/10 text-error border-error/30',
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -377,7 +377,7 @@ export default function APIDocsPage(): React.ReactElement {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border border-yellow-500/30 rounded-lg bg-yellow-500/5">
|
||||
<div className="p-4 border border-warning/30 rounded-lg bg-warning/5">
|
||||
<h3 className="font-semibold text-foreground mb-2">API Key Scopes</h3>
|
||||
<ul className="text-sm text-foreground-secondary list-disc list-inside space-y-1">
|
||||
<li>
|
||||
|
|
@ -398,7 +398,7 @@ export default function APIDocsPage(): React.ReactElement {
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border border-green-500/30 rounded-lg bg-green-500/5">
|
||||
<div className="p-4 border border-success/30 rounded-lg bg-success/5">
|
||||
<h3 className="font-semibold text-foreground mb-2">Rate Limits</h3>
|
||||
<ul className="text-sm text-foreground-secondary list-disc list-inside space-y-1">
|
||||
<li>FREE tier: 100 requests/hour</li>
|
||||
|
|
|
|||
|
|
@ -174,10 +174,10 @@ export default function ExecutorsDocsPage(): React.ReactElement {
|
|||
<li>Optionally add an API key if your executor requires authentication</li>
|
||||
<li>Click "Verify Connection" to test the configuration</li>
|
||||
</ol>
|
||||
<div className="p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg">
|
||||
<p className="text-sm text-amber-600 dark:text-amber-400">
|
||||
<div className="p-4 bg-warning/10 border border-warning/30 rounded-lg">
|
||||
<p className="text-sm text-warning">
|
||||
<strong>Security tip:</strong> Set the{' '}
|
||||
<code className="px-1 bg-amber-500/20 rounded">EXECUTOR_API_KEY</code> environment
|
||||
<code className="px-1 bg-warning/20 rounded">EXECUTOR_API_KEY</code> environment
|
||||
variable in your Vercel project to require authentication for all requests.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -214,7 +214,7 @@ export default function ExecutorsDocsPage(): React.ReactElement {
|
|||
{/* GET /health */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
<code className="px-2 py-1 bg-green-500/10 text-green-500 rounded">GET</code>{' '}
|
||||
<code className="px-2 py-1 bg-success/10 text-success rounded">GET</code>{' '}
|
||||
/health
|
||||
</h3>
|
||||
<p className="text-foreground-secondary mb-4">
|
||||
|
|
|
|||
|
|
@ -224,15 +224,15 @@ const slides: Slide[] = [
|
|||
</p>
|
||||
<ul className="space-y-2 text-sm text-foreground-secondary">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span className="text-success">✓</span>
|
||||
<span>Execute JavaScript/Python</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span className="text-success">✓</span>
|
||||
<span>Fetch web pages</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span className="text-success">✓</span>
|
||||
<span>Web search</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -244,15 +244,15 @@ const slides: Slide[] = [
|
|||
</p>
|
||||
<ul className="space-y-2 text-sm text-foreground-secondary">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span className="text-success">✓</span>
|
||||
<span>Pre-curated tool sets</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span className="text-success">✓</span>
|
||||
<span>One-click to add many tools</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span className="text-success">✓</span>
|
||||
<span>Community collections</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -308,7 +308,7 @@ const slides: Slide[] = [
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-2xl mx-auto p-4 bg-green-500/10 border border-green-500/20 rounded-lg">
|
||||
<div className="max-w-2xl mx-auto p-4 bg-success/10 border border-success/20 rounded-lg">
|
||||
<p className="text-sm text-foreground text-center">
|
||||
🎉 <strong>Conversations are automatically saved</strong> — pick up where you left off
|
||||
anytime!
|
||||
|
|
|
|||
|
|
@ -66,23 +66,23 @@ const slides: Slide[] = [
|
|||
<h4 className="text-lg font-semibold text-foreground mb-3">Cloud-Only Tools</h4>
|
||||
<ul className="space-y-3 text-foreground-secondary">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">✓</span>
|
||||
<span className="text-success mt-0.5">✓</span>
|
||||
<span>Code execution (sandboxed)</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">✓</span>
|
||||
<span className="text-success mt-0.5">✓</span>
|
||||
<span>Web fetching</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">✓</span>
|
||||
<span className="text-success mt-0.5">✓</span>
|
||||
<span>Web search</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">✗</span>
|
||||
<span className="text-error mt-0.5">✗</span>
|
||||
<span>Browser automation</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">✗</span>
|
||||
<span className="text-error mt-0.5">✗</span>
|
||||
<span>Local file access</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -91,23 +91,23 @@ const slides: Slide[] = [
|
|||
<h4 className="text-lg font-semibold text-foreground mb-3">With Bridge</h4>
|
||||
<ul className="space-y-3 text-foreground-secondary">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">✓</span>
|
||||
<span className="text-success mt-0.5">✓</span>
|
||||
<span>Chrome DevTools control</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">✓</span>
|
||||
<span className="text-success mt-0.5">✓</span>
|
||||
<span>Read/write local files</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">✓</span>
|
||||
<span className="text-success mt-0.5">✓</span>
|
||||
<span>Local database access</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">✓</span>
|
||||
<span className="text-success mt-0.5">✓</span>
|
||||
<span>Custom internal APIs</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">✓</span>
|
||||
<span className="text-success mt-0.5">✓</span>
|
||||
<span>Any stdio MCP server</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -439,13 +439,13 @@ Opening browser for authentication...
|
|||
</div>
|
||||
<div className="flex justify-center gap-3 text-sm text-foreground-secondary">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-green-500">✓</span> Chrome automation
|
||||
<span className="text-success">✓</span> Chrome automation
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-green-500">✓</span> Local files
|
||||
<span className="text-success">✓</span> Local files
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-green-500">✓</span> Custom tools
|
||||
<span className="text-success">✓</span> Custom tools
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -176,15 +176,15 @@ export default function CustomExecutorTutorialPage(): React.ReactElement {
|
|||
|
||||
<CodeBlock language="bash" code={envVarsExample} />
|
||||
|
||||
<div className="mt-4 p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg">
|
||||
<div className="mt-4 p-4 bg-warning/10 border border-warning/30 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<Icon icon="alertCircle" className="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5" />
|
||||
<Icon icon="alertCircle" className="w-5 h-5 text-warning flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-amber-600 dark:text-amber-400">
|
||||
<p className="font-medium text-warning">
|
||||
Security Recommendation
|
||||
</p>
|
||||
<p className="text-sm text-foreground-secondary mt-1">
|
||||
Set <code className="px-1 bg-amber-500/20 rounded">EXECUTOR_API_KEY</code> to
|
||||
Set <code className="px-1 bg-warning/20 rounded">EXECUTOR_API_KEY</code> to
|
||||
require authentication. Without it, anyone with your executor URL can execute
|
||||
tools.
|
||||
</p>
|
||||
|
|
@ -221,12 +221,12 @@ export default function CustomExecutorTutorialPage(): React.ReactElement {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<div className="mt-4 p-4 bg-success/10 border border-success/30 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<Icon icon="check" className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
|
||||
<Icon icon="check" className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-foreground-secondary">
|
||||
If you see{' '}
|
||||
<code className="px-1 bg-green-500/20 rounded">
|
||||
<code className="px-1 bg-success/20 rounded">
|
||||
"status": "ok"
|
||||
</code>
|
||||
, your executor is running and ready to use!
|
||||
|
|
|
|||
|
|
@ -65,19 +65,19 @@ const slides: Slide[] = [
|
|||
<h4 className="text-lg font-semibold text-foreground mb-3">Without MCP</h4>
|
||||
<ul className="space-y-3 text-foreground-secondary">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">✗</span>
|
||||
<span className="text-error mt-0.5">✗</span>
|
||||
<span>AI can only process text</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">✗</span>
|
||||
<span className="text-error mt-0.5">✗</span>
|
||||
<span>No access to external data</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">✗</span>
|
||||
<span className="text-error mt-0.5">✗</span>
|
||||
<span>Can't take real actions</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">✗</span>
|
||||
<span className="text-error mt-0.5">✗</span>
|
||||
<span>Limited to knowledge cutoff</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -86,19 +86,19 @@ const slides: Slide[] = [
|
|||
<h4 className="text-lg font-semibold text-foreground mb-3">With MCP + TPMJS</h4>
|
||||
<ul className="space-y-3 text-foreground-secondary">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">✓</span>
|
||||
<span className="text-success mt-0.5">✓</span>
|
||||
<span>Execute code in 40+ languages</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">✓</span>
|
||||
<span className="text-success mt-0.5">✓</span>
|
||||
<span>Fetch data from any URL</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">✓</span>
|
||||
<span className="text-success mt-0.5">✓</span>
|
||||
<span>Search the web in real-time</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-500 mt-0.5">✓</span>
|
||||
<span className="text-success mt-0.5">✓</span>
|
||||
<span>Use specialized tools</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -362,16 +362,16 @@ const slides: Slide[] = [
|
|||
</div>
|
||||
<div className="flex justify-center gap-3 text-sm text-foreground-secondary">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-green-500">✓</span> Code execution
|
||||
<span className="text-success">✓</span> Code execution
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-green-500">✓</span> Web fetching
|
||||
<span className="text-success">✓</span> Web fetching
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-green-500">✓</span> Web search
|
||||
<span className="text-success">✓</span> Web search
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-green-500">✓</span> And more!
|
||||
<span className="text-success">✓</span> And more!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ export default function PublishPage(): React.ReactElement {
|
|||
</div>
|
||||
|
||||
{/* Auto-discovery callout */}
|
||||
<div className="mb-8 p-6 border-2 border-amber-500/30 rounded-lg bg-amber-500/5">
|
||||
<div className="mb-8 p-6 border-2 border-warning/30 rounded-lg bg-warning/5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="text-3xl">🔍</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { TPMJS_CATEGORIES } from '@tpmjs/types/tpmjs';
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { CodeBlock } from '@tpmjs/ui/CodeBlock/CodeBlock';
|
||||
import { Container } from '@tpmjs/ui/Container/Container';
|
||||
import { useState } from 'react';
|
||||
|
|
@ -122,28 +123,22 @@ export default function SpecPage(): React.ReactElement {
|
|||
|
||||
{/* View Toggle */}
|
||||
<div className="flex gap-1 p-1 bg-surface rounded-lg border border-border self-center md:self-auto">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant={view === 'spec' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setView('spec')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
view === 'spec'
|
||||
? 'bg-foreground text-background'
|
||||
: 'text-foreground-secondary hover:text-foreground'
|
||||
}`}
|
||||
className="rounded-md"
|
||||
>
|
||||
Specification
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant={view === 'example' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setView('example')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
view === 'example'
|
||||
? 'bg-foreground text-background'
|
||||
: 'text-foreground-secondary hover:text-foreground'
|
||||
}`}
|
||||
className="rounded-md"
|
||||
>
|
||||
Full Example
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Container } from '@tpmjs/ui/Container/Container';
|
||||
import Link from 'next/link';
|
||||
import { AppHeader } from '~/components/AppHeader';
|
||||
|
|
@ -450,20 +451,10 @@ export default function TermsPage(): React.ReactElement {
|
|||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="mailto:hello@tpmjs.com">
|
||||
<button
|
||||
type="button"
|
||||
className="px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
Contact Us
|
||||
</button>
|
||||
<Button size="lg">Contact Us</Button>
|
||||
</a>
|
||||
<Link href="/">
|
||||
<button
|
||||
type="button"
|
||||
className="px-6 py-3 border border-border rounded-lg font-medium hover:bg-surface transition-colors text-foreground"
|
||||
>
|
||||
Back to Home
|
||||
</button>
|
||||
<Button variant="outline" size="lg">Back to Home</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import { Input } from '@tpmjs/ui/Input/Input';
|
||||
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { parseEnvString } from '~/lib/utils/env-parser';
|
||||
|
||||
|
|
@ -148,9 +150,6 @@ export function EnvVarsEditor({
|
|||
return parseEnvString(pasteContent);
|
||||
}, [pasteContent]);
|
||||
|
||||
const inputClassName =
|
||||
'flex-1 px-3 py-2 bg-surface border border-border rounded-lg text-foreground font-mono text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{/* Header */}
|
||||
|
|
@ -182,11 +181,12 @@ export function EnvVarsEditor({
|
|||
<Icon icon="x" size="xs" />
|
||||
</Button>
|
||||
</div>
|
||||
<textarea
|
||||
<Textarea
|
||||
value={pasteContent}
|
||||
onChange={(e) => setPasteContent(e.target.value)}
|
||||
placeholder={`# Paste your .env content here\nAPI_KEY=your-api-key\nDATABASE_URL="postgres://..."\n`}
|
||||
className="w-full h-32 px-3 py-2 bg-surface border border-border rounded-lg text-foreground font-mono text-xs focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary resize-none"
|
||||
className="h-32 text-xs"
|
||||
resize="none"
|
||||
disabled={disabled}
|
||||
/>
|
||||
{parsedPreview.length > 0 && (
|
||||
|
|
@ -217,20 +217,20 @@ export function EnvVarsEditor({
|
|||
<div className="space-y-2 mb-3">
|
||||
{envVars.map((env, index) => (
|
||||
<div key={`env-${env.key || index}`} className="flex items-center gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={env.key}
|
||||
onChange={(e) => updateEnvVar(index, 'key', e.target.value)}
|
||||
placeholder="KEY"
|
||||
className={inputClassName}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type="password"
|
||||
value={env.value}
|
||||
onChange={(e) => updateEnvVar(index, 'value', e.target.value)}
|
||||
placeholder="value"
|
||||
className={inputClassName}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Button
|
||||
|
|
@ -249,12 +249,12 @@ export function EnvVarsEditor({
|
|||
|
||||
{/* Add new env var */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={newEnvKey}
|
||||
onChange={(e) => setNewEnvKey(e.target.value.toUpperCase())}
|
||||
placeholder={keyPlaceholder}
|
||||
className={inputClassName}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && newEnvKey.trim()) {
|
||||
|
|
@ -262,12 +262,12 @@ export function EnvVarsEditor({
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={newEnvValue}
|
||||
onChange={(e) => setNewEnvValue(e.target.value)}
|
||||
placeholder={valuePlaceholder}
|
||||
className={inputClassName}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && newEnvKey.trim()) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tpmjs/tools-unsandbox",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Execute code in a secure sandbox environment. Supports 42+ programming languages with async execution, input files, and compiled artifacts.",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
|
|
@ -41,66 +41,38 @@
|
|||
"homepage": "https://tpmjs.com",
|
||||
"license": "MIT",
|
||||
"tpmjs": {
|
||||
"category": "code-execution",
|
||||
"category": "sandbox",
|
||||
"frameworks": [
|
||||
"vercel-ai"
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "executeCodeAsync",
|
||||
"description": "Execute code asynchronously in a secure sandbox. Returns a job_id immediately. Use getJob to check status and retrieve results.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "language",
|
||||
"type": "string",
|
||||
"description": "Programming language to execute",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "code",
|
||||
"type": "string",
|
||||
"description": "The source code to execute",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "input_files",
|
||||
"type": "array",
|
||||
"description": "Optional array of input files",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "network_mode",
|
||||
"type": "string",
|
||||
"description": "Network isolation mode: zerotrust or semitrusted",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "ttl",
|
||||
"type": "number",
|
||||
"description": "Execution timeout in seconds (1-900)",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "object",
|
||||
"description": "Job ID and initial status"
|
||||
}
|
||||
"description": "Execute code asynchronously in a secure sandbox. Returns a job_id immediately. Use getJob to check status and retrieve results. Supports 42+ languages."
|
||||
},
|
||||
{
|
||||
"name": "getJob",
|
||||
"description": "Get the status and results of an async code execution job",
|
||||
"parameters": [
|
||||
"description": "Get the status and results of an async code execution job by job_id."
|
||||
},
|
||||
{
|
||||
"name": "job_id",
|
||||
"type": "string",
|
||||
"description": "The job ID returned from executeCodeAsync",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "object",
|
||||
"description": "Job status and results"
|
||||
}
|
||||
"name": "execute",
|
||||
"description": "Execute code synchronously in a secure sandbox. Waits for completion and returns results directly. Best for quick scripts."
|
||||
},
|
||||
{
|
||||
"name": "run",
|
||||
"description": "Execute code synchronously with automatic language detection via shebang (e.g., #!/usr/bin/env python)."
|
||||
},
|
||||
{
|
||||
"name": "runAsync",
|
||||
"description": "Execute code asynchronously with automatic language detection via shebang. Returns job_id for polling."
|
||||
},
|
||||
{
|
||||
"name": "listJobs",
|
||||
"description": "List all active code execution jobs with their current status."
|
||||
},
|
||||
{
|
||||
"name": "deleteJob",
|
||||
"description": "Cancel an active code execution job by job_id."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue