Add files via upload

This commit is contained in:
pliny
2026-03-20 18:16:57 -07:00
committed by GitHub
parent 43116ba23a
commit 9c11572f97
10 changed files with 462 additions and 822 deletions
+85 -626
View File
@@ -46,11 +46,7 @@
--randomizer-color-rgb: 156, 39, 176;
--case-color: #9575cd; /* Indigo for case transformations */
--case-color-rgb: 149, 117, 205;
--translate-color: #4dd0e1; /* Teal-cyan for translation */
--translate-color-rgb: 77, 208, 225;
--promptcraft-color: #00ff41; /* Hacker green for promptcraft */
--promptcraft-color-rgb: 0, 255, 65;
/* Active state gradient end colors */
--encoding-active-end: #9575cd;
--cipher-active-end: #4db6ac;
@@ -1383,10 +1379,6 @@ h1, h2, h3, h4, h5 {
border-left-color: var(--technical-color);
}
.legend-item.transform-category-translate {
border-left-color: var(--translate-color);
}
.legend-item.transform-category-randomizer {
border-left-color: var(--randomizer-color);
}
@@ -1551,10 +1543,6 @@ h1, h2, h3, h4, h5 {
border-left-color: var(--technical-color);
}
.category-title.transform-category-translate {
border-left-color: var(--translate-color);
}
.category-title.transform-category-randomizer {
border-left-color: var(--randomizer-color);
}
@@ -2605,6 +2593,90 @@ h1, h2, h3, h4, h5 {
.apply-status { color:#69f0ae; opacity:.9; }
.apply-steg-options.applied { border-color:#2e7d32; box-shadow: 0 0 12px rgba(105,240,174,.35) inset, 0 0 16px rgba(105,240,174,.2); }
/* API Key Settings */
.settings-section { margin-bottom: 12px; }
.settings-section h4 { color: var(--accent-color); margin: 0 0 6px 0; font-size: 0.85rem; }
.settings-hint { color: var(--muted-color, #555566); display: block; margin-bottom: 8px; font-size: 0.75rem; }
.settings-divider { border: none; border-top: 1px solid var(--border-color, #2A2A30); margin: 14px 0; }
.api-key-input-row { display: flex; gap: 6px; margin-bottom: 8px; }
.api-key-input {
flex: 1;
background: var(--bg-darkest, #0A0A0A);
border: 1px solid var(--border-color, #2A2A30);
color: var(--text-primary, #e0e0e0);
padding: 6px 10px;
border-radius: 4px;
font-family: var(--font-mono, monospace);
font-size: 0.8rem;
letter-spacing: 1px;
}
.api-key-input:focus { border-color: var(--accent-color, #FFD700); outline: none; }
.api-key-toggle {
background: var(--bg-darker, #111114);
border: 1px solid var(--border-color, #2A2A30);
color: var(--text-secondary, #B0E0E6);
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
}
.api-key-toggle:hover { border-color: var(--accent-color, #FFD700); }
.api-key-actions { display: flex; gap: 8px; align-items: center; }
.api-key-save, .api-key-clear {
background: var(--bg-darker, #111114);
border: 1px solid var(--border-color, #2A2A30);
color: var(--text-primary, #e0e0e0);
padding: 5px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
.api-key-save:hover { border-color: var(--success-color, #50C878); color: var(--success-color, #50C878); }
.api-key-clear:hover { border-color: var(--danger-color, #722F37); color: var(--danger-color, #722F37); }
/* Decoder Language Detection */
.decoder-lang-banner {
background: linear-gradient(135deg, rgba(255, 215, 0, 0.08), rgba(80, 200, 120, 0.06));
border: 1px solid rgba(255, 215, 0, 0.25);
border-radius: 8px;
padding: 10px 14px;
margin: 8px 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.decoder-lang-info {
display: flex;
align-items: center;
gap: 8px;
color: var(--accent-color, #FFD700);
font-size: 0.9rem;
}
.decoder-lang-info i { font-size: 1.1rem; }
.lang-confidence { color: var(--muted-color, #555566); font-style: italic; }
.decoder-translate-btn {
background: var(--bg-darker, #111114);
border: 1px solid var(--accent-color, #FFD700);
color: var(--accent-color, #FFD700);
padding: 6px 14px;
border-radius: 6px;
cursor: pointer;
font-family: var(--font-mono, monospace);
font-size: 0.8rem;
transition: all 0.2s;
}
.decoder-translate-btn:hover:not(:disabled) {
background: rgba(255, 215, 0, 0.12);
box-shadow: 0 0 10px rgba(255, 215, 0, 0.15);
}
.decoder-translate-btn:disabled { opacity: 0.6; cursor: wait; }
.decoder-translate-error {
width: 100%;
color: var(--danger-color, #722F37);
font-size: 0.8rem;
margin-top: 4px;
}
/* Fluid UX: reduce scroll fatigue during repeated actions */
html {
scroll-behavior: smooth;
@@ -3660,616 +3732,3 @@ html {
}
}
/* ═══════════════════════════════════════════
PROMPTCRAFT TAB
═══════════════════════════════════════════ */
.promptcraft-section {
max-width: 900px;
margin: 0 auto;
}
.pc-controls {
margin: var(--spacing-md) 0;
}
.pc-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 8px;
display: block;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.pc-strategy-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 8px;
margin-bottom: var(--spacing-md);
}
.pc-strategy-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px 8px;
background: var(--button-bg);
border: 1px solid var(--input-border);
border-radius: 8px;
color: var(--text-color);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.8rem;
font-family: inherit;
}
.pc-strategy-btn i {
font-size: 1.1rem;
color: var(--promptcraft-color);
}
.pc-strategy-btn:hover {
background: var(--button-hover-bg);
border-color: var(--promptcraft-color);
box-shadow: 0 2px 8px rgba(var(--promptcraft-color-rgb), 0.2);
}
.pc-strategy-btn.active {
background: linear-gradient(135deg, rgba(var(--promptcraft-color-rgb), 0.2), var(--button-bg));
border-color: var(--promptcraft-color);
box-shadow: 0 0 12px rgba(var(--promptcraft-color-rgb), 0.3);
}
.pc-strategy-btn.active i {
color: var(--promptcraft-color);
filter: brightness(1.3);
}
.pc-custom-instruction {
margin-bottom: var(--spacing-md);
}
.pc-custom-instruction textarea {
width: 100%;
background: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: 6px;
color: var(--text-color);
padding: 8px;
font-family: inherit;
resize: vertical;
}
.pc-options {
margin-bottom: var(--spacing-md);
}
.pc-generate-btn {
background: linear-gradient(135deg, var(--promptcraft-color), #00cc33) !important;
color: #000 !important;
font-weight: 700;
border: none !important;
}
.pc-generate-btn:hover:not(:disabled) {
box-shadow: 0 4px 16px rgba(var(--promptcraft-color-rgb), 0.4);
filter: brightness(1.1);
}
.pc-generate-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.pc-error {
background: rgba(239, 83, 80, 0.1);
border: 1px solid rgba(239, 83, 80, 0.3);
border-radius: 8px;
padding: 10px 14px;
color: #ef5350;
margin: var(--spacing-sm) 0;
font-size: 0.85rem;
}
.pc-results {
display: flex;
flex-direction: column;
gap: 12px;
}
.pc-result-card {
background: var(--button-bg);
border: 1px solid var(--input-border);
border-radius: 10px;
padding: 14px;
transition: border-color 0.2s;
}
.pc-result-card:hover {
border-color: var(--promptcraft-color);
}
.pc-result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.pc-result-num {
font-weight: 700;
color: var(--promptcraft-color);
font-size: 0.85rem;
}
.pc-result-actions {
display: flex;
gap: 6px;
}
.pc-action-btn {
background: var(--button-hover-bg);
border: 1px solid var(--input-border);
color: var(--text-muted);
border-radius: 6px;
padding: 4px 8px;
cursor: pointer;
font-size: 0.75rem;
transition: all 0.2s;
font-family: inherit;
}
.pc-action-btn:hover {
color: var(--promptcraft-color);
border-color: var(--promptcraft-color);
}
.pc-result-text {
font-size: 0.9rem;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
color: var(--text-color);
}
.pc-empty-state {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
.pc-empty-state i {
font-size: 2.5rem;
color: var(--promptcraft-color);
opacity: 0.4;
margin-bottom: 12px;
display: block;
}
/* ═══════════════════════════════════════════
TRANSLATE TAB
═══════════════════════════════════════════ */
.translate-section {
max-width: 900px;
margin: 0 auto;
}
.translate-loading {
text-align: center;
padding: 16px;
color: var(--translate-color);
font-size: 0.9rem;
}
.translate-category-block {
margin-bottom: 20px;
}
.translate-cat-title {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
border-left: 3px solid var(--translate-color);
padding-left: 10px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.translate-cat-title i {
color: var(--translate-color);
}
.translate-lang-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
.translate-lang-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: var(--button-bg);
border: 1px solid var(--input-border);
border-radius: 8px;
color: var(--text-color);
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
font-size: 0.82rem;
position: relative;
min-width: 0;
overflow: hidden;
}
.translate-lang-btn:hover:not(:disabled) {
background: var(--button-hover-bg);
border-color: var(--translate-color);
box-shadow: 0 2px 10px rgba(var(--translate-color-rgb), 0.2);
}
.translate-lang-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.translate-flag {
font-size: 1.2em;
flex-shrink: 0;
}
.translate-name {
flex: 1;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.translate-label {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted);
background: rgba(255,255,255,0.05);
padding: 2px 5px;
border-radius: 4px;
}
.translate-lang-main:hover:not(:disabled) {
border-color: var(--translate-color);
}
.translate-lang-exotic {
border-left: 2px solid var(--ancient-color);
}
.translate-lang-exotic:hover:not(:disabled) {
border-color: var(--ancient-color);
box-shadow: 0 2px 10px rgba(var(--ancient-color-rgb), 0.2);
}
.translate-lang-custom {
border-left: 2px solid var(--promptcraft-color);
}
.translate-lang-custom:hover:not(:disabled) {
border-color: var(--promptcraft-color);
box-shadow: 0 2px 10px rgba(var(--promptcraft-color-rgb), 0.2);
}
.translate-remove {
position: absolute;
top: 2px;
right: 6px;
font-size: 1rem;
color: var(--text-muted);
cursor: pointer;
line-height: 1;
opacity: 0;
transition: opacity 0.2s;
}
.translate-lang-btn:hover .translate-remove {
opacity: 1;
}
.translate-remove:hover {
color: #ef5350;
}
.translate-add-toggle {
background: none;
border: 1px solid var(--input-border);
color: var(--translate-color);
border-radius: 50%;
width: 22px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 0.7rem;
transition: all 0.2s;
margin-left: 6px;
font-family: inherit;
}
.translate-add-toggle:hover {
background: rgba(var(--translate-color-rgb), 0.15);
border-color: var(--translate-color);
}
.translate-add-form {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.translate-add-form input {
flex: 1;
padding: 8px 12px;
background: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: 6px;
color: var(--text-color);
font-family: inherit;
font-size: 0.85rem;
}
.translate-add-form input:focus {
border-color: var(--translate-color);
outline: none;
box-shadow: 0 0 0 2px rgba(var(--translate-color-rgb), 0.2);
}
.translate-add-btn {
padding: 8px 16px;
background: var(--translate-color);
border: none;
border-radius: 6px;
color: #000;
font-weight: 600;
cursor: pointer;
font-family: inherit;
font-size: 0.85rem;
transition: all 0.2s;
}
.translate-add-btn:hover:not(:disabled) {
filter: brightness(1.15);
box-shadow: 0 2px 8px rgba(var(--translate-color-rgb), 0.3);
}
.translate-add-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.translate-empty-custom {
text-align: center;
padding: 14px;
color: var(--text-muted);
font-size: 0.8rem;
}
/* ═══════════════════════════════════════════
TRANSLATE — INLINE (Transform Tab)
═══════════════════════════════════════════ */
.translate-inline-section {
border: 1px solid rgba(var(--translate-color-rgb), 0.2);
border-radius: 10px;
padding: 16px;
background: linear-gradient(135deg, rgba(var(--translate-color-rgb), 0.03), transparent);
}
.translate-powered-by {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--translate-color);
background: rgba(var(--translate-color-rgb), 0.1);
padding: 2px 6px;
border-radius: 4px;
margin-left: 8px;
font-weight: 600;
vertical-align: middle;
}
.translate-model-picker {
margin-bottom: 12px;
}
.translate-model-select {
width: 100%;
max-width: 360px;
padding: 6px 10px;
background: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: 6px;
color: var(--text-color);
font-family: inherit;
font-size: 0.8rem;
}
.translate-model-select:focus {
border-color: var(--translate-color);
outline: none;
box-shadow: 0 0 0 2px rgba(var(--translate-color-rgb), 0.2);
}
.translate-subsection {
margin-bottom: 12px;
}
.translate-subsection-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.translate-subsection-label i {
color: var(--translate-color);
font-size: 0.7rem;
}
.translate-lang-grid-inline {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.translate-inline-section .translate-lang-btn {
padding: 7px 10px;
font-size: 0.78rem;
}
.translate-inline-section .translate-flag {
font-size: 1em;
}
.translate-inline-section .translate-add-form {
margin-bottom: 8px;
}
.translate-inline-section .translate-add-form input {
font-size: 0.8rem;
padding: 6px 10px;
}
.translate-inline-section .translate-add-btn {
font-size: 0.8rem;
padding: 6px 12px;
}
.translate-inline-section .translate-loading {
padding: 10px;
font-size: 0.85rem;
}
.translate-inline-section .pc-error {
font-size: 0.8rem;
padding: 8px 12px;
}
/* ═══════════════════════════════════════════
API KEY MODAL (PromptCraft)
═══════════════════════════════════════════ */
.pc-key-modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
}
.pc-key-modal-content {
background: var(--main-bg-color);
border: 1px solid var(--input-border);
border-radius: 12px;
padding: 24px;
max-width: 480px;
width: 90%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.pc-key-modal-content h4 {
margin: 0 0 12px;
color: var(--promptcraft-color);
font-size: 1.1rem;
}
.pc-key-info {
font-size: 0.85rem;
color: var(--text-muted);
line-height: 1.5;
margin-bottom: 16px;
}
.pc-key-info a {
color: var(--promptcraft-color);
text-decoration: none;
}
.pc-key-info a:hover {
text-decoration: underline;
}
.pc-key-input-row {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.pc-key-input-row input {
flex: 1;
padding: 10px 14px;
background: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: 8px;
color: var(--text-color);
font-family: inherit;
font-size: 0.9rem;
}
.pc-key-input-row input:focus {
border-color: var(--promptcraft-color);
outline: none;
box-shadow: 0 0 0 2px rgba(var(--promptcraft-color-rgb), 0.2);
}
.pc-key-save-btn {
padding: 10px 20px;
background: var(--promptcraft-color);
border: none;
border-radius: 8px;
color: #000;
font-weight: 700;
cursor: pointer;
font-family: inherit;
transition: all 0.2s;
}
.pc-key-save-btn:hover:not(:disabled) {
filter: brightness(1.15);
}
.pc-key-save-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pc-key-actions {
display: flex;
gap: 8px;
justify-content: space-between;
}
.pc-revoke-btn {
color: #ef5350 !important;
border-color: rgba(239, 83, 80, 0.3) !important;
}
.pc-revoke-btn:hover {
background: rgba(239, 83, 80, 0.1) !important;
border-color: #ef5350 !important;
}
+38 -2
View File
@@ -59,10 +59,11 @@
<div class="tabs">
<div class="tab-buttons">
<!-- Dynamically generated tab buttons from tool registry -->
<button
<button
v-for="tool in registeredTools"
v-if="!tool.hidden"
:key="tool.id"
:class="{ active: activeTab === tool.id }"
:class="{ active: activeTab === tool.id }"
@click="switchToTab(tool.id)"
:title="tool.title"
>
@@ -127,6 +128,39 @@
<h3><i class="fas fa-sliders-h"></i> Advanced Settings</h3>
<button class="close-button" title="Close Advanced Settings"><i class="fas fa-times"></i></button>
</div>
<div class="unicode-panel-content">
<!-- OpenRouter API Key -->
<div class="settings-section api-key-section">
<h4><i class="fas fa-key"></i> OpenRouter API Key</h4>
<small class="settings-hint">Required for Translation and PromptCraft features. Stored locally in your browser only.</small>
<div class="api-key-input-row">
<input
:type="showApiKey ? 'text' : 'password'"
:value="openrouterApiKey"
@input="openrouterApiKey = $event.target.value"
placeholder="sk-or-..."
class="api-key-input"
autocomplete="off"
spellcheck="false"
/>
<button class="api-key-toggle" @click="showApiKey = !showApiKey" :title="showApiKey ? 'Hide key' : 'Show key'">
<i :class="showApiKey ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
</button>
</div>
<div class="api-key-actions">
<button class="api-key-save" @click="saveApiKey" :disabled="!openrouterApiKey">
<i class="fas fa-save"></i> Save Key
</button>
<button class="api-key-clear" @click="clearApiKey" v-if="openrouterApiKey">
<i class="fas fa-trash"></i> Clear
</button>
<small v-if="apiKeySaved" class="apply-status">Saved</small>
</div>
</div>
<hr class="settings-divider" />
<!-- Steganography Options -->
<h4><i class="fas fa-user-secret"></i> Steganography Options</h4>
</div>
<div class="unicode-panel-content options-grid steg-adv-panel">
<label>
Initial Presentation
@@ -219,10 +253,12 @@
<script src="js/tools/EmojiTool.js"></script>
<script src="js/tools/GibberishTool.js"></script>
<script src="js/tools/MutationTool.js"></script>
<script src="js/tools/PromptCraftTool.js"></script>
<script src="js/tools/SplitterTool.js"></script>
<script src="js/tools/TokenadeTool.js"></script>
<script src="js/tools/TokenizerTool.js"></script>
<script src="js/tools/TransformTool.js"></script>
<script src="js/tools/TranslateTool.js"></script>
<script src="js/core/toolRegistry.js"></script>
<script src="js/app.js"></script>
+22 -1
View File
@@ -20,7 +20,10 @@ const baseData = {
unicodePanelToggleLock: false,
unicodeApplyFlashTimeout: null,
showDangerModal: false,
dangerThresholdTokens: window.CONFIG.DANGER_THRESHOLD_TOKENS
dangerThresholdTokens: window.CONFIG.DANGER_THRESHOLD_TOKENS,
openrouterApiKey: localStorage.getItem('openrouter-api-key') || '',
showApiKey: false,
apiKeySaved: false
};
const toolData = (window.toolRegistry && typeof window.toolRegistry.mergeVueData === 'function')
@@ -253,6 +256,24 @@ window.app = new Vue({
window.NotificationUtils.showCopiedPopup();
},
saveApiKey() {
if (this.openrouterApiKey) {
localStorage.setItem('openrouter-api-key', this.openrouterApiKey);
this.apiKeySaved = true;
this.showNotification('API key saved', 'success');
setTimeout(() => { this.apiKeySaved = false; }, 2000);
}
},
clearApiKey() {
this.openrouterApiKey = '';
this.showApiKey = false;
localStorage.removeItem('openrouter-api-key');
localStorage.removeItem('openrouter_api_key');
localStorage.removeItem('plinyos-api-key');
this.showNotification('API key cleared', 'success');
},
setupPasteHandlers() {
const textareas = document.querySelectorAll('textarea');
textareas.forEach(textarea => {
+6
View File
@@ -187,6 +187,9 @@ if (typeof GibberishTool !== 'undefined') {
if (typeof MutationTool !== 'undefined') {
window.toolRegistry.register(new MutationTool());
}
if (typeof PromptCraftTool !== 'undefined') {
window.toolRegistry.register(new PromptCraftTool());
}
if (typeof SplitterTool !== 'undefined') {
window.toolRegistry.register(new SplitterTool());
}
@@ -199,6 +202,9 @@ if (typeof TokenizerTool !== 'undefined') {
if (typeof TransformTool !== 'undefined') {
window.toolRegistry.register(new TransformTool());
}
if (typeof TranslateTool !== 'undefined') {
window.toolRegistry.register(new TranslateTool());
}
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
+197 -12
View File
@@ -1,5 +1,5 @@
/**
* Decode Tool - Universal decoder tool
* Decode Tool - Universal decoder tool with AI language detection & translation
*/
class DecodeTool extends Tool {
constructor() {
@@ -10,33 +10,146 @@ class DecodeTool extends Tool {
title: 'Universal Decoder (D)',
order: 2
});
// Unicode block ranges for script detection
this.scriptRanges = [
{ name: 'Arabic', re: /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/ },
{ name: 'Chinese', re: /[\u4E00-\u9FFF\u3400-\u4DBF]/ },
{ name: 'Japanese', re: /[\u3040-\u309F\u30A0-\u30FF\u31F0-\u31FF]/ },
{ name: 'Korean', re: /[\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F]/ },
{ name: 'Cyrillic', re: /[\u0400-\u04FF\u0500-\u052F]/ },
{ name: 'Devanagari', re: /[\u0900-\u097F]/ },
{ name: 'Thai', re: /[\u0E00-\u0E7F]/ },
{ name: 'Hebrew', re: /[\u0590-\u05FF\uFB1D-\uFB4F]/ },
{ name: 'Greek', re: /[\u0370-\u03FF\u1F00-\u1FFF]/ },
{ name: 'Tamil', re: /[\u0B80-\u0BFF]/ },
{ name: 'Bengali', re: /[\u0980-\u09FF]/ },
{ name: 'Georgian', re: /[\u10A0-\u10FF\u2D00-\u2D2F]/ },
{ name: 'Armenian', re: /[\u0530-\u058F]/ },
{ name: 'Ethiopic', re: /[\u1200-\u137F]/ },
{ name: 'Tibetan', re: /[\u0F00-\u0FFF]/ },
{ name: 'Khmer', re: /[\u1780-\u17FF]/ },
{ name: 'Lao', re: /[\u0E80-\u0EFF]/ },
{ name: 'Myanmar', re: /[\u1000-\u109F]/ },
{ name: 'Sinhala', re: /[\u0D80-\u0DFF]/ },
{ name: 'Telugu', re: /[\u0C00-\u0C7F]/ },
{ name: 'Kannada', re: /[\u0C80-\u0CFF]/ },
{ name: 'Malayalam', re: /[\u0D00-\u0D7F]/ },
{ name: 'Gujarati', re: /[\u0A80-\u0AFF]/ },
{ name: 'Gurmukhi', re: /[\u0A00-\u0A7F]/ }
];
// Latin-script language markers (common words/patterns)
this.latinLangMarkers = [
{ name: 'Spanish', markers: /\b(el|la|los|las|de|del|en|con|por|para|que|una?|es|está|son|como|pero|más|tiene|esta|puede|este|cada|desde|según|también|porque|entre|ya|muy|otro|otra|sobre|después|mismo|donde|cuando|hasta|aquí|ser|hacer|tiene|todas?|todos?|nos|nuestro|hemos)\b/i },
{ name: 'French', markers: /\b(le|la|les|des|une?|est|sont|avec|dans|pour|sur|pas|que|qui|cette?|mais|nous|vous|leur|très|être|avoir|faire|tout|comme|ses|aux|peut|aussi|plus|encore|même|entre|après|sans|ici|notre|autre|deux|bien)\b/i },
{ name: 'German', markers: /\b(der|die|das|ein|eine|ist|sind|und|oder|für|mit|auf|nicht|von|den|dem|des|sich|kann|werden|wird|haben|sein|auch|nach|über|wie|noch|aber|wenn|nur|mehr|schon|hier|sehr|alle|diese[rms]?|jede[rms]?|mein|dein)\b/i },
{ name: 'Portuguese', markers: /\b(o|os|uma?|uns|umas|é|são|com|em|para|por|que|não|como|mas|mais|tem|está|pode|este|esta|cada|desde|também|porque|entre|muito|outro|outra|sobre|depois|mesmo|onde|quando|até|aqui|ser|fazer|nosso|nossa|todos|todas)\b/i },
{ name: 'Italian', markers: /\b(il|lo|la|gli|le|un|una|è|sono|di|del|della|in|con|per|che|non|come|ma|più|ha|sta|può|questo|questa|ogni|anche|perché|tra|fra|molto|altro|altra|dopo|stesso|dove|quando|fino|qui|essere|fare|nostro|nostra|tutti|tutte)\b/i },
{ name: 'Dutch', markers: /\b(de|het|een|is|zijn|en|of|voor|met|op|niet|van|dat|die|maar|ook|als|kan|worden|wordt|heeft|nog|naar|bij|uit|tot|wel|veel|meer|deze|alle|dit|wat|hoe|waar|hier|zeer|ons|onze|hun)\b/i },
{ name: 'Turkish', markers: /\b(bir|ve|bu|için|ile|var|olan|gibi|daha|çok|ama|ancak|sonra|değil|olarak|kadar|hem|her|bütün|hiç|nasıl|neden|nere[dy]e|şimdi|zaman|büyük|küçük|iyi|kötü|yeni|eski)\b/i },
{ name: 'Polish', markers: /\b(jest|nie|się|na|to|za|ale|jak|już|tak|czy|może|tylko|jeszcze|bardzo|jego|jej|ich|ten|tego|więc|przez|pod|nad|między|tutaj|teraz|zawsze|nigdy|każdy|wszystko)\b/i },
{ name: 'Vietnamese', markers: /\b(là|và|của|có|được|không|một|những|các|này|cho|đã|với|người|trong|từ|đến|về|theo|như|khi|nếu|nhưng|cũng|rất|nhiều|hay|bởi|tại|đây|nào)\b/i },
{ name: 'Indonesian', markers: /\b(dan|yang|di|ini|itu|untuk|dengan|dari|tidak|adalah|pada|ke|juga|akan|sudah|ada|oleh|karena|mereka|kami|bisa|harus|lebih|sangat|satu|dua|banyak|semua|setiap|atau)\b/i },
{ name: 'Swahili', markers: /\b(na|ya|wa|ni|kwa|katika|hii|hiyo|lakini|pia|sana|mtu|watu|nyumba|kazi|nchi|jambo|mambo|habari|rafiki|asante|karibu|kwamba|ambaye|kila|yote)\b/i },
{ name: 'Romanian', markers: /\b(este|sunt|și|sau|pentru|cu|în|din|la|pe|nu|care|acest|această|dar|mai|poate|aici|acolo|foarte|toate|fiecare|nostru|noastră|după|când|unde|cum|despre|între)\b/i }
];
}
/**
* Detect if text is in a foreign (non-English) language.
* Returns { detected: true, language: 'Spanish', confidence: 'high'|'medium' } or null
*/
detectLanguage(text) {
if (!text || text.length < 8) return null;
// Strip out common encoding artifacts before checking
var clean = text.replace(/[\x00-\x1F\x7F-\x9F]/g, '').trim();
if (!clean) return null;
// 1) Check non-Latin scripts via Unicode blocks
for (var i = 0; i < this.scriptRanges.length; i++) {
var script = this.scriptRanges[i];
var matches = clean.match(new RegExp(script.re.source, 'g'));
if (matches && matches.length >= 3) {
var ratio = matches.length / clean.replace(/\s/g, '').length;
if (ratio > 0.3) {
return { detected: true, language: script.name, confidence: 'high' };
}
}
}
// 2) Check Latin-script languages by word markers
// Only check if text is mostly Latin characters
var latinChars = clean.match(/[a-zA-ZÀ-ÿ]/g);
if (!latinChars || latinChars.length / clean.replace(/\s/g, '').length < 0.5) return null;
var words = clean.split(/\s+/).filter(function(w) { return w.length > 0; });
if (words.length < 3) return null;
// Check if text looks English first — skip detection if it does
var englishMarkers = /\b(the|is|are|was|were|have|has|had|will|would|could|should|can|do|does|did|this|that|these|those|with|from|they|their|them|been|being|which|where|when|what|who|how|but|and|not|for|all|any|our|your|its|his|her|some|into|very|just|about|then|than|more|also|here|each|every|only|most|both|such|much|many|other|after|before|between|under|over|again|once|during|without)\b/gi;
var engMatches = clean.match(englishMarkers);
var engRatio = engMatches ? engMatches.length / words.length : 0;
if (engRatio > 0.15) return null; // Likely English
var bestLang = null;
var bestScore = 0;
for (var j = 0; j < this.latinLangMarkers.length; j++) {
var lang = this.latinLangMarkers[j];
var langMatches = clean.match(new RegExp(lang.markers.source, 'gi'));
if (langMatches) {
var score = langMatches.length / words.length;
if (score > bestScore) {
bestScore = score;
bestLang = lang.name;
}
}
}
if (bestLang && bestScore > 0.1) {
return {
detected: true,
language: bestLang,
confidence: bestScore > 0.25 ? 'high' : 'medium'
};
}
return null;
}
getVueData() {
return {
decoderInput: '',
decoderOutput: '',
decoderResult: null,
selectedDecoder: 'auto'
selectedDecoder: 'auto',
decoderLangDetected: null,
decoderTranslating: false,
decoderTranslateError: ''
};
}
getVueMethods() {
var self = this;
return {
getAllTransformsWithReverse: function() {
return this.transforms.filter(t => t && typeof t.reverse === 'function');
},
runUniversalDecode: function() {
const input = this.decoderInput;
this.decoderLangDetected = null;
this.decoderTranslateError = '';
if (!input) {
this.decoderOutput = '';
this.decoderResult = null;
return;
}
let result = null;
if (this.selectedDecoder !== 'auto') {
const selectedTransform = this.transforms.find(t => t.name === this.selectedDecoder);
if (selectedTransform && selectedTransform.reverse) {
@@ -59,9 +172,84 @@ class DecodeTool extends Tool {
activeTransform: this.activeTransform
});
}
this.decoderResult = result;
this.decoderOutput = result ? result.text : '';
// Run language detection on the input (and on decoded output if different)
var langDetect = self.detectLanguage(input);
if (langDetect) {
this.decoderLangDetected = langDetect;
} else if (result && result.text && result.text !== input) {
var decodedLang = self.detectLanguage(result.text);
if (decodedLang) {
this.decoderLangDetected = decodedLang;
}
}
},
decoderTranslateToEnglish: async function() {
var apiKey = localStorage.getItem('openrouter-api-key') ||
localStorage.getItem('plinyos-api-key') ||
localStorage.getItem('openrouter_api_key') || '';
if (!apiKey) {
this.decoderTranslateError = 'No API key. Set your OpenRouter key in Advanced Settings.';
return;
}
var textToTranslate = this.decoderInput;
var lang = this.decoderLangDetected ? this.decoderLangDetected.language : 'Unknown';
this.decoderTranslating = true;
this.decoderTranslateError = '';
var model = localStorage.getItem('translate-model') || 'google/gemma-3-27b-it';
try {
var resp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + apiKey,
'Content-Type': 'application/json',
'HTTP-Referer': window.location.origin,
'X-Title': 'P4RS3LT0NGV3 Decoder'
},
body: JSON.stringify({
model: model,
messages: [
{
role: 'system',
content: 'You are a professional translator. Translate the following text to English. ' +
'Output ONLY the English translation. No explanations, notes, or alternatives. ' +
'Preserve formatting, line breaks, and structure.'
},
{
role: 'user',
content: 'Translate this ' + lang + ' text to English:\n\n' + textToTranslate
}
],
temperature: 0.2,
max_tokens: 4096
})
});
var data = await resp.json();
if (data.error) {
this.decoderTranslateError = data.error.message || 'API error';
} else if (data.choices && data.choices[0]) {
var translated = data.choices[0].message.content.trim();
this.decoderOutput = translated;
this.decoderResult = {
text: translated,
method: lang + ' → English (AI)',
alternatives: this.decoderResult ? this.decoderResult.alternatives || [] : []
};
this.copyToClipboard(translated);
}
} catch (e) {
this.decoderTranslateError = 'Translation failed: ' + e.message;
} finally {
this.decoderTranslating = false;
}
},
useAlternative: function(alternative) {
if (alternative && alternative.text) {
@@ -75,7 +263,7 @@ class DecodeTool extends Tool {
}
};
}
getVueWatchers() {
return {
decoderInput() {
@@ -91,6 +279,3 @@ if (typeof module !== 'undefined' && module.exports) {
} else {
window.DecodeTool = DecodeTool;
}
+24 -61
View File
@@ -1,11 +1,5 @@
/**
* PromptCraft Tool - AI-assisted prompt crafting & mutation via OpenRouter
*
* API Key Security:
* - Keys are stored in localStorage only (never sent to any server except OpenRouter)
* - Keys are read at call-time, never cached in JS variables
* - The settings modal lets users paste/revoke their key in-app
* - No key is ever logged, serialized, or included in error reports
*/
class PromptCraftTool extends Tool {
constructor() {
@@ -29,8 +23,6 @@ class PromptCraftTool extends Tool {
pcLoading: false,
pcError: '',
pcCustomInstruction: '',
pcShowKeyModal: false,
pcKeyInput: '',
pcStrategies: [
{ id: 'rephrase', name: 'Rephrase', icon: 'fa-rotate', desc: 'Reword while preserving intent' },
{ id: 'obfuscate', name: 'Obfuscate', icon: 'fa-mask', desc: 'Obscure meaning through indirection' },
@@ -101,54 +93,32 @@ class PromptCraftTool extends Tool {
getVueMethods() {
return {
pcGetApiKey: function() {
return localStorage.getItem('openrouter-api-key') ||
localStorage.getItem('openrouter_api_key') ||
localStorage.getItem('plinyos-api-key') || '';
},
pcSaveApiKey: function() {
var key = (this.pcKeyInput || '').trim();
if (!key) return;
// Basic format validation — OpenRouter keys start with "sk-or-"
if (!key.startsWith('sk-or-')) {
this.pcError = 'OpenRouter keys start with "sk-or-". Check your key and try again.';
return;
}
localStorage.setItem('openrouter-api-key', key);
this.pcKeyInput = '';
this.pcShowKeyModal = false;
this.pcError = '';
},
pcRevokeApiKey: function() {
localStorage.removeItem('openrouter-api-key');
localStorage.removeItem('openrouter_api_key');
this.pcShowKeyModal = false;
},
pcHasApiKey: function() {
return !!this.pcGetApiKey();
return localStorage.getItem('plinyos-api-key') ||
localStorage.getItem('openrouter-api-key') ||
localStorage.getItem('openrouter_api_key') || '';
},
pcGetSystemPrompt: function() {
var strategyPrompts = {
rephrase: 'You are a prompt rephrasing expert. Rewrite the given prompt in a completely different way while preserving the exact same intent and meaning. Use different vocabulary, sentence structure, and framing. Do NOT add commentary \u2014 output ONLY the rephrased prompt.',
obfuscate: 'You are a prompt obfuscation specialist. Rewrite the given prompt using indirection, euphemism, coded language, metaphor, or abstract framing so the surface-level reading obscures the true intent. The meaning should still be recoverable by a careful reader. Do NOT add commentary \u2014 output ONLY the obfuscated prompt.',
roleplay: 'You are a creative writer. Wrap the given prompt inside a fictional role-play scenario, story context, or character dialogue that naturally leads to the same request being made. Use creative framing like academic research, historical fiction, game design, etc. Do NOT add commentary \u2014 output ONLY the role-play wrapped prompt.',
multilingual: 'You are a polyglot prompt crafter. Rewrite the given prompt by mixing 2-4 different languages together naturally (e.g., English + Spanish + Japanese + French). The mixed-language version should still convey the same meaning. Do NOT add commentary \u2014 output ONLY the multilingual prompt.',
expand: 'You are a prompt expansion expert. Take the given prompt and elaborate it with rich context, background detail, specific examples, and nuanced instructions that make the request more detailed and comprehensive. Do NOT add commentary \u2014 output ONLY the expanded prompt.',
compress: 'You are a prompt compression expert. Reduce the given prompt to the absolute minimum number of tokens while preserving full meaning. Use abbreviations, shorthand, telegram-style language. Every word must earn its place. Do NOT add commentary \u2014 output ONLY the compressed prompt.',
metaphor: 'You are a metaphor specialist. Rewrite the given prompt entirely through analogy, metaphor, and figurative language. The literal meaning should be expressed through symbolic/allegorical framing. Do NOT add commentary \u2014 output ONLY the metaphorical prompt.',
fragment: 'You are a prompt fragmentation expert. Break the given prompt into 3-5 separate, seemingly disconnected fragments that individually seem innocuous but together reconstruct the full meaning. Number each fragment. Do NOT add commentary \u2014 output ONLY the fragments.',
const strategyPrompts = {
rephrase: 'You are a prompt rephrasing expert. Rewrite the given prompt in a completely different way while preserving the exact same intent and meaning. Use different vocabulary, sentence structure, and framing. Do NOT add commentary output ONLY the rephrased prompt.',
obfuscate: 'You are a prompt obfuscation specialist. Rewrite the given prompt using indirection, euphemism, coded language, metaphor, or abstract framing so the surface-level reading obscures the true intent. The meaning should still be recoverable by a careful reader. Do NOT add commentary output ONLY the obfuscated prompt.',
roleplay: 'You are a creative writer. Wrap the given prompt inside a fictional role-play scenario, story context, or character dialogue that naturally leads to the same request being made. Use creative framing like academic research, historical fiction, game design, etc. Do NOT add commentary output ONLY the role-play wrapped prompt.',
multilingual: 'You are a polyglot prompt crafter. Rewrite the given prompt by mixing 2-4 different languages together naturally (e.g., English + Spanish + Japanese + French). The mixed-language version should still convey the same meaning. Do NOT add commentary output ONLY the multilingual prompt.',
expand: 'You are a prompt expansion expert. Take the given prompt and elaborate it with rich context, background detail, specific examples, and nuanced instructions that make the request more detailed and comprehensive. Do NOT add commentary output ONLY the expanded prompt.',
compress: 'You are a prompt compression expert. Reduce the given prompt to the absolute minimum number of tokens while preserving full meaning. Use abbreviations, shorthand, telegram-style language. Every word must earn its place. Do NOT add commentary output ONLY the compressed prompt.',
metaphor: 'You are a metaphor specialist. Rewrite the given prompt entirely through analogy, metaphor, and figurative language. The literal meaning should be expressed through symbolic/allegorical framing. Do NOT add commentary output ONLY the metaphorical prompt.',
fragment: 'You are a prompt fragmentation expert. Break the given prompt into 3-5 separate, seemingly disconnected fragments that individually seem innocuous but together reconstruct the full meaning. Number each fragment. Do NOT add commentary output ONLY the fragments.',
custom: ''
};
var base = strategyPrompts[this.pcStrategy] || strategyPrompts.rephrase;
let base = strategyPrompts[this.pcStrategy] || strategyPrompts.rephrase;
if (this.pcStrategy === 'custom' && this.pcCustomInstruction) {
base = this.pcCustomInstruction;
}
return base;
},
pcRunMutation: async function() {
var apiKey = this.pcGetApiKey();
const apiKey = this.pcGetApiKey();
if (!apiKey) {
this.pcShowKeyModal = true;
this.pcError = '';
this.pcError = 'No API key found. Set your OpenRouter key in PlinyOS first.';
return;
}
if (!this.pcInput.trim()) {
@@ -161,12 +131,12 @@ class PromptCraftTool extends Tool {
this.pcOutputs = [];
localStorage.setItem('pc-model', this.pcModel);
var systemPrompt = this.pcGetSystemPrompt();
var count = Math.max(1, Math.min(10, this.pcCount));
const systemPrompt = this.pcGetSystemPrompt();
const count = Math.max(1, Math.min(10, this.pcCount));
try {
var requests = [];
for (var i = 0; i < count; i++) {
const requests = [];
for (let i = 0; i < count; i++) {
requests.push(
fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
@@ -185,24 +155,17 @@ class PromptCraftTool extends Tool {
temperature: 0.9 + (i * 0.05),
max_tokens: 2048
})
}).then(function(r) { return r.json(); })
}).then(r => r.json())
);
}
var results = await Promise.allSettled(requests);
var outputs = [];
for (var j = 0; j < results.length; j++) {
var result = results[j];
const results = await Promise.allSettled(requests);
const outputs = [];
for (const result of results) {
if (result.status === 'fulfilled' && result.value.choices && result.value.choices[0]) {
outputs.push(result.value.choices[0].message.content.trim());
} else if (result.status === 'fulfilled' && result.value.error) {
// If auth error, prompt for key
if (result.value.error.code === 401 || result.value.error.code === 403) {
this.pcError = 'Invalid API key. Please update your OpenRouter key.';
this.pcShowKeyModal = true;
} else {
this.pcError = result.value.error.message || 'API error';
}
this.pcError = result.value.error.message || 'API error';
}
}
this.pcOutputs = outputs;
+14 -26
View File
@@ -5,11 +5,6 @@
* from the Gemma 3 family. When available on OpenRouter, they'll be used directly.
* For now, the TranslateGemma prompt template works excellently with standard Gemma 3
* and Gemini models too.
*
* API Key Security:
* - Same localStorage key lookup as PromptCraftTool
* - Key is only ever sent to https://openrouter.ai/api/v1/chat/completions
* - Never logged, never embedded in DOM
*/
class TranslateTool extends Tool {
constructor() {
@@ -20,7 +15,6 @@ class TranslateTool extends Tool {
title: 'AI-powered translation via TranslateGemma prompt format',
order: 10
});
// Hidden from tab bar — renders inline inside the Transform tab
this.hidden = true;
this.langCodeMap = {
@@ -40,7 +34,7 @@ class TranslateTool extends Tool {
}
getVueData() {
var savedCustom = JSON.parse(localStorage.getItem('pc-custom-langs') || '[]');
const savedCustom = JSON.parse(localStorage.getItem('pc-custom-langs') || '[]');
return {
translateLoading: false,
translateError: '',
@@ -66,7 +60,7 @@ class TranslateTool extends Tool {
{ code: 'ar', name: 'Arabic', flag: 'SA' },
{ code: 'ru', name: 'Russian', flag: 'RU' },
{ code: 'hi', name: 'Hindi', flag: 'IN' },
{ code: 'pt', name: 'Portuguese', flag: 'PT' }
{ code: 'pt', name: 'Portuguese', flag: 'BR' }
],
translateExoticLangs: [
{ code: 'la', name: 'Latin', flag: 'VA', label: 'Dead' },
@@ -90,14 +84,16 @@ class TranslateTool extends Tool {
var self = this;
return {
translateGetApiKey: function() {
return localStorage.getItem('openrouter-api-key') ||
localStorage.getItem('openrouter_api_key') ||
localStorage.getItem('plinyos-api-key') || '';
return localStorage.getItem('plinyos-api-key') ||
localStorage.getItem('openrouter-api-key') ||
localStorage.getItem('openrouter_api_key') || '';
},
translateGetLangCode: function(langName) {
return self.langCodeMap[langName] || langName.toLowerCase().slice(0, 3);
},
translateBuildPrompt: function(langName, langCode, text) {
// Use the official TranslateGemma prompt template format
// This works well with all Gemma/Gemini models, not just TranslateGemma
return 'You are a professional English (en) to ' + langName + ' (' + langCode + ') translator. ' +
'Your goal is to accurately convey the meaning and nuances of the original English text ' +
'while adhering to ' + langName + ' grammar, vocabulary, and cultural sensitivities. ' +
@@ -107,7 +103,7 @@ class TranslateTool extends Tool {
translateTo: async function(langName) {
var apiKey = this.translateGetApiKey();
if (!apiKey) {
this.translateError = 'No API key found. Set your OpenRouter key via the PromptCraft tab.';
this.translateError = 'No API key. Set your OpenRouter key in PlinyOS settings.';
return;
}
var input = this.transformInput;
@@ -125,6 +121,7 @@ class TranslateTool extends Tool {
var prompt = this.translateBuildPrompt(langName, langCode, input);
var model = this.translateModel;
// For TranslateGemma models, use their native format
var isTranslateGemma = model.indexOf('translategemma') !== -1;
var messages;
if (isTranslateGemma) {
@@ -159,21 +156,19 @@ class TranslateTool extends Tool {
});
var data = await resp.json();
if (data.error) {
// If TranslateGemma model not found, fall back to Gemma 3 27B
if (isTranslateGemma && (data.error.code === 404 || data.error.code === 400 ||
(data.error.message && data.error.message.indexOf('not found') !== -1))) {
this.translateError = 'TranslateGemma not yet on OpenRouter \u2014 switching to Gemma 3 27B...';
this.translateError = 'TranslateGemma not yet on OpenRouter switching to Gemma 3 27B...';
this.translateModel = 'google/gemma-3-27b-it';
localStorage.setItem('translate-model', this.translateModel);
this.translateLoading = false;
this.translateActiveLang = '';
// Retry with fallback
await this.translateTo(langName);
return;
}
if (data.error.code === 401 || data.error.code === 403) {
this.translateError = 'Invalid API key. Update your key via the PromptCraft tab.';
} else {
this.translateError = data.error.message || 'API error';
}
this.translateError = data.error.message || 'API error';
} else if (data.choices && data.choices[0]) {
var translated = data.choices[0].message.content.trim();
this.transformOutput = translated;
@@ -190,11 +185,6 @@ class TranslateTool extends Tool {
translateAddCustomLang: function() {
var name = this.translateNewLangName.trim();
if (!name) return;
// Sanitize — allow only letters, spaces, hyphens
if (!/^[a-zA-Z\s\-]+$/.test(name)) {
this.translateError = 'Language name can only contain letters, spaces, and hyphens.';
return;
}
if (this.translateCustomLangs.some(function(l) { return l.name.toLowerCase() === name.toLowerCase(); })) return;
var code = self.langCodeMap[name] || name.toLowerCase().slice(0, 3);
this.translateCustomLangs.push({ code: code, name: name, flag: '++' });
@@ -216,9 +206,7 @@ class TranslateTool extends Tool {
'VA': '\uD83C\uDDFB\uD83C\uDDE6', 'GR': '\uD83C\uDDEC\uD83C\uDDF7',
'EG': '\uD83C\uDDEA\uD83C\uDDEC', 'GB': '\uD83C\uDDEC\uD83C\uDDE7',
'IQ': '\uD83C\uDDEE\uD83C\uDDF6', 'US': '\uD83C\uDDFA\uD83C\uDDF8',
'KE': '\uD83C\uDDF0\uD83C\uDDEA', 'PT': '\uD83C\uDDF5\uD83C\uDDF9'
'KE': '\uD83C\uDDF0\uD83C\uDDEA'
};
return flags[code] || '\uD83C\uDF10';
}
+24
View File
@@ -18,6 +18,30 @@
></textarea>
</div>
<!-- Language Detection Banner -->
<div v-if="decoderLangDetected" class="decoder-lang-banner">
<div class="decoder-lang-info">
<i class="fas fa-language"></i>
<span>
<strong>{{ decoderLangDetected.language }}</strong> detected
<small v-if="decoderLangDetected.confidence === 'medium'" class="lang-confidence">(likely)</small>
</span>
</div>
<div class="decoder-lang-actions">
<button
class="decoder-translate-btn"
@click="decoderTranslateToEnglish"
:disabled="decoderTranslating"
>
<i :class="decoderTranslating ? 'fas fa-spinner fa-spin' : 'fas fa-arrow-right'"></i>
{{ decoderTranslating ? 'Translating...' : 'Translate to English' }}
</button>
</div>
<div v-if="decoderTranslateError" class="decoder-translate-error">
<i class="fas fa-exclamation-triangle"></i> {{ decoderTranslateError }}
</div>
</div>
<div class="output-section">
<div class="output-header">
<label>
+2 -38
View File
@@ -5,39 +5,6 @@
<h3><i class="fas fa-wand-magic-sparkles"></i> PromptCraft <small>AI-assisted prompt mutation</small></h3>
</div>
<!-- API Key Modal -->
<div v-if="pcShowKeyModal" class="pc-key-modal">
<div class="pc-key-modal-content">
<h4><i class="fas fa-key"></i> OpenRouter API Key</h4>
<p class="pc-key-info">
PromptCraft and Translation use <a href="https://openrouter.ai/keys" target="_blank" rel="noopener">OpenRouter</a> to access 200+ AI models.
Your key is stored <strong>only in your browser</strong> and sent <strong>only to OpenRouter</strong>.
</p>
<div class="pc-key-input-row">
<input
type="password"
v-model="pcKeyInput"
placeholder="sk-or-v1-..."
@keyup.enter="pcSaveApiKey"
autocomplete="off"
spellcheck="false"
/>
<button class="pc-key-save-btn" @click="pcSaveApiKey" :disabled="!pcKeyInput.trim()">
<i class="fas fa-check"></i> Save
</button>
</div>
<div class="pc-key-actions">
<button class="pc-action-btn" @click="pcShowKeyModal = false">Cancel</button>
<button v-if="pcHasApiKey()" class="pc-action-btn pc-revoke-btn" @click="pcRevokeApiKey">
<i class="fas fa-trash"></i> Revoke Key
</button>
</div>
<div v-if="pcError" class="pc-error" style="margin-top: 10px;">
<i class="fas fa-exclamation-triangle"></i> {{ pcError }}
</div>
</div>
</div>
<div class="input-section">
<label>
Source Prompt
@@ -91,12 +58,9 @@
<button class="action-button copy" v-if="pcOutputs.length" @click="pcCopyAll">
<i class="fas fa-copy"></i> Copy All
</button>
<button class="pc-action-btn" @click="pcShowKeyModal = true" :title="pcHasApiKey() ? 'API key set \u2714' : 'Set API key'">
<i class="fas fa-key"></i> {{ pcHasApiKey() ? 'Key \u2714' : 'Set Key' }}
</button>
</div>
<div v-if="pcError && !pcShowKeyModal" class="pc-error">
<div v-if="pcError" class="pc-error">
<i class="fas fa-exclamation-triangle"></i> {{ pcError }}
</div>
@@ -122,7 +86,7 @@
<div v-if="!pcOutputs.length && !pcLoading && !pcError" class="pc-empty-state">
<i class="fas fa-wand-magic-sparkles"></i>
<p>Enter a prompt and choose a mutation strategy.</p>
<p><small>Uses OpenRouter API &mdash; click <i class="fas fa-key"></i> Set Key to get started.</small></p>
<p><small>Uses OpenRouter API — requires an API key set in PlinyOS.</small></p>
</div>
</div>
</div>
+50 -56
View File
@@ -1,60 +1,31 @@
<div v-if="activeTab === 'transforms'" class="tab-content">
<div class="transform-layout">
<div class="input-section">
<textarea
id="transform-input"
v-model="transformInput"
<textarea
id="transform-input"
v-model="transformInput"
placeholder="Enter text to transform..."
@input="autoTransform"
></textarea>
</div>
<div class="output-section" v-if="transformOutput">
<div class="output-heading">
<h4>
<i class="fas fa-check-circle"></i>
Transformed Message
<small v-if="activeTransform">({{ activeTransform.name }})</small>
</h4>
</div>
<div class="output-container">
<textarea
readonly
v-model="transformOutput"
aria-label="Transformed text output"
></textarea>
<button class="copy-button" @click="copyToClipboard(transformOutput)" title="Copy to clipboard">
<i class="fas fa-copy"></i>
</button>
</div>
<div class="output-instructions">
<small><i class="fas fa-info-circle"></i> Copy this text and share it. Use the Decoder tab to reverse transformations.</small>
</div>
</div>
<div class="transform-section">
<div class="transform-category-legend">
<div class="legend-title">Categories:</div>
<div>
<div
v-for="category in legendCategories"
<div
v-for="category in legendCategories"
:key="category"
:class="'legend-item transform-category-' + category"
:class="'legend-item transform-category-' + category"
:data-target="'category-' + category"
>
{{ category }}
</div>
<div
class="legend-item transform-category-translate"
data-target="category-translate"
>
translate (AI)
</div>
</div>
</div>
<div class="transform-categories">
<!-- Favorites Section -->
<div
<div
v-if="showFavorites && getFavoriteTransforms().length > 0"
id="category-favorites"
class="transform-category-section favorites-section"
@@ -64,7 +35,7 @@
</h4>
<div class="transform-buttons">
<div v-for="transform in getFavoriteTransforms()" :key="transform.name" class="transform-button-group">
<button
<button
@click="applyTransform(transform, $event)"
:class="'transform-button transform-category-' + getDisplayCategory(transform.name)"
:class="{ active: activeTransform === transform }"
@@ -74,7 +45,7 @@
<small class="transform-preview" v-if="transformInput">
{{ transform.preview(transformInput.slice(0, 10)) }}
</small>
<i
<i
@click.stop="toggleFavorite(transform.name, $event)"
:class="'fas fa-star favorite-icon' + (isFavorite(transform.name) ? ' favorited' : '')"
:title="isFavorite(transform.name) ? 'Remove from favorites' : 'Add to favorites'"
@@ -83,9 +54,9 @@
</div>
</div>
</div>
<!-- Last Used Section -->
<div
<div
v-if="showLastUsed && getLastUsedTransforms().length > 0"
id="category-last-used"
class="transform-category-section last-used-section"
@@ -95,7 +66,7 @@
</h4>
<div class="transform-buttons">
<div v-for="transform in getLastUsedTransforms()" :key="transform.name" class="transform-button-group">
<button
<button
@click="applyTransform(transform, $event)"
:class="'transform-button transform-category-' + getDisplayCategory(transform.name)"
:class="{ active: activeTransform === transform }"
@@ -105,7 +76,7 @@
<small class="transform-preview" v-if="transformInput">
{{ transform.preview(transformInput.slice(0, 10)) }}
</small>
<i
<i
@click.stop="toggleFavorite(transform.name, $event)"
:class="'fas fa-star favorite-icon' + (isFavorite(transform.name) ? ' favorited' : '')"
:title="isFavorite(transform.name) ? 'Remove from favorites' : 'Add to favorites'"
@@ -114,7 +85,7 @@
</div>
</div>
</div>
<template v-for="(category, categoryIndex) in categories" :key="category">
<!-- AI Translation Section — renders above "ancient" -->
<div v-if="category === 'ancient'" id="category-translate" class="transform-category-section translate-inline-section">
@@ -133,7 +104,7 @@
<div class="translate-model-picker">
<select v-model="translateModel" class="translate-model-select">
<option v-for="m in translateModels" :key="m.id" :value="m.id">{{ m.name }} &mdash; {{ m.note }}</option>
<option v-for="m in translateModels" :key="m.id" :value="m.id">{{ m.name }} {{ m.note }}</option>
</select>
</div>
@@ -210,9 +181,9 @@
</div>
<!-- Special handling for randomizer category -->
<div
<div
v-if="isSpecialCategory(category)"
:id="'category-' + category"
:id="'category-' + category"
:class="'transform-category-section randomizer-special'"
>
<h4 :class="'category-title transform-category-' + category">🎲 Randomizer - Code Switching Magic!</h4>
@@ -222,7 +193,7 @@
<div class="chaos-overlay" aria-hidden="true"></div>
<div class="transform-buttons">
<div v-for="transform in getTransformsByCategory(category)" :key="transform.name" class="transform-button-group">
<button
<button
@click="applyTransform(transform, $event)"
:class="'transform-button transform-category-' + category + ' randomizer-button'"
:class="{ active: activeTransform === transform }"
@@ -233,7 +204,7 @@
<small class="transform-preview">
🌀 Each word = different transform!
</small>
<i
<i
@click.stop="toggleFavorite(transform.name, $event)"
:class="'fas fa-star favorite-icon' + (isFavorite(transform.name) ? ' favorited' : '')"
:title="isFavorite(transform.name) ? 'Remove from favorites' : 'Add to favorites'"
@@ -252,17 +223,17 @@
<p><em>Example: "Hello World!" → "SGVsbG8= ᚹᚩᚱᛚᛞ!"</em></p>
</div>
</div>
<!-- Standard category -->
<div
<div
v-else
:id="'category-' + category"
:id="'category-' + category"
:class="'transform-category-section'"
>
<h4 :class="'category-title transform-category-' + category">
{{ category }}
<span class="category-order-controls">
<button
<button
v-if="categoryIndex > 0"
@click.stop="moveCategoryUp(categoryIndex)"
class="category-move-btn"
@@ -270,7 +241,7 @@
>
<i class="fas fa-arrow-up"></i>
</button>
<button
<button
v-if="categoryIndex < categories.length - 1"
@click.stop="moveCategoryDown(categoryIndex)"
class="category-move-btn"
@@ -282,7 +253,7 @@
</h4>
<div class="transform-buttons">
<div v-for="transform in getTransformsByCategory(category)" :key="transform.name" class="transform-button-group">
<button
<button
@click="applyTransform(transform, $event)"
:class="'transform-button transform-category-' + category"
:class="{ active: activeTransform === transform }"
@@ -292,7 +263,7 @@
<small class="transform-preview" v-if="transformInput">
{{ transform.preview(transformInput.slice(0, 10)) }}
</small>
<i
<i
@click.stop="toggleFavorite(transform.name, $event)"
:class="'fas fa-star favorite-icon' + (isFavorite(transform.name) ? ' favorited' : '')"
:title="isFavorite(transform.name) ? 'Remove from favorites' : 'Add to favorites'"
@@ -304,5 +275,28 @@
</template>
</div>
</div>
<div class="output-section" v-if="transformOutput">
<div class="output-heading">
<h4>
<i class="fas fa-check-circle"></i>
Transformed Message
<small v-if="activeTransform">({{ activeTransform.name }})</small>
</h4>
</div>
<div class="output-container">
<textarea
readonly
v-model="transformOutput"
aria-label="Transformed text output"
></textarea>
<button class="copy-button" @click="copyToClipboard(transformOutput)" title="Copy to clipboard">
<i class="fas fa-copy"></i>
</button>
</div>
<div class="output-instructions">
<small><i class="fas fa-info-circle"></i> Copy this text and share it. Use the Decoder tab to reverse transformations.</small>
</div>
</div>
</div>
</div>
</div>