feat: containerize with Docker Compose + Ollama

Add full Docker setup so the app runs with a single 'docker compose up':

- Dockerfile: multi-stage build (node:22-alpine) for the SvelteKit app
- docker-compose.yml: three services:
  1. ollama: runs Ollama server with persistent volume for models
  2. model-init: one-shot container that pulls the configured model
     after Ollama is healthy, then exits
  3. app: the SvelteKit app, starts only after model-init succeeds
- .env.docker: set OLLAMA_MODEL to control which model is pulled
- .dockerignore: keeps image lean
- Switched adapter-auto to adapter-node (required for Docker/Node hosting)
- Updated README with Docker and local dev instructions

Usage:
  docker compose up              # default: llama3
  OLLAMA_MODEL=gemma2 docker compose up  # any Ollama model
This commit is contained in:
2026-04-13 00:22:19 -04:00
parent 11bb42240a
commit 792fafc661
10 changed files with 1191 additions and 422 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
node_modules
.svelte-kit
build
.git
.env
*.md
docs
.vscode

3
.env.docker Normal file
View File

@@ -0,0 +1,3 @@
# Model to use with Ollama — change this to any Ollama-compatible model
# Examples: llama3, llama3.1, llama3.2, gemma2, mistral, phi3, qwen2, codellama
OLLAMA_MODEL=llama3

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@ Thumbs.db
.env.* .env.*
!.env.example !.env.example
!.env.test !.env.test
!.env.docker
# Vite # Vite
vite.config.js.timestamp-* vite.config.js.timestamp-*

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
# ---- Build stage ----
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- Run stage ----
FROM node:22-alpine AS run
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/build ./build
COPY --from=build /app/package.json .
ENV PORT=3000
ENV HOST=0.0.0.0
EXPOSE 3000
CMD ["node", "build"]

View File

