Include widgets in README, setup storybook
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,762 @@
|
||||
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 <Open3DLab> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user