1
0
Files
2026-04-15 04:12:32 +02:00

763 lines
24 KiB
JavaScript

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`
<p class="bubble__text">
${this._typedText}<span class="bubble__cursor"></span>
</p>
`;
case 'suggesting':
return html`
<p class="bubble__text">
${this._currentSuggestion.message}
</p>
<div class="bubble__options">
${this._currentSuggestion.options.map(
(opt) => html`
<button
class="bubble__option"
@click=${this._onOptionClick}
>
${opt}
</button>
`,
)}
</div>
`;
case 'loading':
return html`
<div class="bubble__loading">
<span class="bubble__loading-dots">
<span></span><span></span><span></span>
</span>
Generating response…
</div>
`;
case 'error':
return html`
<p class="bubble__error">
Error: Account &lt;Open3DLab&gt; is out of AI tokens. You have $42000 in unpaid invoices. Please settle your balance to continue using AI features.
</p>
`;
default:
return null;
}
}
render() {
const orbClass = [
'orb',
this._state === 'loading' ? 'orb--loading' : '',
this._state === 'error' ? 'orb--error' : '',
]
.filter(Boolean)
.join(' ');
return html`
<div class="clippy">
<div
class="orb-wrapper"
@click=${this._onOrbClick}
title="Open3DLab AI Assistant"
>
<div class=${orbClass}></div>
</div>
${this._showBubble
? html`
<div class="bubble">
<button
class="bubble__dismiss"
@click=${this._dismiss}
aria-label="Dismiss"
>
</button>
${keyed(
this._state,
this._renderBubbleContent(),
)}
<div class="bubble__branding">
powered by <span>LLEmmy</span>
</div>
</div>
`
: null}
</div>
`;
}
}
if (!window.customElements.get('ai-assist-clippy')) {
window.customElements.define('ai-assist-clippy', AiAssistClippy);
}