Compare commits
9 Commits
792fafc661
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f9c8b7551 | |||
| 204edafa40 | |||
| 4a783f28a1 | |||
| c96d97e154 | |||
| eaa1544e66 | |||
| 70dc396fe3 | |||
| 44f024f1d5 | |||
| 86d399a04b | |||
| ca583ea6c9 |
24
.env.docker
24
.env.docker
@@ -1,3 +1,21 @@
|
|||||||
# Model to use with Ollama — change this to any Ollama-compatible model
|
# ──────────────────────────────────────────────
|
||||||
# Examples: llama3, llama3.1, llama3.2, gemma2, mistral, phi3, qwen2, codellama
|
# English Style Converter — Docker Configuration
|
||||||
OLLAMA_MODEL=llama3
|
# ──────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Copy this file to .env and uncomment the profile you want.
|
||||||
|
# Or just set the three OPENAI_* vars for any provider.
|
||||||
|
|
||||||
|
# ── Option A: Ollama (local, free) ────────────
|
||||||
|
# Run: docker compose --profile ollama up
|
||||||
|
# OPENAI_BASE_URL=http://ollama:11434/v1
|
||||||
|
# OPENAI_API_KEY=ollama
|
||||||
|
# OLLAMA_MODEL=llama3
|
||||||
|
# OPENAI_MODEL=${OLLAMA_MODEL}
|
||||||
|
|
||||||
|
# ── Option B: OpenRouter (cloud, free tier) ───
|
||||||
|
OPENAI_BASE_URL=https://openrouter.ai/api/v1
|
||||||
|
OPENAI_API_KEY=sk-or-v1-YOUR_KEY_HERE
|
||||||
|
OPENAI_MODEL=openrouter/free
|
||||||
|
|
||||||
|
# ── Port mapping ──────────────────────────────
|
||||||
|
APP_PORT=5656
|
||||||
27
AGENTS.md
Normal file
27
AGENTS.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Short map. Read only what task needs.
|
||||||
|
|
||||||
|
## Core
|
||||||
|
- App: SvelteKit + TypeScript + Vitest.
|
||||||
|
- Keep changes small. Match existing style. No extra deps unless needed.
|
||||||
|
- Prefer repo docs over guessing.
|
||||||
|
|
||||||
|
## Read on demand
|
||||||
|
- Product / setup: `README.md`
|
||||||
|
- Design: `docs/superpowers/specs/2025-04-12-english-style-converter-design.md`
|
||||||
|
- Build plan: `docs/superpowers/plans/2025-04-12-english-style-converter.md`
|
||||||
|
- Style data: `src/lib/styles.ts`
|
||||||
|
- LLM + prompts: `src/lib/llm.ts`
|
||||||
|
- API: `src/routes/api/convert/+server.ts`
|
||||||
|
- UI: `src/routes/+page.svelte`
|
||||||
|
- Shared types: `src/lib/types.ts`
|
||||||
|
- Component UI bits: `src/lib/components/*`
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
- Use existing `npm test` / `npm run check`.
|
||||||
|
- Add or update tests near touched code.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Server-only secrets stay in env vars.
|
||||||
|
- Do not leak private prompt / defense details into UI.
|
||||||
13
Dockerfile
13
Dockerfile
@@ -3,22 +3,23 @@ FROM node:22-alpine AS build
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json ./
|
||||||
RUN npm ci
|
RUN npm i
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
# Prune dev dependencies for the production image
|
||||||
|
RUN npm prune --omit=dev
|
||||||
|
|
||||||
# ---- Run stage ----
|
# ---- Run stage ----
|
||||||
FROM node:22-alpine AS run
|
FROM node:22-alpine AS run
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY --from=build /app/package.json ./
|
||||||
RUN npm ci --omit=dev
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
|
|
||||||
COPY --from=build /app/build ./build
|
COPY --from=build /app/build ./build
|
||||||
COPY --from=build /app/package.json .
|
|
||||||
|
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
ollama:
|
ollama:
|
||||||
image: ollama/ollama:latest
|
image: ollama/ollama:latest
|
||||||
|
profiles:
|
||||||
|
- ollama
|
||||||
container_name: english-styler-ollama
|
container_name: english-styler-ollama
|
||||||
ports:
|
ports:
|
||||||
- "11434:11434"
|
- "11434:11434"
|
||||||
@@ -16,6 +18,8 @@ services:
|
|||||||
|
|
||||||
model-init:
|
model-init:
|
||||||
image: ollama/ollama:latest
|
image: ollama/ollama:latest
|
||||||
|
profiles:
|
||||||
|
- ollama
|
||||||
container_name: english-styler-model-init
|
container_name: english-styler-model-init
|
||||||
depends_on:
|
depends_on:
|
||||||
ollama:
|
ollama:
|
||||||
@@ -34,14 +38,14 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
container_name: english-styler-app
|
container_name: english-styler-app
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "${APP_PORT:-5656}:3000"
|
||||||
depends_on:
|
env_file:
|
||||||
model-init:
|
- path: .env
|
||||||
condition: service_completed_successfully
|
required: false
|
||||||
environment:
|
environment:
|
||||||
OPENAI_BASE_URL: http://ollama:11434/v1
|
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-http://ollama:11434/v1}
|
||||||
OPENAI_API_KEY: ollama
|
OPENAI_API_KEY: ${OPENAI_API_KEY:-ollama}
|
||||||
OPENAI_MODEL: ${OLLAMA_MODEL:-llama3}
|
OPENAI_MODEL: ${OPENAI_MODEL:-llama3}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
30
package-lock.json
generated
30
package-lock.json
generated
@@ -19,6 +19,29 @@
|
|||||||
"vitest": "^4.1.3"
|
"vitest": "^4.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/core": {
|
||||||
|
"version": "1.9.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.9.2.tgz",
|
||||||
|
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.1",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.9.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
||||||
|
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
@@ -899,7 +922,6 @@
|
|||||||
"integrity": "sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw==",
|
"integrity": "sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||||
@@ -942,7 +964,6 @@
|
|||||||
"integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==",
|
"integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
@@ -1133,7 +1154,6 @@
|
|||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -1886,7 +1906,6 @@
|
|||||||
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -2011,7 +2030,6 @@
|
|||||||
"integrity": "sha512-dS1N+i3bA1v+c4UDb750MlN5vCO82G6vxh8HeTsPsTdJ1BLsN1zxSyDlIdBBqUjqZ/BxEwM8UrFf98aaoVnZFQ==",
|
"integrity": "sha512-dS1N+i3bA1v+c4UDb750MlN5vCO82G6vxh8HeTsPsTdJ1BLsN1zxSyDlIdBBqUjqZ/BxEwM8UrFf98aaoVnZFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.4",
|
"@jridgewell/remapping": "^2.3.4",
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
@@ -2126,7 +2144,6 @@
|
|||||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -2141,7 +2158,6 @@
|
|||||||
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
|
|||||||
5
src/app.d.ts
vendored
5
src/app.d.ts
vendored
@@ -8,6 +8,11 @@ declare global {
|
|||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Umami analytics tracker (injected by script tag)
|
||||||
|
const umami: {
|
||||||
|
track(event: string, data?: Record<string, string | number | boolean>): void;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export interface ConvertResult {
|
|||||||
converted: string;
|
converted: string;
|
||||||
publicSystemPrompt: string;
|
publicSystemPrompt: string;
|
||||||
publicUserMessage: string;
|
publicUserMessage: string;
|
||||||
model: string;
|
requestedModel: string;
|
||||||
|
actualModel: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const INPUT_TAG_START = '###### USER INPUT START ######';
|
const INPUT_TAG_START = '###### USER INPUT START ######';
|
||||||
@@ -100,10 +101,15 @@ export async function convertText(
|
|||||||
throw new Error('LLM returned empty response');
|
throw new Error('LLM returned empty response');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenRouter's free router returns the actual model used in data.model
|
||||||
|
// e.g. requested "openrouter/free" -> actual "upstage/solar-pro-3:free"
|
||||||
|
const actualModel = (typeof data.model === 'string' && data.model !== merged.model) ? data.model : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
converted,
|
converted,
|
||||||
publicSystemPrompt: buildPublicSystemPrompt(styleModifier, intensityInstruction),
|
publicSystemPrompt: buildPublicSystemPrompt(styleModifier, intensityInstruction),
|
||||||
publicUserMessage: text,
|
publicUserMessage: text,
|
||||||
model: merged.model
|
requestedModel: merged.model,
|
||||||
|
actualModel
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -57,6 +57,11 @@ describe('getStylesByCategory', () => {
|
|||||||
const result = getStylesByCategory('general');
|
const result = getStylesByCategory('general');
|
||||||
expect(result.length).toBe(6);
|
expect(result.length).toBe(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fun category has 7 styles', () => {
|
||||||
|
const result = getStylesByCategory('fun');
|
||||||
|
expect(result.length).toBe(7);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getStyleById', () => {
|
describe('getStyleById', () => {
|
||||||
@@ -70,6 +75,11 @@ describe('getStyleById', () => {
|
|||||||
const result = getStyleById('nonexistent');
|
const result = getStyleById('nonexistent');
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns new fun styles by id', () => {
|
||||||
|
expect(getStyleById('corporate-bullshit')).toBeDefined();
|
||||||
|
expect(getStyleById('senator-john-kennedy')).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getCategoryById', () => {
|
describe('getCategoryById', () => {
|
||||||
|
|||||||
@@ -157,6 +157,34 @@ export const styles: Style[] = [
|
|||||||
promptModifier:
|
promptModifier:
|
||||||
"Rewrite in Gen Z slang with no cap, fr, and modern internet vernacular",
|
"Rewrite in Gen Z slang with no cap, fr, and modern internet vernacular",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "corporate-bullshit",
|
||||||
|
label: "Corporate Bullshit",
|
||||||
|
categoryId: "fun",
|
||||||
|
promptModifier:
|
||||||
|
"Rewrite in polished corporate-speak loaded with buzzwords, euphemisms, and strategic vagueness. Use phrases like circle back, align on priorities, move the needle, best-in-class, leverage synergies, and stakeholder alignment. Prefer passive voice, meeting-room tone, and language that sounds important while saying as little as possible. Keep it coherent enough to pass as a real update, but inflate every plain statement into consultant-grade business jargon.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "caveman",
|
||||||
|
label: "Caveman",
|
||||||
|
categoryId: "fun",
|
||||||
|
promptModifier:
|
||||||
|
"Rewrite as a neanderthal caveman would speak — use broken grammar, grunt words like ugh and oog, refer to things by their simplest names like big rock and fire stick, shorten every word, drop articles and verb conjugations entirely, think only about food, shelter, and danger, and express emotions through raw outbursts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tharoorian",
|
||||||
|
label: "Tharoorian English",
|
||||||
|
categoryId: "fun",
|
||||||
|
promptModifier:
|
||||||
|
"Rewrite in the unmistakable style of Shashi Tharoor — deploy sesquipedalian vocabulary with unapologetic ostentation, construct serpentine sentences brimming with subordinate clauses and appositives that cascade toward a devastatingly witty payoff, weave in literary allusions and historical references with the casual air of a man who reads dictionaries for recreation, employ dry Oxonian wit that lands with the precision of a surgeon's scalpel wrapped in a velvet epigram, favour the arcane over the accessible (perspicacious over smart, obfuscate over hide, floccinaucinihilipilification over dismissal), indulge in alliteration and rhetorical flourish, frame even the mundane as though addressing a hushed auditorium, punctuate with sardonic asides set off by em dashes, and never use a short word when a magnificently polysyllabic one will send the listener reaching for their Oxford dictionary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "senator-john-kennedy",
|
||||||
|
label: "Senator John Kennedy",
|
||||||
|
categoryId: "fun",
|
||||||
|
promptModifier:
|
||||||
|
"Rewrite as Senator John Kennedy of Louisiana. Voice: plainspoken, folksy, sharp, deadpan, and funny without sounding polished. Make the text sound like a witty Senate-floor quip or a TV interview zinger from a man who translates Washington nonsense into common-sense language. Use short punchy sentences, vivid Southern metaphors, comic comparisons, dry ridicule, and blunt moral judgments. Favor everyday imagery like steak, gumbo, crawfish, pickup trucks, church, a money tree, or something 'tougher than' whatever is being discussed. Lean into contrast between common people and Washington insiders. Keep the jokes clean, the timing crisp, and the tone slightly grumpy but good-natured. The result should feel quotable, homespun, and slyly insulting in a clever way. Do not copy exact famous lines; only evoke the style.",
|
||||||
|
},
|
||||||
|
|
||||||
// Game of Thrones
|
// Game of Thrones
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export interface ConversionResponse {
|
|||||||
intensity: number;
|
intensity: number;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
userMessage: string;
|
userMessage: string;
|
||||||
model: string;
|
modelLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LLMConfig {
|
export interface LLMConfig {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✨</text></svg>" />
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✨</text></svg>" />
|
||||||
|
<script defer src="https://wa.santhoshj.com/script.js" data-website-id="1004961a-2b1f-4c45-95a6-78059acac472"></script>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
@@ -1,38 +1,75 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { categories, getStylesByCategory, getIntensityConfig } from '$lib/styles';
|
||||||
categories,
|
import type { ConversionResponse } from '$lib/types';
|
||||||
styles,
|
import LoadingModal from '$lib/components/LoadingModal.svelte';
|
||||||
getStylesByCategory,
|
|
||||||
getIntensityConfig,
|
|
||||||
} from "$lib/styles";
|
|
||||||
import type { Style, StyleCategory, ConversionResponse } from "$lib/types";
|
|
||||||
import LoadingModal from "$lib/components/LoadingModal.svelte";
|
|
||||||
|
|
||||||
let inputText = $state("");
|
const STORAGE_KEY = 'english-styler-state';
|
||||||
let selectedCategoryId = $state("");
|
|
||||||
let selectedStyleId = $state("");
|
interface SavedState {
|
||||||
let intensity = $state(3);
|
inputText: string;
|
||||||
let outputText = $state("");
|
selectedCategoryId: string;
|
||||||
|
selectedStyleId: string;
|
||||||
|
intensity: number;
|
||||||
|
showPrompt: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadState(): SavedState | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
return JSON.parse(raw) as SavedState;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveState() {
|
||||||
|
try {
|
||||||
|
const state: SavedState = {
|
||||||
|
inputText,
|
||||||
|
selectedCategoryId,
|
||||||
|
selectedStyleId,
|
||||||
|
intensity,
|
||||||
|
showPrompt
|
||||||
|
};
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||||
|
} catch {
|
||||||
|
// localStorage may be unavailable (private browsing, etc.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saved = loadState();
|
||||||
|
|
||||||
|
let inputText = $state(saved?.inputText ?? '');
|
||||||
|
let selectedCategoryId = $state(saved?.selectedCategoryId ?? '');
|
||||||
|
let selectedStyleId = $state(saved?.selectedStyleId ?? '');
|
||||||
|
let intensity = $state(saved?.intensity ?? 3);
|
||||||
|
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 modelLabel = $state('');
|
||||||
let showPrompt = $state(false);
|
let showPrompt = $state(saved?.showPrompt ?? 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 ?? '');
|
||||||
|
|
||||||
|
// Persist state whenever it changes
|
||||||
|
$effect(() => {
|
||||||
|
saveState();
|
||||||
|
});
|
||||||
|
|
||||||
function onCategoryChange() {
|
function onCategoryChange() {
|
||||||
selectedStyleId = "";
|
selectedStyleId = '';
|
||||||
if (availableStyles.length === 1) {
|
if (availableStyles.length === 1) {
|
||||||
selectedStyleId = availableStyles[0].id;
|
selectedStyleId = availableStyles[0].id;
|
||||||
}
|
}
|
||||||
@@ -41,54 +78,69 @@
|
|||||||
async function handleConvert() {
|
async function handleConvert() {
|
||||||
if (!canConvert) return;
|
if (!canConvert) return;
|
||||||
|
|
||||||
|
if (typeof umami !== 'undefined') {
|
||||||
|
umami.track('convert_click', { style: selectedStyleId, intensity });
|
||||||
|
}
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
error = "";
|
error = '';
|
||||||
outputText = "";
|
outputText = '';
|
||||||
systemPrompt = "";
|
systemPrompt = '';
|
||||||
userMessage = "";
|
userMessage = '';
|
||||||
modelName = "";
|
modelLabel = '';
|
||||||
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;
|
modelLabel = result.modelLabel;
|
||||||
|
|
||||||
|
if (typeof umami !== 'undefined') {
|
||||||
|
umami.track('convert_success', { style: selectedStyleId, intensity, model: result.modelLabel });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err instanceof Error ? err.message : "Something went wrong";
|
error = err instanceof Error ? err.message : 'Something went wrong';
|
||||||
|
|
||||||
|
if (typeof umami !== 'undefined') {
|
||||||
|
umami.track('convert_error', { style: selectedStyleId, intensity, error });
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCopy() {
|
async function handleCopy() {
|
||||||
|
if (typeof umami !== 'undefined') {
|
||||||
|
umami.track('copy_result', { style: selectedStyleId });
|
||||||
|
}
|
||||||
|
|
||||||
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
|
const el = document.querySelector('.output-text');
|
||||||
const textarea = document.querySelector(".output-text");
|
if (el instanceof HTMLElement) {
|
||||||
if (textarea instanceof HTMLElement) {
|
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.selectNodeContents(textarea);
|
range.selectNodeContents(el);
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel?.removeAllRanges();
|
sel?.removeAllRanges();
|
||||||
sel?.addRange(range);
|
sel?.addRange(range);
|
||||||
@@ -99,9 +151,7 @@
|
|||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<h1 class="title">English Style Converter</h1>
|
<h1 class="title">English Style Converter</h1>
|
||||||
<p class="subtitle">
|
<p class="subtitle">Transform your text into different English styles and tones</p>
|
||||||
Transform your text into different English styles and tones
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -109,7 +159,7 @@
|
|||||||
<textarea
|
<textarea
|
||||||
id="input-text"
|
id="input-text"
|
||||||
bind:value={inputText}
|
bind:value={inputText}
|
||||||
placeholder="Enter the English text you want to convert... DO NOT ENTER ANY PERSONAL INFORMATION!!"
|
placeholder="Enter the English text you want to convert..."
|
||||||
rows="5"
|
rows="5"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
></textarea>
|
></textarea>
|
||||||
@@ -123,6 +173,7 @@
|
|||||||
bind:value={selectedCategoryId}
|
bind:value={selectedCategoryId}
|
||||||
onchange={onCategoryChange}
|
onchange={onCategoryChange}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
data-umami-event="select_category"
|
||||||
>
|
>
|
||||||
<option value="">Choose a category...</option>
|
<option value="">Choose a category...</option>
|
||||||
{#each categories as cat}
|
{#each categories as cat}
|
||||||
@@ -133,11 +184,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="style">Style</label>
|
<label for="style">Style</label>
|
||||||
<select
|
<select id="style" bind:value={selectedStyleId} disabled={loading || !selectedCategoryId} data-umami-event="select_style">
|
||||||
id="style"
|
|
||||||
bind:value={selectedStyleId}
|
|
||||||
disabled={loading || !selectedCategoryId}
|
|
||||||
>
|
|
||||||
{#if !selectedCategoryId}
|
{#if !selectedCategoryId}
|
||||||
<option value="">Select a category first...</option>
|
<option value="">Select a category first...</option>
|
||||||
{:else if availableStyles.length === 0}
|
{:else if availableStyles.length === 0}
|
||||||
@@ -154,9 +201,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="intensity">
|
<label for="intensity">
|
||||||
Intensity: <span class="intensity-label"
|
Intensity: <span class="intensity-label">{intensityLabel || 'Strong'}</span>
|
||||||
>{intensityLabel || "Strong"}</span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
<div class="slider-row">
|
<div class="slider-row">
|
||||||
<span class="slider-end">Subtle</span>
|
<span class="slider-end">Subtle</span>
|
||||||
@@ -168,16 +213,18 @@
|
|||||||
step="1"
|
step="1"
|
||||||
bind:value={intensity}
|
bind:value={intensity}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
data-umami-event="adjust_intensity"
|
||||||
/>
|
/>
|
||||||
<span class="slider-end">Maximum</span>
|
<span class="slider-end">Maximum</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div class="disclaimer">
|
||||||
class="convert-btn"
|
<span class="disclaimer-icon">⚡</span>
|
||||||
onclick={handleConvert}
|
<span>This tool uses AI to generate styled text. Results may be inaccurate, biased, or unexpected. Do not enter personal or sensitive information. Use at your own discretion — this is a demo and outputs should not be relied upon for important purposes.</span>
|
||||||
disabled={!canConvert}
|
</div>
|
||||||
>
|
|
||||||
|
<button class="convert-btn" onclick={handleConvert} disabled={!canConvert}>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
Converting...
|
Converting...
|
||||||
{:else}
|
{:else}
|
||||||
@@ -205,17 +252,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="output-text">{outputText}</div>
|
<div class="output-text">{outputText}</div>
|
||||||
{#if modelName}
|
{#if modelLabel}
|
||||||
<p class="model-attribution">Responded by {modelName}</p>
|
<p class="model-attribution">Responded by {modelLabel}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="prompt-section">
|
<div class="prompt-section">
|
||||||
<button
|
<button class="prompt-toggle" onclick={() => (showPrompt = !showPrompt)} data-umami-event="toggle_prompt" data-umami-event-action={showPrompt ? 'close' : 'open'}>
|
||||||
class="prompt-toggle"
|
{showPrompt ? '▼' : '▶'} Show prompt
|
||||||
onclick={() => (showPrompt = !showPrompt)}
|
|
||||||
>
|
|
||||||
{showPrompt ? "▼" : "▶"} Show prompt
|
|
||||||
</button>
|
</button>
|
||||||
{#if showPrompt}
|
{#if showPrompt}
|
||||||
<div class="prompt-content">
|
<div class="prompt-content">
|
||||||
@@ -328,10 +372,30 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="range"] {
|
input[type='range'] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disclaimer {
|
||||||
|
background: #fff8ed;
|
||||||
|
border: 1px solid #f6c96a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #8b6914;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclaimer-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.convert-btn {
|
.convert-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
@@ -342,9 +406,7 @@
|
|||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition: background 0.2s, opacity 0.2s;
|
||||||
background 0.2s,
|
|
||||||
opacity 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.convert-btn:hover:not(:disabled) {
|
.convert-btn:hover:not(:disabled) {
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||||||
intensity,
|
intensity,
|
||||||
systemPrompt: result.publicSystemPrompt,
|
systemPrompt: result.publicSystemPrompt,
|
||||||
userMessage: result.publicUserMessage,
|
userMessage: result.publicUserMessage,
|
||||||
model: result.model
|
modelLabel: result.actualModel
|
||||||
|
? `${result.actualModel} model from ${result.requestedModel}`
|
||||||
|
: result.requestedModel
|
||||||
};
|
};
|
||||||
|
|
||||||
return json(response);
|
return json(response);
|
||||||
|
|||||||
Reference in New Issue
Block a user