216 lines
6.7 KiB
Markdown
216 lines
6.7 KiB
Markdown
# Plimi: Privacy-First Browser Toolbox
|
|
|
|
Plimi is a modern, modular, zero-backend utility suite that executes all tools directly inside your browser. No data ever leaves your device.
|
|
|
|
## Features
|
|
- **Privacy-First Design**: Completely offline-capable. No data is sent to external servers.
|
|
- **Plugin Architecture**: Modular, strictly-typed plugin ecosystem supporting custom UI or generated UI based on schema.
|
|
- **Web Workers & WASM**: Supports off-loading expensive operations (e.g. PDF manipulation, media processing) into separate threads.
|
|
- **High Performance**: Built with React, Vite, and Tailwind CSS v4.
|
|
|
|
---
|
|
|
|
## Getting Started
|
|
|
|
### Prerequisites
|
|
- Node.js 18+
|
|
- npm or pnpm
|
|
|
|
### Installation & Run Dev Server
|
|
```bash
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
### Run Unit Tests
|
|
```bash
|
|
npm test
|
|
```
|
|
|
|
### Production Build
|
|
```bash
|
|
npm run build
|
|
```
|
|
|
|
---
|
|
|
|
## How to Create a Tool (Plugin)
|
|
|
|
Adding a new tool to Plimi is seamless thanks to its generic rendering architecture. By default, Plimi generates the tool's interface based on its options schema and input/output definitions.
|
|
|
|
### Step 1: Create the Plugin Definition
|
|
Create a new directory in `src/tools/` (e.g., `src/tools/my-tool/index.ts`):
|
|
|
|
```typescript
|
|
import type { PlimiPlugin } from '../../core/plugins/plugin-types';
|
|
import { runMyTool } from './run';
|
|
|
|
export interface MyToolOptions {
|
|
uppercase: boolean;
|
|
prefix: string;
|
|
}
|
|
|
|
export const myToolPlugin: PlimiPlugin<MyToolOptions> = {
|
|
manifest: {
|
|
id: 'my-tool',
|
|
name: 'My Custom Tool',
|
|
description: 'Does something awesome entirely in the browser.',
|
|
category: 'developer',
|
|
version: '1.0.0',
|
|
tags: ['custom', 'developer', 'text'],
|
|
|
|
// 1. Defining Inputs (see Input Options below)
|
|
input: {
|
|
type: 'text',
|
|
multiline: false, // Renders a single-line input field instead of a textarea
|
|
placeholder: 'Type something...',
|
|
example: 'Hello Plimi'
|
|
},
|
|
|
|
// 2. Defining Outputs (text, json, table, files)
|
|
output: { type: 'text' },
|
|
offlineReady: true,
|
|
},
|
|
|
|
// 3. User options generated in the right panel automatically
|
|
optionsSchema: {
|
|
fields: [
|
|
{
|
|
type: 'boolean',
|
|
key: 'uppercase',
|
|
label: 'Uppercase Output',
|
|
defaultValue: false
|
|
},
|
|
{
|
|
type: 'text',
|
|
key: 'prefix',
|
|
label: 'Output Prefix',
|
|
defaultValue: 'Result: ',
|
|
placeholder: 'e.g. Result: '
|
|
}
|
|
]
|
|
},
|
|
|
|
// Optional: override or add user-facing examples.
|
|
// If omitted, Plimi automatically builds a "Try example" action from
|
|
// manifest.example for single-input tools or field.example for grouped inputs.
|
|
examples: [
|
|
{
|
|
label: 'Try greeting',
|
|
input: { text: 'Hello Plimi' },
|
|
options: { uppercase: true, prefix: 'Result: ' }
|
|
}
|
|
],
|
|
|
|
capabilities: {
|
|
cancelable: false,
|
|
worker: false,
|
|
},
|
|
|
|
run: runMyTool,
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### Step 2: Implement the Runner Logic
|
|
Create the runner file (e.g., `src/tools/my-tool/run.ts`):
|
|
|
|
```typescript
|
|
import { getTextInput, type ToolInput } from '../../core/io/input-types';
|
|
import type { ToolResult } from '../../core/io/output-types';
|
|
import type { ToolContext } from '../../core/plugins/plugin-types';
|
|
import type { MyToolOptions } from './index';
|
|
|
|
export async function runMyTool(
|
|
input: ToolInput,
|
|
options: MyToolOptions,
|
|
context: ToolContext
|
|
): Promise<ToolResult> {
|
|
// Use getTextInput helper for safe retrieval
|
|
const text = getTextInput(input);
|
|
if (!text) {
|
|
throw new Error("No text provided.");
|
|
}
|
|
|
|
// Report progress back to the UI progress bar (if needed)
|
|
context.reportProgress({ percentage: 50, message: "Processing text..." });
|
|
|
|
let result = text;
|
|
if (options.uppercase) {
|
|
result = result.toUpperCase();
|
|
}
|
|
|
|
return {
|
|
type: 'text',
|
|
value: `${options.prefix}${result}`,
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Step 3: Register the Plugin
|
|
Import your new plugin into `src/core/plugins/plugin-registry.ts` and append it to the `pluginRegistry` array:
|
|
|
|
```typescript
|
|
import { myToolPlugin } from "../../tools/my-tool";
|
|
|
|
export const pluginRegistry: PlimiPlugin<any>[] = [
|
|
// ... existing plugins
|
|
myToolPlugin,
|
|
];
|
|
```
|
|
The application will automatically register the tool, list it in the directory, enable search, and construct its input and options UI!
|
|
|
|
---
|
|
|
|
## UI Abstraction Details
|
|
|
|
### Input Schema Configuration
|
|
The input property in the manifest supports three structures:
|
|
1. **`type: "none"`**: No input field is rendered (useful for generators, e.g., Lorem Ipsum).
|
|
2. **Single Field**: Renders a single input field.
|
|
- `type: "text"`: Renders a text entry box.
|
|
- Specify `multiline: false` to make it a compact single-line input field (default is a textarea).
|
|
- Specify `rows: number` to customize multiline text area height.
|
|
- `type: "files"`: Renders a drag-and-drop file uploader area.
|
|
3. **`type: "group"`**: Renders a group of input fields stacked vertically. Each field requires a unique `key`.
|
|
- Plimi will automatically expose a shared **"Try example"** action when fields contain example data, allowing the user to populate the form with one click.
|
|
- Access group values inside the runner using `getTextInput(input, "field_key")`.
|
|
|
|
### Output Visualizers
|
|
Plimi automatically formats results based on the returned `ToolResult.type`:
|
|
- **`type: "text"`**: Renders a code block or plain text area with a global copy button.
|
|
- **`type: "files"`**: Renders a lists of downloadable files with size comparisons.
|
|
- **`type: "table"`**: Renders a tabular data format with column headers, row styles, and a **"Copy CSV"** button.
|
|
- **`type: "json"`**: Renders a flat grid of cards for each key.
|
|
- Return a key named `"primary"` in your JSON output to showcase it prominently in a large card at the top of the results panel.
|
|
- Each key in the grid renders with its own individual, hoverable **"Copy"** button for a premium experience.
|
|
|
|
---
|
|
|
|
### Writing Custom UIs
|
|
For complex interfaces (like Canvas-based image manipulation or interactive dropzones), set `customUi: true` under `capabilities` in the manifest and supply a React component to the `customUi` key:
|
|
|
|
```typescript
|
|
import { lazy } from "react";
|
|
const CustomUiComponent = lazy(() => import("./CustomUiComponent"));
|
|
|
|
export const myPlugin = {
|
|
// ...
|
|
capabilities: { customUi: true },
|
|
customUi: CustomUiComponent
|
|
};
|
|
```
|
|
Your component will receive the plugin definition as `plugin`, the current `dark` theme boolean, and normalized `examples` as props. Custom UIs can import `ExampleActionButton` from `src/components/tool/ExampleActionButton` to render the same "Try example" action style used by generated tools.
|
|
|
|
---
|
|
|
|
### Writing Tests
|
|
Every tool should be tested. Create a `run.test.ts` alongside your tool and run:
|
|
```bash
|
|
npm test
|
|
```
|
|
See `src/tools/regex-tester/run.test.ts` for an example of testing both key-based inputs and fallback single-string values.
|