@@ -1,42 +1,61 @@
# sv # English Style Converter
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). A SvelteKit web app that converts English text into various styles and tones using an LLM.
## Creating a project ## Quick Start (Docker)
If you're seeing this, you've probably already done this step. Congrats! ```bash
# Option 1: Use default model (llama3)
docker compose up
```sh # Option 2: Choose a different model
# create a new project OLLAMA_MODEL=gemma2 docker compose up
npx sv create my-app
``` ```
To recreate this project with the same configuration: - **App:** http://localhost:3000
- **Ollama API:** http://localhost:11434
```sh First startup pulls the model from Ollama, which may take a few minutes depending on model size and your connection. Subsequent starts are instant (model is cached in a Docker volume).
# recreate this project
npx sv@0.15.1 create --template minimal --types ts --no-install . To change the model later, edit `.env.docker` and run:
```bash
docker compose down
docker compose up --build
``` ```
## Developing ## Local Development (without Docker)
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: Prerequisites: [Ollama](https://ollama.ai) running locally with a model pulled.
```sh ```bash
# Install dependencies
npm install
# Copy env config (defaults to Ollama at localhost:11434)
cp .env.example .env
# Start dev server
npm run dev npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
``` ```
## Building ## Configuration
To create a production version of your app: | Variable | Default | Description |
|----------|---------|-------------|
| `OPENAI_BASE_URL` | `http://localhost:11434/v1` | OpenAI-compatible API endpoint |
| `OPENAI_API_KEY` | `ollama` | API key (use `ollama` for local Ollama) |
| `OPENAI_MODEL` | `llama3` | Model to use |
| `PORT` | `3000` | App port (Docker/adapter-node only) |
```sh For Docker, set `OLLAMA_MODEL` in `.env.docker` — it controls both the model Ollama pulls and the model the app requests.
npm run build
## Styles
6 categories, 25 styles: Sarcastic, Formal, British (Polite, Formal, Witty, Gentlemanly, Upper Class, Royal, Victorian, Downton Abbey), American (New Yorker, AAVE, Southern, Redneck), Pirate, Shakespearean, Gen Z, Game of Thrones (King's Landing, Wildlings, Winterfell), and Newspeak (Orwellian).
## Testing
```bash
npm test
``` ```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

48
docker-compose.yml Normal file
View File

@@ -0,0 +1,48 @@
services:
ollama:
image: ollama/ollama:latest
container_name: english-styler-ollama
ports:
- "11434:11434"
volumes:
- ollama-data:/root/.ollama
healthcheck:
test: ["CMD", "ollama", "list"]
interval: 5s
timeout: 3s
retries: 30
start_period: 5s
restart: unless-stopped
model-init:
image: ollama/ollama:latest
container_name: english-styler-model-init
depends_on:
ollama:
condition: service_healthy
environment:
OLLAMA_HOST: http://ollama:11434
entrypoint: >
sh -c "
echo 'Pulling Ollama model: ${OLLAMA_MODEL:-llama3}' &&
ollama pull ${OLLAMA_MODEL:-llama3} &&
echo 'Model ready ✅'
"
restart: "no"
app:
build: .
container_name: english-styler-app
ports:
- "3000:3000"
depends_on:
model-init:
condition: service_completed_successfully
environment:
OPENAI_BASE_URL: http://ollama:11434/v1
OPENAI_API_KEY: ollama
OPENAI_MODEL: ${OLLAMA_MODEL:-llama3}
restart: unless-stopped
volumes:
ollama-data:

646
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.57.0", "@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0",
"svelte": "^5.55.2", "svelte": "^5.55.2",
@@ -379,6 +380,476 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/plugin-commonjs": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz",
"integrity": "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"commondir": "^1.0.1",
"estree-walker": "^2.0.2",
"fdir": "^6.2.0",
"is-reference": "1.2.1",
"magic-string": "^0.30.3",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=16.0.0 || 14 >= 14.17"
},
"peerDependencies": {
"rollup": "^2.68.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/plugin-commonjs/node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/@rollup/plugin-json": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
"integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils/node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
"integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
"integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
"integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
"integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
"integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
"integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
"integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
"integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@standard-schema/spec": { "node_modules/@standard-schema/spec": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -406,6 +877,22 @@
"@sveltejs/kit": "^2.0.0" "@sveltejs/kit": "^2.0.0"
} }
}, },
"node_modules/@sveltejs/adapter-node": {
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.4.tgz",
"integrity": "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"rollup": "^4.59.0"
},
"peerDependencies": {
"@sveltejs/kit": "^2.4.0"
}
},
"node_modules/@sveltejs/kit": { "node_modules/@sveltejs/kit": {
"version": "2.57.1", "version": "2.57.1",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.57.1.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.57.1.tgz",
@@ -513,6 +1000,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": { "node_modules/@types/trusted-types": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -713,6 +1207,13 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true,
"license": "MIT"
},
"node_modules/convert-source-map": { "node_modules/convert-source-map": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -757,6 +1258,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
@@ -842,6 +1353,52 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true,
"license": "MIT"
},
"node_modules/is-reference": { "node_modules/is-reference": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
@@ -1190,6 +1747,13 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/pathe": { "node_modules/pathe": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -1210,7 +1774,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -1261,6 +1824,28 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/resolve": {
"version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.0-rc.15", "version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
@@ -1295,6 +1880,52 @@
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
} }
}, },
"node_modules/rollup": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.60.1",
"@rollup/rollup-android-arm64": "4.60.1",
"@rollup/rollup-darwin-arm64": "4.60.1",
"@rollup/rollup-darwin-x64": "4.60.1",
"@rollup/rollup-freebsd-arm64": "4.60.1",
"@rollup/rollup-freebsd-x64": "4.60.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
"@rollup/rollup-linux-arm-musleabihf": "4.60.1",
"@rollup/rollup-linux-arm64-gnu": "4.60.1",
"@rollup/rollup-linux-arm64-musl": "4.60.1",
"@rollup/rollup-linux-loong64-gnu": "4.60.1",
"@rollup/rollup-linux-loong64-musl": "4.60.1",
"@rollup/rollup-linux-ppc64-gnu": "4.60.1",
"@rollup/rollup-linux-ppc64-musl": "4.60.1",
"@rollup/rollup-linux-riscv64-gnu": "4.60.1",
"@rollup/rollup-linux-riscv64-musl": "4.60.1",
"@rollup/rollup-linux-s390x-gnu": "4.60.1",
"@rollup/rollup-linux-x64-gnu": "4.60.1",
"@rollup/rollup-linux-x64-musl": "4.60.1",
"@rollup/rollup-openbsd-x64": "4.60.1",
"@rollup/rollup-openharmony-arm64": "4.60.1",
"@rollup/rollup-win32-arm64-msvc": "4.60.1",
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
"@rollup/rollup-win32-x64-gnu": "4.60.1",
"@rollup/rollup-win32-x64-msvc": "4.60.1",
"fsevents": "~2.3.2"
}
},
"node_modules/sade": { "node_modules/sade": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@@ -1361,6 +1992,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svelte": { "node_modules/svelte": {
"version": "5.55.3", "version": "5.55.3",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.3.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.3.tgz",

View File

@@ -15,6 +15,7 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.57.0", "@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0",
"svelte": "^5.55.2", "svelte": "^5.55.2",

View File

@@ -1,456 +1,478 @@
<script lang="ts"> <script lang="ts">
import { categories, styles, getStylesByCategory, getIntensityConfig } from '$lib/styles'; import {
import type { Style, StyleCategory, ConversionResponse } from '$lib/types'; categories,
import LoadingModal from '$lib/components/LoadingModal.svelte'; styles,
getStylesByCategory,
getIntensityConfig,
} from "$lib/styles";
import type { Style, StyleCategory, ConversionResponse } from "$lib/types";
import LoadingModal from "$lib/components/LoadingModal.svelte";
let inputText = $state(''); let inputText = $state("");
let selectedCategoryId = $state(''); let selectedCategoryId = $state("");
let selectedStyleId = $state(''); let selectedStyleId = $state("");
let intensity = $state(3); let intensity = $state(3);
let outputText = $state(''); let outputText = $state("");
let loading = $state(false); let loading = $state(false);
let error = $state(''); let error = $state("");
let systemPrompt = $state(''); let systemPrompt = $state("");
let userMessage = $state(''); let userMessage = $state("");
let modelName = $state(''); let modelName = $state("");
let showPrompt = $state(false); let showPrompt = $state(false);
let copied = $state(false); let copied = $state(false);
let availableStyles = $derived( let availableStyles = $derived(
selectedCategoryId ? getStylesByCategory(selectedCategoryId) : [] selectedCategoryId ? getStylesByCategory(selectedCategoryId) : [],
); );
let canConvert = $derived( let canConvert = $derived(
inputText.trim().length > 0 && selectedStyleId.length > 0 && !loading inputText.trim().length > 0 && selectedStyleId.length > 0 && !loading,
); );
let intensityLabel = $derived(getIntensityConfig(intensity)?.label ?? ''); let intensityLabel = $derived(getIntensityConfig(intensity)?.label ?? "");
function onCategoryChange() { function onCategoryChange() {
selectedStyleId = ''; selectedStyleId = "";
if (availableStyles.length === 1) { if (availableStyles.length === 1) {
selectedStyleId = availableStyles[0].id; selectedStyleId = availableStyles[0].id;
} }
} }
async function handleConvert() { async function handleConvert() {
if (!canConvert) return; if (!canConvert) return;
loading = true; loading = true;
error = ''; error = "";
outputText = ''; outputText = "";
systemPrompt = ''; systemPrompt = "";
userMessage = ''; userMessage = "";
modelName = ''; modelName = "";
showPrompt = false; showPrompt = false;
try { try {
const res = await fetch('/api/convert', { const res = await fetch("/api/convert", {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
text: inputText, text: inputText,
styleId: selectedStyleId, styleId: selectedStyleId,
intensity intensity,
}) }),
}); });
const data = await res.json(); const data = await res.json();
if (!res.ok) { if (!res.ok) {
throw new Error(data.error || 'Conversion failed'); throw new Error(data.error || "Conversion failed");
} }
const result: ConversionResponse = data; const result: ConversionResponse = data;
outputText = result.converted; outputText = result.converted;
systemPrompt = result.systemPrompt; systemPrompt = result.systemPrompt;
userMessage = result.userMessage; userMessage = result.userMessage;
modelName = result.model; modelName = result.model;
} catch (err) { } catch (err) {
error = err instanceof Error ? err.message : 'Something went wrong'; error = err instanceof Error ? err.message : "Something went wrong";
} finally { } finally {
loading = false; loading = false;
} }
} }
async function handleCopy() { async function handleCopy() {
try { try {
await navigator.clipboard.writeText(outputText); await navigator.clipboard.writeText(outputText);
copied = true; copied = true;
setTimeout(() => (copied = false), 2000); setTimeout(() => (copied = false), 2000);
} catch { } catch {
// Fallback: select text // Fallback: select text
const textarea = document.querySelector('.output-text'); const textarea = document.querySelector(".output-text");
if (textarea instanceof HTMLElement) { if (textarea instanceof HTMLElement) {
const range = document.createRange(); const range = document.createRange();
range.selectNodeContents(textarea); range.selectNodeContents(textarea);
const sel = window.getSelection(); const sel = window.getSelection();
sel?.removeAllRanges(); sel?.removeAllRanges();
sel?.addRange(range); sel?.addRange(range);
} }
} }
} }
</script> </script>
<main class="container"> <main class="container">
<h1 class="title">English Style Converter</h1> <h1 class="title">English Style Converter</h1>
<p class="subtitle">Transform your text into different English styles and tones</p> <p class="subtitle">
Transform your text into different English styles and tones
</p>
<div class="card"> <div class="card">
<div class="form-group"> <div class="form-group">
<label for="input-text">Your Text</label> <label for="input-text">Your Text</label>
<textarea <textarea
id="input-text" id="input-text"
bind:value={inputText} bind:value={inputText}
placeholder="Enter the English text you want to convert..." placeholder="Enter the English text you want to convert... DO NOT ENTER ANY PERSONAL INFORMATION!!"
rows="5" rows="5"
disabled={loading} disabled={loading}
></textarea> ></textarea>
</div> </div>
<div class="selectors"> <div class="selectors">
<div class="form-group"> <div class="form-group">
<label for="category">Style Category</label> <label for="category">Style Category</label>
<select <select
id="category" id="category"
bind:value={selectedCategoryId} bind:value={selectedCategoryId}
onchange={onCategoryChange} onchange={onCategoryChange}
disabled={loading} disabled={loading}
> >
<option value="">Choose a category...</option> <option value="">Choose a category...</option>
{#each categories as cat} {#each categories as cat}
<option value={cat.id}>{cat.emoji} {cat.label}</option> <option value={cat.id}>{cat.emoji} {cat.label}</option>
{/each} {/each}
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="style">Style</label> <label for="style">Style</label>
<select id="style" bind:value={selectedStyleId} disabled={loading || !selectedCategoryId}> <select
{#if !selectedCategoryId} id="style"
<option value="">Select a category first...</option> bind:value={selectedStyleId}
{:else if availableStyles.length === 0} disabled={loading || !selectedCategoryId}
<option value="">No styles available</option> >
{:else} {#if !selectedCategoryId}
<option value="">Choose a style...</option> <option value="">Select a category first...</option>
{#each availableStyles as style} {:else if availableStyles.length === 0}
<option value={style.id}>{style.label}</option> <option value="">No styles available</option>
{/each} {:else}
{/if} <option value="">Choose a style...</option>
</select> {#each availableStyles as style}
</div> <option value={style.id}>{style.label}</option>
</div> {/each}
{/if}
</select>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label for="intensity"> <label for="intensity">
Intensity: <span class="intensity-label">{intensityLabel || 'Strong'}</span> Intensity: <span class="intensity-label"
</label> >{intensityLabel || "Strong"}</span
<div class="slider-row"> >
<span class="slider-end">Subtle</span> </label>
<input <div class="slider-row">
id="intensity" <span class="slider-end">Subtle</span>
type="range" <input
min="1" id="intensity"
max="5" type="range"
step="1" min="1"
bind:value={intensity} max="5"
disabled={loading} step="1"
/> bind:value={intensity}
<span class="slider-end">Maximum</span> disabled={loading}
</div> />
</div> <span class="slider-end">Maximum</span>
</div>
</div>
<button class="convert-btn" onclick={handleConvert} disabled={!canConvert}> <button
{#if loading} class="convert-btn"
Converting... onclick={handleConvert}
{:else} disabled={!canConvert}
✨ Convert >
{/if} {#if loading}
</button> Converting...
</div> {:else}
✨ Convert
{/if}
</button>
</div>
{#if error} {#if error}
<div class="output-card error-card"> <div class="output-card error-card">
<p class="error-text">⚠️ {error}</p> <p class="error-text">⚠️ {error}</p>
</div> </div>
{/if} {/if}
{#if outputText} {#if outputText}
<div class="output-card"> <div class="output-card">
<div class="output-header"> <div class="output-header">
<h3>Result</h3> <h3>Result</h3>
<button class="copy-btn" onclick={handleCopy}> <button class="copy-btn" onclick={handleCopy}>
{#if copied} {#if copied}
✓ Copied! ✓ Copied!
{:else} {:else}
📋 Copy 📋 Copy
{/if} {/if}
</button> </button>
</div> </div>
<div class="output-text">{outputText}</div> <div class="output-text">{outputText}</div>
{#if modelName} {#if modelName}
<p class="model-attribution">Responded by {modelName}</p> <p class="model-attribution">Responded by {modelName}</p>
{/if} {/if}
</div> </div>
<div class="prompt-section"> <div class="prompt-section">
<button class="prompt-toggle" onclick={() => (showPrompt = !showPrompt)}> <button
{showPrompt ? '▼' : '▶'} Show prompt class="prompt-toggle"
</button> onclick={() => (showPrompt = !showPrompt)}
{#if showPrompt} >
<div class="prompt-content"> {showPrompt ? "▼" : "▶"} Show prompt
<div class="prompt-block"> </button>
<h4>System Prompt</h4> {#if showPrompt}
<pre>{systemPrompt}</pre> <div class="prompt-content">
</div> <div class="prompt-block">
<div class="prompt-block"> <h4>System Prompt</h4>
<h4>User Message</h4> <pre>{systemPrompt}</pre>
<pre>{userMessage}</pre> </div>
</div> <div class="prompt-block">
</div> <h4>User Message</h4>
{/if} <pre>{userMessage}</pre>
</div> </div>
{/if} </div>
{/if}
</div>
{/if}
</main> </main>
{#if loading} {#if loading}
<LoadingModal /> <LoadingModal />
{/if} {/if}
<style> <style>
.container { .container {
max-width: 680px; max-width: 680px;
margin: 0 auto; margin: 0 auto;
padding: 2rem 1.5rem; padding: 2rem 1.5rem;
min-height: 100vh; min-height: 100vh;
} }
.title { .title {
font-size: 2rem; font-size: 2rem;
font-weight: 800; font-weight: 800;
color: #1f2937; color: #1f2937;
text-align: center; text-align: center;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.subtitle { .subtitle {
text-align: center; text-align: center;
color: #6b7280; color: #6b7280;
margin-bottom: 2rem; margin-bottom: 2rem;
font-size: 1.05rem; font-size: 1.05rem;
} }
.card { .card {
background: #ffffff; background: #ffffff;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
border-radius: 12px; border-radius: 12px;
padding: 1.5rem; padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
} }
.form-group { .form-group {
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
.form-group label { .form-group label {
display: block; display: block;
font-weight: 600; font-weight: 600;
font-size: 0.9rem; font-size: 0.9rem;
color: #374151; color: #374151;
margin-bottom: 0.4rem; margin-bottom: 0.4rem;
} }
textarea, textarea,
select { select {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.75rem;
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
border-radius: 8px; border-radius: 8px;
font-size: 0.95rem; font-size: 0.95rem;
font-family: inherit; font-family: inherit;
background: #fafafa; background: #fafafa;
color: #1f2937; color: #1f2937;
transition: border-color 0.2s; transition: border-color 0.2s;
} }
textarea:focus, textarea:focus,
select:focus { select:focus {
outline: none; outline: none;
border-color: #3b82f6; border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
} }
textarea { textarea {
resize: vertical; resize: vertical;
min-height: 100px; min-height: 100px;
} }
.selectors { .selectors {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 1rem; gap: 1rem;
} }
.intensity-label { .intensity-label {
color: #3b82f6; color: #3b82f6;
font-weight: 700; font-weight: 700;
} }
.slider-row { .slider-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
} }
.slider-end { .slider-end {
font-size: 0.8rem; font-size: 0.8rem;
color: #9ca3af; color: #9ca3af;
white-space: nowrap; white-space: nowrap;
} }
input[type='range'] { input[type="range"] {
flex: 1; flex: 1;
} }
.convert-btn { .convert-btn {
width: 100%; width: 100%;
padding: 0.85rem; padding: 0.85rem;
background: #3b82f6; background: #3b82f6;
color: #ffffff; color: #ffffff;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
font-size: 1.05rem; font-size: 1.05rem;
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
transition: background 0.2s, opacity 0.2s; transition:
} background 0.2s,
opacity 0.2s;
}
.convert-btn:hover:not(:disabled) { .convert-btn:hover:not(:disabled) {
background: #2563eb; background: #2563eb;
} }
.convert-btn:disabled { .convert-btn:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }
.output-card { .output-card {
margin-top: 1.5rem; margin-top: 1.5rem;
background: #ffffff; background: #ffffff;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
border-radius: 12px; border-radius: 12px;
padding: 1.25rem; padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
} }
.error-card { .error-card {
border-color: #fca5a5; border-color: #fca5a5;
background: #fef2f2; background: #fef2f2;
} }
.error-text { .error-text {
color: #dc2626; color: #dc2626;
font-weight: 500; font-weight: 500;
} }
.output-header { .output-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
.output-header h3 { .output-header h3 {
margin: 0; margin: 0;
font-size: 1.1rem; font-size: 1.1rem;
color: #1f2937; color: #1f2937;
} }
.copy-btn { .copy-btn {
padding: 0.35rem 0.75rem; padding: 0.35rem 0.75rem;
background: #f3f4f6; background: #f3f4f6;
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
border-radius: 6px; border-radius: 6px;
font-size: 0.85rem; font-size: 0.85rem;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
} }
.copy-btn:hover { .copy-btn:hover {
background: #e5e7eb; background: #e5e7eb;
} }
.output-text { .output-text {
white-space: pre-wrap; white-space: pre-wrap;
line-height: 1.6; line-height: 1.6;
color: #1f2937; color: #1f2937;
font-size: 1rem; font-size: 1rem;
} }
.model-attribution { .model-attribution {
margin-top: 0.75rem; margin-top: 0.75rem;
font-size: 0.8rem; font-size: 0.8rem;
color: #9ca3af; color: #9ca3af;
font-style: italic; font-style: italic;
} }
.prompt-section { .prompt-section {
margin-top: 1rem; margin-top: 1rem;
} }
.prompt-toggle { .prompt-toggle {
background: none; background: none;
border: none; border: none;
color: #3b82f6; color: #3b82f6;
font-size: 0.9rem; font-size: 0.9rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
padding: 0.5rem 0; padding: 0.5rem 0;
} }
.prompt-toggle:hover { .prompt-toggle:hover {
text-decoration: underline; text-decoration: underline;
} }
.prompt-content { .prompt-content {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
.prompt-block { .prompt-block {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.prompt-block h4 { .prompt-block h4 {
font-size: 0.85rem; font-size: 0.85rem;
color: #6b7280; color: #6b7280;
margin-bottom: 0.4rem; margin-bottom: 0.4rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
.prompt-block pre { .prompt-block pre {
background: #f9fafb; background: #f9fafb;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
border-radius: 8px; border-radius: 8px;
padding: 0.75rem; padding: 0.75rem;
font-size: 0.85rem; font-size: 0.85rem;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
color: #374151; color: #374151;
margin: 0; margin: 0;
} }
@media (max-width: 600px) { @media (max-width: 600px) {
.selectors { .selectors {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.container { .container {
padding: 1rem; padding: 1rem;
} }
.title { .title {
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
</style> </style>

View File

@@ -1,15 +1,11 @@
import adapter from '@sveltejs/adapter-auto'; import adapter from '@sveltejs/adapter-node';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
compilerOptions: { compilerOptions: {
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
}, },
kit: { kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter() adapter: adapter()
} }
}; };