First implementation of Plimi
This commit is contained in:
215
README.md
Normal file
215
README.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user