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
1028 lines
30 KiB
HTML
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>
|