# Plimi: Privacy-First Browser Toolbox

Plimi is a modern, modular utility suite that executes tool processing directly inside your browser. Tool inputs and outputs are not uploaded by Plimi.

## Features
- **Privacy-First Design**: Tool processing is offline-capable. Optional audience analytics load only after consent.
- **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
```

### Docker
Build and run the static production app with Nginx:

```bash
cp .env.example .env
# Edit .env with the real deployment and legal values.
docker build --secret id=vite_env,src=.env -t plimi:local .

docker run --rm -p 8080:80 --name plimi plimi:local
```

Or use Docker Compose:

```bash
cp .env.example .env
# Edit .env, then:
docker compose up --build
```

Environment:
- `VITE_PLIMI_REPO_URL`: build-time public repository URL shown on the Contribute page.
- `VITE_SITE_URL`: canonical public URL used to restrict analytics to the deployed hostname.
- `VITE_LEGAL_NAME`, `VITE_LEGAL_COUNTRY`: controller identity shown in the privacy notice.
- `VITE_PRIVACY_EMAIL`: contact address for privacy and data-subject requests.
- `VITE_PRIVACY_EFFECTIVE_DATE`: effective date displayed on the privacy notice.
- `VITE_ANALYTICS_SCRIPT_URL`, `VITE_ANALYTICS_WEBSITE_ID`: Umami tracker configuration.
- `VITE_ANALYTICS_HOST`, `VITE_ANALYTICS_HOST_COUNTRY`: analytics recipient and hosting location disclosed to visitors.
- `VITE_ANALYTICS_RETENTION_MONTHS`: disclosed analytics retention period. Configure Umami/database deletion to match it.
- `PLIMI_PORT`: host port used by Docker Compose, default `8080`.

Analytics is consent-first. The tracker is injected only after acceptance and is
configured to exclude URL search parameters and hashes and honor Do Not Track.
The privacy page is available at `/privacy`; deployers must replace all example
controller and hosting values before publishing.

Docker mounts `.env` as a BuildKit secret only while Vite builds the static
bundle. The file is ignored by Git and excluded from the regular Docker context.
Remember that `VITE_*` values are public build-time configuration and can be
read from the resulting browser JavaScript; do not put passwords or API secrets
in them.

---

## 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.
