import { LitElement, css, html } from 'lit'; import { keyed } from 'lit/directives/keyed.js'; const UPLOADER_SUGGESTIONS = [ { message: "It looks like you're trying to respond to your idiot fans. Would you like me to think of a creative insult?", options: [ 'Yes, make it devastating', 'No thanks, I can be mean on my own', 'Just write "skill issue" for me', ], }, { message: "Your fans seem to be dumb as bricks. Do you need help telling them that they're idiots?", options: [ 'Generate a passive-aggressive response', "Compose something they won't understand", 'I enjoy suffering, let me type it myself', ], }, { message: "I see someone asked a question that's answered in your description. Want me to draft a condescending reply?", options: [ 'Maximum condescension, please', 'Mild sarcasm will suffice', 'Just link them to the description… again', ], }, { message: "It looks like you're trying to say \"read the description\" for the 47th time. Would you like me to automate this?", options: [ 'Set up an auto-reply bot', 'Generate a FAQ nobody will read', 'Let me savor typing it myself', ], }, { message: "I notice a fan left a one-word comment. Would you like me to generate a proportionally low-effort response?", options: [ 'Reply with a single emoji', 'Match their energy with "k"', 'Ignore them with style', ], }, { message: "I see someone has asked you to make your file compatible with software you have never heard of. Would you like me to draft a refusal that implies it's their fault for using it?", options: [ 'Blame their taste in software', 'Suggest they learn to port it themselves', 'Pretend I didn\'t see this one', ], }, { message: "It looks like someone left a paragraph of feedback you didn't ask for. Would you like me to compose a response that technically says \"thank you\" but conveys something else entirely?", options: [ 'Weaponize politeness', 'Reply with a single period', 'Screenshot it and mock it privately', ], }, { message: "I notice someone has commented \"cute\" on your incredibly detailed and technically complex model. Would you like me to help you express how that makes you feel?", options: [ 'Write something deeply passive-aggressive', 'Reply "thanks" while crying', 'Retire from making things', ], }, { message: "It looks like a fan is asking for the exact same file but in a different format, for free, immediately. Would you like me to explain to them how file conversion works, or would you prefer something ruder?", options: [ 'Something ruder, please', 'Link them to a 2-hour tutorial they won\'t watch', 'Close the tab and lie down', ], }, { message: "I see someone has replied to your pinned notice saying \"I didn't read this\" as if that is your problem. Would you like me to make it their problem instead?", options: [ 'Firmly and creatively, yes', 'Un-pin the notice out of spite', 'Add more words to the notice nobody will read', ], }, ]; const COMMENTER_SUGGESTIONS = [ { message: "It looks like you're trying to ask a question that can be easily answered by a Google search. Would you like me to hallucinate a bunch of wrong answers for you instead?", options: [ 'Yes, I love misinformation', "Actually, I can't read", 'Just Google it for me (we both know you won\'t)', ], }, { message: "It seems like you're about to comment \"doesn't work\" without any details whatsoever. Would you like me to help you be equally unhelpful, but with more words?", options: [ 'Add vague complaints for me', 'Generate a 3-paragraph rant with zero specifics', 'I was actually going to say "fix pls"', ], }, { message: "I see you're writing a comment. Would you like me to check if this exact question has been asked and answered 15 times already in this thread?", options: [ "No, I'm sure I'm the first", "Reading other comments is for losers", 'Just post it, what could go wrong', ], }, { message: "It looks like you're about to request a feature the uploader has already said no to. Would you like me to help you phrase it in a way that's slightly more annoying?", options: [ 'Make it sound urgent and entitled', "Add 'please' so it's technically polite", 'Threaten to make it myself (I obviously won\'t)', ], }, { message: "I notice you're typing a comment that could be interpreted as rude. Would you like me to make it sound even worse while technically following the rules?", options: [ 'Maximize passive aggression', 'Add a smiley face to disguise the hostility', 'On second thought, I\'ll just say "thanks"', ], }, { message: "It looks like you're about to complain that the file doesn't work on your specific setup that you refuse to describe. Would you like me to blame the uploader anyway?", options: [ 'Yes, it\'s definitely their fault', 'Generate a bug report with zero useful info', 'Just type "broken" and log off', ], }, { message: "I see you're writing a comment in a language the uploader almost certainly doesn't speak. Would you like me to run it through a translator that will make it slightly more confusing?", options: [ 'Triple-translate it for maximum chaos', 'Just add "(pls)" at the end in English', 'Send it anyway, vibes will carry it', ], }, { message: "It looks like you're about to leave a five-paragraph essay demanding a free commission. Would you like me to add some light emotional manipulation to improve your odds?", options: [ 'Lay on the guilt thick', 'Claim it\'s "just a small thing"', 'Threaten to be very disappointed', ], }, { message: "I notice you haven't downloaded this file but are about to review it one star anyway. Would you like me to help you sound more authoritative?", options: [ 'Add "trust me" for credibility', 'Reference a file you definitely didn\'t open', 'One star, no note, raw power move', ], }, { message: "It seems like you're asking when the next update will be released. The uploader has not indicated this. Would you like me to invent a deadline for them?", options: [ 'Tell them it should\'ve been done already', 'Suggest they quit their day job', 'Just post "update?" every week forever', ], }, { message: "I see you're typing a comment that starts with \"No offense but\". I have run the numbers and offense will, in fact, be taken. Shall I proceed?", options: [ 'Proceed, I am unstoppable', 'Replace it with "With all due respect"', 'Change it to just "offense"', ], }, ]; class AiAssistClippy extends LitElement { static properties = { mode: { type: String }, _state: { state: true }, _currentSuggestion: { state: true }, _typedText: { state: true }, _showBubble: { state: true }, }; static styles = css` :host { display: inline-block; position: relative; width: 56px; --ai-glow-cyan: #22d3ee; --ai-glow-purple: #a855f7; } .clippy { position: relative; user-select: none; } /* ── Orb ─────────────────────────────────── */ .orb-wrapper { position: relative; flex: 0 0 auto; width: 56px; height: 56px; cursor: pointer; } .orb { position: relative; width: 56px; height: 56px; border-radius: 50%; background: radial-gradient( circle at 38% 35%, var(--ai-glow-cyan) 0%, var(--ai-glow-purple) 60%, #6d28d9 100% ); background-size: 200% 200%; animation: orbShift 4s ease-in-out infinite; box-shadow: 0 0 18px color-mix(in srgb, var(--ai-glow-cyan) 45%, transparent), 0 0 36px color-mix(in srgb, var(--ai-glow-purple) 30%, transparent); transition: box-shadow 250ms ease, transform 250ms ease; } .orb::after { content: ''; position: absolute; top: 14%; left: 22%; width: 30%; height: 24%; border-radius: 50%; background: radial-gradient( ellipse, rgba(255, 255, 255, 0.7) 0%, rgba(255, 255, 255, 0) 100% ); transform: rotate(-30deg); pointer-events: none; } .orb-wrapper:hover .orb { transform: scale(1.08); box-shadow: 0 0 24px color-mix( in srgb, var(--ai-glow-cyan) 60%, transparent ), 0 0 48px color-mix( in srgb, var(--ai-glow-purple) 45%, transparent ); } .orb--loading { animation: orbShift 1.2s ease-in-out infinite, orbPulse 0.6s ease-in-out infinite alternate; } .orb--error { background: radial-gradient( circle at 38% 35%, #f87171 0%, #dc2626 60%, #991b1b 100% ) !important; box-shadow: 0 0 18px rgba(248, 113, 113, 0.45), 0 0 36px rgba(220, 38, 38, 0.3) !important; animation: orbShift 4s ease-in-out infinite !important; } @keyframes orbShift { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } } @keyframes orbPulse { from { transform: scale(1); box-shadow: 0 0 18px color-mix( in srgb, var(--ai-glow-cyan) 45%, transparent ), 0 0 36px color-mix( in srgb, var(--ai-glow-purple) 30%, transparent ); } to { transform: scale(1.1); box-shadow: 0 0 30px color-mix( in srgb, var(--ai-glow-cyan) 70%, transparent ), 0 0 54px color-mix( in srgb, var(--ai-glow-purple) 55%, transparent ); } } /* ── Speech Bubble ───────────────────────── */ .bubble { position: absolute; bottom: 0; left: calc(100% + 12px); background: var(--panel-bg-color, #1a1a1a); border: 1px solid var(--border-color, #363636); border-radius: 12px; padding: 14px 16px; max-width: 500px; min-width: 380px; font-size: 0.9rem; line-height: 1.5; color: var(--body-color, #b9b5af); box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25), 0 0 12px color-mix( in srgb, var(--ai-glow-cyan) 12%, transparent ); animation: bubbleIn 200ms ease-out; transform-origin: top left; } .bubble::before { content: ''; position: absolute; bottom: 18px; left: -8px; width: 0; height: 0; border-top: 8px solid transparent; border-bottom: 8px solid transparent; border-right: 8px solid var(--border-color, #363636); } .bubble::after { content: ''; position: absolute; bottom: 19px; left: -6px; width: 0; height: 0; border-top: 7px solid transparent; border-bottom: 7px solid transparent; border-right: 7px solid var(--panel-bg-color, #1a1a1a); } @keyframes bubbleIn { from { opacity: 0; transform: scale(0.9) translateY(6px); } to { opacity: 1; transform: scale(1) translateY(0); } } .bubble__text { margin: 0 0 12px; } .bubble__cursor { display: inline-block; width: 2px; height: 1em; background: var(--ai-glow-cyan); vertical-align: text-bottom; animation: caretBlink 0.6s step-end infinite; margin-left: 1px; } @keyframes caretBlink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } .bubble__options { display: flex; flex-direction: column; gap: 6px; } .bubble__option { display: block; width: 100%; padding: 8px 12px; border: 1px solid var(--border-color, #363636); border-radius: 8px; background: var(--input-bg-color, #222); color: var(--ai-glow-cyan); font-size: 0.82rem; cursor: pointer; text-align: left; transition: background 140ms ease, border-color 140ms ease; } .bubble__option:hover { background: color-mix( in srgb, var(--ai-glow-cyan) 14%, var(--input-bg-color, #222) ); border-color: var(--ai-glow-cyan); } .bubble__option:active { background: color-mix( in srgb, var(--ai-glow-cyan) 22%, var(--input-bg-color, #222) ); } /* ── Loading state ───────────────────────── */ .bubble__loading { display: flex; align-items: center; gap: 8px; color: var(--ai-glow-cyan); font-size: 0.85rem; } .bubble__loading-dots { display: inline-flex; gap: 3px; } .bubble__loading-dots span { width: 5px; height: 5px; border-radius: 50%; background: var(--ai-glow-cyan); animation: dotBounce 1.2s ease-in-out infinite; } .bubble__loading-dots span:nth-child(2) { animation-delay: 0.15s; } .bubble__loading-dots span:nth-child(3) { animation-delay: 0.3s; } @keyframes dotBounce { 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); } 40% { opacity: 1; transform: scale(1.1); } } /* ── Error state ─────────────────────────── */ .bubble__error { color: #f87171; font-size: 0.85rem; font-family: monospace; } /* ── Dismiss button ──────────────────────── */ .bubble__dismiss { position: absolute; top: 6px; right: 8px; background: none; border: none; color: var(--body-color, #b9b5af); opacity: 0.5; cursor: pointer; font-size: 1rem; line-height: 1; padding: 2px 4px; } .bubble__dismiss:hover { opacity: 1; } /* ── Hidden utility ──────────────────────── */ .bubble__branding { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border-color, #363636); font-size: 0.7rem; color: var(--body-color, #b9b5af); opacity: 0.45; text-align: right; } .bubble__branding span { background: linear-gradient( 90deg, var(--ai-glow-cyan) 0%, var(--ai-glow-purple) 100% ); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: 700; } [hidden] { display: none !important; } `; constructor() { super(); this.mode = 'commenter'; this._state = 'idle'; this._currentSuggestion = null; this._typedText = ''; this._showBubble = false; this._typeTimer = null; this._idleTimer = null; } connectedCallback() { super.connectedCallback(); this._schedulePopUp(); } disconnectedCallback() { super.disconnectedCallback(); clearTimeout(this._typeTimer); clearTimeout(this._idleTimer); clearTimeout(this._loadingTimer); } /* ── Helpers ─────────────────────────────── */ get _suggestions() { return this.mode === 'uploader' ? UPLOADER_SUGGESTIONS : COMMENTER_SUGGESTIONS; } _pickRandom() { const pool = this._suggestions; return pool[Math.floor(Math.random() * pool.length)]; } _schedulePopUp() { clearTimeout(this._idleTimer); this._idleTimer = setTimeout(() => { if (this._state === 'idle') { this._showSuggestion(); } }, 3000); } /* ── State transitions ───────────────────── */ _showSuggestion() { this._currentSuggestion = this._pickRandom(); this._typedText = ''; this._showBubble = true; this._state = 'typing'; this._typeNextChar(0); } _typeNextChar(index) { const full = this._currentSuggestion.message; if (index <= full.length) { this._typedText = full.slice(0, index); this._typeTimer = setTimeout( () => this._typeNextChar(index + 1), 22 + Math.random() * 28, ); } else { this._state = 'suggesting'; } } _onOptionClick() { this._state = 'loading'; this._loadingTimer = setTimeout(() => { this._state = 'error'; }, 2000); } _dismiss() { clearTimeout(this._typeTimer); clearTimeout(this._loadingTimer); this._showBubble = false; this._state = 'idle'; this._schedulePopUp(); } _onOrbClick() { if (this._showBubble) { this._dismiss(); } else { clearTimeout(this._idleTimer); this._showSuggestion(); } } /* ── Render ──────────────────────────────── */ _renderBubbleContent() { switch (this._state) { case 'typing': return html`

${this._typedText}

`; case 'suggesting': return html`

${this._currentSuggestion.message}

${this._currentSuggestion.options.map( (opt) => html` `, )}
`; case 'loading': return html`
Generating response…
`; case 'error': return html`

Error: Account <Open3DLab> is out of AI tokens. You have $42000 in unpaid invoices. Please settle your balance to continue using AI features.

`; default: return null; } } render() { const orbClass = [ 'orb', this._state === 'loading' ? 'orb--loading' : '', this._state === 'error' ? 'orb--error' : '', ] .filter(Boolean) .join(' '); return html`
${this._showBubble ? html`
${keyed( this._state, this._renderBubbleContent(), )}
powered by LLEmmy
` : null}
`; } } if (!window.customElements.get('ai-assist-clippy')) { window.customElements.define('ai-assist-clippy', AiAssistClippy); }