Jargonify/index.html
Richard Nixon 0e9f6952b4 feat: add LinkedIn jargon translator web app
Single-page interactive translator that converts plain language into
LinkedIn-style corporate posts and decodes them back to human speak.

- Two translation modes: to LinkedIn and to Human
- Cringe intensity slider (1-5)
- 3 variation cards per translation with copy buttons
- Dual engine support: direct API key or local proxy server
- i18n toggle for Portuguese and English (UI + AI prompts)
- Dark theme with LinkedIn blue accent, fully responsive
- Local proxy server (server.js) bridges to the LLM CLI
2026-03-19 22:56:50 +00:00

1028 lines
30 KiB
HTML

<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>">
<title>Jargonify — Tradutor LinkedIn</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=DM+Serif+Display&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0d0d0f;
--surface: #161619;
--surface-hover: #1e1e22;
--border: #2a2a2f;
--text: #e8e6e3;
--text-muted: #8a8a8f;
--accent: #0077B5;
--accent-glow: #0077B540;
--accent-light: #00a0dc;
--danger: #ff4757;
--success: #2ed573;
--radius: 12px;
--radius-lg: 16px;
}
body {
font-family: 'Space Grotesk', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
}
/* Header */
header {
padding: 2rem 2rem 1.2rem;
text-align: center;
position: relative;
}
header::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80%;
max-width: 600px;
height: 1px;
background: linear-gradient(90deg, transparent, var(--border), transparent);
}
.header-top {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.logo {
font-family: 'DM Serif Display', serif;
font-size: 2.5rem;
letter-spacing: -1px;
background: linear-gradient(135deg, var(--text), var(--accent-light));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
color: var(--text-muted);
font-size: 0.9rem;
margin-top: 0.3rem;
font-weight: 300;
letter-spacing: 2px;
text-transform: uppercase;
}
/* Language Toggle */
.lang-toggle {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 20px;
padding: 0.3rem;
cursor: pointer;
transition: border-color 0.2s;
}
.lang-toggle:hover { border-color: var(--accent); }
.lang-flag {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
transition: all 0.25s;
opacity: 0.4;
}
.lang-flag.active {
opacity: 1;
background: var(--accent-glow);
}
/* Engine Selector */
.engine-bar {
max-width: 480px;
margin: 1.2rem auto 0;
display: flex;
gap: 0;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.engine-option {
flex: 1;
padding: 0.6rem 1rem;
font-family: inherit;
font-size: 0.8rem;
font-weight: 500;
color: var(--text-muted);
background: transparent;
border: none;
cursor: pointer;
transition: all 0.25s;
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
}
.engine-option:hover {
color: var(--text);
background: var(--surface-hover);
}
.engine-option.active {
color: var(--accent-light);
background: var(--accent-glow);
}
.engine-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-muted);
transition: background 0.25s;
}
.engine-option.active .engine-dot { background: var(--success); }
.engine-config {
max-width: 480px;
margin: 0.6rem auto 0;
text-align: center;
}
.engine-hint {
font-size: 0.7rem;
color: var(--text-muted);
min-height: 1.2em;
margin-bottom: 0.4rem;
}
.api-key-row {
display: none;
gap: 0.5rem;
align-items: center;
}
.api-key-row.visible { display: flex; }
.api-key-row input {
flex: 1;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.5rem 0.8rem;
color: var(--text);
font-family: inherit;
font-size: 0.78rem;
outline: none;
transition: border-color 0.2s;
}
.api-key-row input:focus { border-color: var(--accent); }
.api-key-row input::placeholder { color: var(--text-muted); }
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--danger);
transition: background 0.3s;
flex-shrink: 0;
}
.status-dot.ok { background: var(--success); }
/* Main Layout */
main {
max-width: 1200px;
margin: 1.5rem auto;
padding: 0 1.5rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: start;
}
/* Toggle Mode */
.mode-toggle-wrapper {
grid-column: 1 / -1;
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.mode-label {
font-size: 0.85rem;
font-weight: 500;
color: var(--text-muted);
transition: color 0.3s;
min-width: 90px;
}
.mode-label.active { color: var(--accent-light); }
.mode-label:first-of-type { text-align: right; }
.mode-label:last-of-type { text-align: left; }
.toggle-track {
width: 64px;
height: 32px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
position: relative;
cursor: pointer;
transition: background 0.3s, border-color 0.3s;
flex-shrink: 0;
}
.toggle-track:hover { border-color: var(--accent); }
.toggle-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--accent);
position: absolute;
top: 3px;
left: 4px;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 0 8px var(--accent-glow);
}
.toggle-track.human .toggle-thumb { transform: translateX(32px); }
/* Panels */
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 1.5rem;
transition: border-color 0.3s;
}
.panel:hover { border-color: #3a3a40; }
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.panel-title {
font-family: 'DM Serif Display', serif;
font-size: 1.2rem;
color: var(--text);
}
.panel-badge {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--accent-light);
background: var(--accent-glow);
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-weight: 600;
}
/* Textarea */
textarea {
width: 100%;
min-height: 180px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
color: var(--text);
font-family: inherit;
font-size: 0.95rem;
line-height: 1.6;
resize: vertical;
outline: none;
transition: border-color 0.2s;
}
textarea:focus { border-color: var(--accent); }
textarea::placeholder { color: var(--text-muted); }
/* Slider */
.slider-section { margin-top: 1.2rem; }
.slider-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.6rem;
}
.slider-label {
font-size: 0.8rem;
color: var(--text-muted);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1px;
}
.slider-value {
font-size: 0.85rem;
font-weight: 600;
color: var(--accent-light);
}
.slider-descriptions {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
color: var(--text-muted);
margin-top: 0.4rem;
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 4px;
border-radius: 2px;
background: linear-gradient(90deg, var(--border), var(--accent));
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--accent-light);
border: 3px solid var(--bg);
box-shadow: 0 0 10px var(--accent-glow);
cursor: pointer;
transition: transform 0.2s;
}
input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.15); }
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--accent-light);
border: 3px solid var(--bg);
box-shadow: 0 0 10px var(--accent-glow);
cursor: pointer;
}
/* Translate Button */
.translate-btn {
width: 100%;
margin-top: 1.2rem;
padding: 0.9rem;
background: linear-gradient(135deg, var(--accent), var(--accent-light));
color: #fff;
border: none;
border-radius: var(--radius);
font-family: inherit;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
letter-spacing: 0.5px;
transition: opacity 0.2s, transform 0.1s;
overflow: hidden;
}
.translate-btn:hover:not(:disabled) { opacity: 0.9; }
.translate-btn:active:not(:disabled) { transform: scale(0.985); }
.translate-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Output */
.output-area {
min-height: 180px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 250px;
color: var(--text-muted);
text-align: center;
gap: 0.8rem;
}
.empty-state .icon { font-size: 2.5rem; opacity: 0.4; }
.empty-state p { font-size: 0.85rem; line-height: 1.5; }
/* Variation Card */
.variation-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.2rem;
animation: fadeSlideIn 0.4s ease both;
transition: border-color 0.2s;
}
.variation-card:hover { border-color: #3a3a40; }
.variation-card:nth-child(1) { animation-delay: 0s; }
.variation-card:nth-child(2) { animation-delay: 0.1s; }
.variation-card:nth-child(3) { animation-delay: 0.2s; }
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.8rem;
}
.card-number {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
font-weight: 600;
}
.copy-btn {
display: flex;
align-items: center;
gap: 0.4rem;
background: transparent;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-muted);
padding: 0.35rem 0.75rem;
font-family: inherit;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.copy-btn:hover { border-color: var(--accent); color: var(--accent-light); }
.copy-btn.copied { border-color: var(--success); color: var(--success); }
.card-text {
font-size: 0.92rem;
line-height: 1.7;
color: var(--text);
white-space: pre-wrap;
}
/* Loading */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 250px;
gap: 1.2rem;
}
.loading-dots { display: flex; gap: 6px; }
.loading-dots span {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
animation: bounce 1.4s infinite ease-in-out both;
}
.loading-dots span:nth-child(1) { animation-delay: -0.32s; }
.loading-dots span:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
.loading-text { font-size: 0.85rem; color: var(--text-muted); }
/* Error */
.error-msg {
background: #ff475720;
border: 1px solid #ff475740;
border-radius: var(--radius);
padding: 1rem;
color: var(--danger);
font-size: 0.85rem;
line-height: 1.5;
}
/* Footer */
footer {
text-align: center;
padding: 2rem 1rem;
color: var(--text-muted);
font-size: 0.75rem;
border-top: 1px solid var(--border);
margin-top: 2rem;
}
/* Responsive */
@media (max-width: 768px) {
main { grid-template-columns: 1fr; gap: 1rem; }
.logo { font-size: 1.8rem; }
header { padding: 1.5rem 1rem 1rem; }
.mode-label { min-width: 70px; font-size: 0.78rem; }
.engine-bar { flex-direction: column; }
.engine-option { padding: 0.5rem; }
.lang-toggle { position: static; transform: none; margin: 0.8rem auto 0; }
.header-top { flex-direction: column; gap: 0.5rem; }
}
</style>
</head>
<body>
<header>
<div class="header-top">
<div>
<h1 class="logo">Jargonify</h1>
<p class="subtitle" id="subtitle">Tradutor LinkedIn</p>
</div>
<div class="lang-toggle" onclick="toggleLang()">
<span class="lang-flag active" id="flagBr">🇧🇷</span>
<span class="lang-flag" id="flagUs">🇺🇸</span>
</div>
</div>
<div class="engine-bar">
<button class="engine-option" onclick="setEngine('api')" id="engineApi">
<span class="engine-dot"></span>
API Key
</button>
<button class="engine-option active" onclick="setEngine('proxy')" id="engineProxy">
<span class="engine-dot"></span>
<span id="engineProxyLabel">Servidor Local (Pro/Max)</span>
</button>
</div>
<div class="engine-config">
<div class="engine-hint" id="engineHint"></div>
<div class="api-key-row" id="apiKeyRow">
<span class="status-dot" id="apiDot"></span>
<input type="password" id="apiKeyInput" placeholder="" autocomplete="off" />
</div>
</div>
</header>
<main>
<div class="mode-toggle-wrapper">
<span class="mode-label active" id="labelLinkedin"></span>
<div class="toggle-track" id="toggleTrack" onclick="toggleMode()">
<div class="toggle-thumb"></div>
</div>
<span class="mode-label" id="labelHuman"></span>
</div>
<div class="panel">
<div class="panel-header">
<h2 class="panel-title" id="inputTitle"></h2>
<span class="panel-badge" id="inputBadge"></span>
</div>
<textarea id="inputText"></textarea>
<div class="slider-section" id="sliderSection">
<div class="slider-header">
<span class="slider-label" id="sliderLabel"></span>
<span class="slider-value" id="sliderValueLabel"></span>
</div>
<input type="range" id="intensitySlider" min="1" max="5" value="3" oninput="updateSliderLabel()" />
<div class="slider-descriptions">
<span id="sliderMin"></span>
<span id="sliderMax"></span>
</div>
</div>
<button class="translate-btn" id="translateBtn" onclick="doTranslate()"></button>
</div>
<div class="panel">
<div class="panel-header">
<h2 class="panel-title" id="outputTitle"></h2>
<span class="panel-badge" id="outputBadge"></span>
</div>
<div class="output-area" id="outputArea"></div>
</div>
</main>
<footer id="footerText"></footer>
<script>
// ─── i18n ───
var I18N = {
pt: {
subtitle: 'Tradutor LinkedIn',
engineProxy: 'Servidor Local (Pro/Max)',
engineHintProxy: 'Requer: node server.js rodando na porta 3000',
engineHintProxyOk: 'Servidor local conectado',
engineHintProxyFail: 'Servidor nao encontrado — rode: node server.js',
engineHintApi: 'Requer API key da Anthropic (console.anthropic.com)',
apiPlaceholder: 'Cole sua API key (sk-ant-...)',
labelLinkedin: '→ LinkedIn',
labelHuman: '→ Humano',
inputTitle: 'Texto Original',
inputBadge: 'entrada',
outputTitle: 'Resultado',
outputBadge: 'saida',
sliderLabel: 'Intensidade Cringe',
sliderMin: 'Profissional',
sliderMax: 'Guru Maximo',
intensity: {
1: '1 — Profissional Sobrio',
2: '2 — Corporativo Leve',
3: '3 — Classico LinkedIn',
4: '4 — Coach de Carreira',
5: '5 — Guru Motivacional Maximo'
},
btnLinkedin: 'Traduzir para LinkedIn',
btnHuman: 'Traduzir para Humano',
placeholderLinkedin: 'Ex: Eu fui demitido e estou procurando emprego...',
placeholderHuman: 'Cole aqui um post LinkedIn cheio de jargao...',
emptyIcon: '✍️',
emptyText: 'Escreva algo ao lado e clique em traduzir<br/>para ver a magia corporativa acontecer.',
loadingLinkedin: 'Ativando modo corporativo...',
loadingHuman: 'Decodificando jargao...',
labelsLinkedin: ['Variacao A', 'Variacao B', 'Variacao C'],
labelsHuman: ['Literal', 'Equilibrada', 'Ironica'],
copy: 'Copiar',
copied: 'Copiado!',
footer: 'Feito com ironia. Nenhum guru motivacional foi ferido.',
errorNoKey: 'Insira sua API key do Claude no campo acima.',
errorEmpty: 'Escreva algo para traduzir.',
errorServer: 'Nao foi possivel conectar ao servidor local. Rode: node server.js',
errorParse: 'Nenhuma variacao retornada. Tente novamente.',
promptLinkedin: function(text, intensity) {
return 'Voce e um tradutor de linguagem humana para "LinkedIn-es" (corporatives motivacional brasileiro).\n\n' +
'Regras:\n' +
'- Transforme a frase em um post no estilo LinkedIn\n' +
'- Use vocabulario tipico: jornada, ciclo, entregar valor, aprendizado, resiliencia, proposito, mindset, networking, soft skills, growth, ownership\n' +
'- Adicione hashtags relevantes no final (3 a 6)\n' +
'- Use emojis estrategicos\n' +
'- Nivel de intensidade: ' + intensity + '/5 (1 = profissional sobrio, 5 = guru motivacional maximo)\n' +
'- Gere exatamente 3 variacoes diferentes\n\n' +
'FORMATO: Escreva as 3 variacoes separadas pela linha exata: ---VARIACAO---\nNao use markdown. Nao numere. Apenas texto puro separado por ---VARIACAO---\n\n' +
'Frase: ' + text;
},
promptHuman: function(text) {
return 'Voce e um decodificador de "LinkedIn-es". Traduza posts corporativos para o que a pessoa REALMENTE quis dizer.\n\n' +
'Regras:\n' +
'- Seja direto, honesto e levemente ironico\n' +
'- Gere 3 variacoes: literal, equilibrada, ironica\n\n' +
'FORMATO: Escreva as 3 variacoes separadas pela linha exata: ---VARIACAO---\nNao use markdown. Nao numere. Apenas texto puro separado por ---VARIACAO---\n\n' +
'Post: ' + text;
}
},
en: {
subtitle: 'LinkedIn Translator',
engineProxy: 'Local Server (Pro/Max)',
engineHintProxy: 'Requires: node server.js running on port 3000',
engineHintProxyOk: 'Local server connected',
engineHintProxyFail: 'Server not found — run: node server.js',
engineHintApi: 'Requires Anthropic API key (console.anthropic.com)',
apiPlaceholder: 'Paste your API key (sk-ant-...)',
labelLinkedin: '→ LinkedIn',
labelHuman: '→ Human',
inputTitle: 'Original Text',
inputBadge: 'input',
outputTitle: 'Result',
outputBadge: 'output',
sliderLabel: 'Cringe Intensity',
sliderMin: 'Professional',
sliderMax: 'Max Guru',
intensity: {
1: '1 — Sober Professional',
2: '2 — Light Corporate',
3: '3 — Classic LinkedIn',
4: '4 — Career Coach',
5: '5 — Maximum Motivational Guru'
},
btnLinkedin: 'Translate to LinkedIn',
btnHuman: 'Translate to Human',
placeholderLinkedin: 'Ex: I got fired and I\'m looking for a job...',
placeholderHuman: 'Paste a LinkedIn post full of jargon here...',
emptyIcon: '✍️',
emptyText: 'Write something on the left and click translate<br/>to see the corporate magic happen.',
loadingLinkedin: 'Activating corporate mode...',
loadingHuman: 'Decoding jargon...',
labelsLinkedin: ['Variation A', 'Variation B', 'Variation C'],
labelsHuman: ['Literal', 'Balanced', 'Ironic'],
copy: 'Copy',
copied: 'Copied!',
footer: 'Made with irony. No motivational guru was harmed.',
errorNoKey: 'Enter your Claude API key in the field above.',
errorEmpty: 'Write something to translate.',
errorServer: 'Could not connect to local server. Run: node server.js',
errorParse: 'No variations returned. Try again.',
promptLinkedin: function(text, intensity) {
return 'You are a translator from plain human language to "LinkedIn-speak" (corporate motivational jargon).\n\n' +
'Rules:\n' +
'- Transform the phrase into a LinkedIn-style post\n' +
'- Use typical vocabulary: journey, cycle, deliver value, learnings, resilience, purpose, mindset, networking, soft skills, growth, ownership, accountability\n' +
'- Add relevant hashtags at the end (3 to 6)\n' +
'- Use strategic emojis\n' +
'- Cringe intensity level: ' + intensity + '/5 (1 = sober professional, 5 = maximum motivational guru)\n' +
'- Generate exactly 3 different variations\n\n' +
'FORMAT: Write the 3 variations separated by the exact line: ---VARIACAO---\nDo not use markdown. Do not number them. Just plain text separated by ---VARIACAO---\n\n' +
'Phrase: ' + text;
},
promptHuman: function(text) {
return 'You are a "LinkedIn-speak" decoder. Translate corporate motivational posts into what the person REALLY meant.\n\n' +
'Rules:\n' +
'- Be direct, honest and slightly ironic\n' +
'- Generate 3 variations: literal, balanced, ironic\n\n' +
'FORMAT: Write the 3 variations separated by the exact line: ---VARIACAO---\nDo not use markdown. Do not number them. Just plain text separated by ---VARIACAO---\n\n' +
'Post: ' + text;
}
}
};
// ─── State ───
var lang = 'pt';
var mode = 'linkedin';
var engine = 'proxy';
var SEP = '---VARIACAO---';
function t() { return I18N[lang]; }
function $(id) { return document.getElementById(id); }
// ─── Language Toggle ───
function toggleLang() {
lang = lang === 'pt' ? 'en' : 'pt';
$('flagBr').classList.toggle('active', lang === 'pt');
$('flagUs').classList.toggle('active', lang === 'en');
document.documentElement.lang = lang === 'pt' ? 'pt-BR' : 'en';
applyI18n();
}
function applyI18n() {
var s = t();
$('subtitle').textContent = s.subtitle;
$('engineProxyLabel').textContent = s.engineProxy;
$('apiKeyInput').placeholder = s.apiPlaceholder;
$('labelLinkedin').textContent = s.labelLinkedin;
$('labelHuman').textContent = s.labelHuman;
$('inputTitle').textContent = s.inputTitle;
$('inputBadge').textContent = s.inputBadge;
$('outputTitle').textContent = s.outputTitle;
$('outputBadge').textContent = s.outputBadge;
$('sliderLabel').textContent = s.sliderLabel;
$('sliderMin').textContent = s.sliderMin;
$('sliderMax').textContent = s.sliderMax;
$('footerText').textContent = s.footer;
updateSliderLabel();
updateModeUI();
updateEngineHint();
showEmptyState();
}
function updateModeUI() {
var s = t();
if (mode === 'linkedin') {
$('inputText').placeholder = s.placeholderLinkedin;
$('translateBtn').textContent = s.btnLinkedin;
} else {
$('inputText').placeholder = s.placeholderHuman;
$('translateBtn').textContent = s.btnHuman;
}
}
function updateEngineHint() {
var s = t();
if (engine === 'api') {
$('engineHint').textContent = s.engineHintApi;
} else {
$('engineHint').textContent = s.engineHintProxy;
}
}
function showEmptyState() {
var s = t();
$('outputArea').innerHTML =
'<div class="empty-state">' +
'<div class="icon">' + s.emptyIcon + '</div>' +
'<p>' + s.emptyText + '</p>' +
'</div>';
}
// ─── Slider ───
function updateSliderLabel() {
var val = $('intensitySlider').value;
$('sliderValueLabel').textContent = t().intensity[val];
}
// ─── Engine Switch ───
function setEngine(e) {
engine = e;
$('engineApi').classList.toggle('active', e === 'api');
$('engineProxy').classList.toggle('active', e === 'proxy');
$('apiKeyRow').classList.toggle('visible', e === 'api');
updateEngineHint();
if (e === 'proxy') checkServer();
}
async function checkServer() {
var s = t();
try {
var r = await fetch('http://localhost:3000/', { method: 'HEAD', signal: AbortSignal.timeout(2000) });
$('engineHint').textContent = r.ok ? s.engineHintProxyOk + ' ✓' : s.engineHintProxyFail;
} catch (e) {
$('engineHint').textContent = s.engineHintProxyFail;
}
}
$('apiKeyInput').addEventListener('input', function() {
$('apiDot').classList.toggle('ok', $('apiKeyInput').value.trim().length > 10);
});
// ─── Mode Toggle ───
function toggleMode() {
mode = mode === 'linkedin' ? 'human' : 'linkedin';
$('toggleTrack').classList.toggle('human', mode === 'human');
$('labelLinkedin').classList.toggle('active', mode === 'linkedin');
$('labelHuman').classList.toggle('active', mode === 'human');
$('sliderSection').style.display = mode === 'linkedin' ? 'block' : 'none';
updateModeUI();
}
// ─── Parse ───
function parseVariations(raw) {
var parts = raw.split(SEP).map(function(s) { return s.trim(); }).filter(function(s) { return s.length > 0; });
if (parts.length === 0) throw new Error(t().errorParse);
return parts.slice(0, 3);
}
// ─── Translate ───
async function doTranslate() {
var s = t();
var text = $('inputText').value.trim();
if (!text) { showError(s.errorEmpty); return; }
$('translateBtn').disabled = true;
showLoading();
try {
var intensity = parseInt($('intensitySlider').value);
var prompt = mode === 'linkedin' ? s.promptLinkedin(text, intensity) : s.promptHuman(text);
var variations = engine === 'api'
? await translateViaApi(prompt)
: await translateViaProxy(prompt);
showVariations(variations);
} catch (err) {
showError(err.message);
} finally {
$('translateBtn').disabled = false;
}
}
// ─── API Engine ───
async function translateViaApi(prompt) {
var s = t();
var apiKey = $('apiKeyInput').value.trim();
if (!apiKey) throw new Error(s.errorNoKey);
var response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true'
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1500,
messages: [{ role: 'user', content: prompt }]
})
});
if (!response.ok) {
var errBody = await response.json().catch(function() { return {}; });
throw new Error((errBody.error && errBody.error.message) || 'Error ' + response.status);
}
var data = await response.json();
var content = (data.content && data.content[0] && data.content[0].text) || '';
return parseVariations(content);
}
// ─── Proxy Engine ───
async function translateViaProxy(prompt) {
var s = t();
var response;
try {
response = await fetch('http://localhost:3000/translate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: prompt })
});
} catch (e) {
throw new Error(s.errorServer);
}
if (!response.ok) {
var errBody = await response.json().catch(function() { return {}; });
throw new Error(errBody.error || 'Error ' + response.status);
}
var data = await response.json();
return parseVariations(data.result || '');
}
// ─── UI ───
function showLoading() {
var s = t();
var msg = mode === 'linkedin' ? s.loadingLinkedin : s.loadingHuman;
$('outputArea').innerHTML =
'<div class="loading-container">' +
'<div class="loading-dots"><span></span><span></span><span></span></div>' +
'<span class="loading-text">' + msg + '</span>' +
'</div>';
}
function showError(msg) {
$('outputArea').innerHTML = '<div class="error-msg">' + escapeHtml(msg) + '</div>';
}
function showVariations(variations) {
var s = t();
var labels = mode === 'linkedin' ? s.labelsLinkedin : s.labelsHuman;
var copyIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
$('outputArea').innerHTML = variations.map(function(text, i) {
return '<div class="variation-card">' +
'<div class="card-header">' +
'<span class="card-number">' + (labels[i] || 'Variation ' + (i + 1)) + '</span>' +
'<button class="copy-btn" onclick="copyText(this, ' + i + ')">' + copyIcon + ' ' + s.copy + '</button>' +
'</div>' +
'<div class="card-text" id="variation-' + i + '">' + escapeHtml(text) + '</div>' +
'</div>';
}).join('');
}
function escapeHtml(str) {
var d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
async function copyText(btn, index) {
var s = t();
var el = document.getElementById('variation-' + index);
var copyIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
var checkIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
try {
await navigator.clipboard.writeText(el.textContent);
btn.classList.add('copied');
btn.innerHTML = checkIcon + ' ' + s.copied;
setTimeout(function() {
btn.classList.remove('copied');
btn.innerHTML = copyIcon + ' ' + s.copy;
}, 2000);
} catch (e) { btn.textContent = 'Error'; }
}
// Ctrl+Enter shortcut
$('inputText').addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
doTranslate();
}
});
// ─── Init ───
applyI18n();
checkServer();
</script>
</body>
</html>