feat(core): update chat error style (#9885)

[BS-2487](https://linear.app/affine-design/issue/BS-2487/报错样式更新)
This commit is contained in:
donteatfriedrice
2025-01-24 12:39:10 +00:00
parent 5a5779c05a
commit 4b553d153a
2 changed files with 213 additions and 138 deletions

View File

@@ -2,169 +2,244 @@ import { type EditorHost } from '@blocksuite/affine/block-std';
import {
type AIError,
PaymentRequiredError,
scrollbarStyle,
UnauthorizedError,
unsafeCSSVarV2,
} from '@blocksuite/affine/blocks';
import { WithDisposable } from '@blocksuite/affine/global/utils';
import { html, LitElement, nothing, type TemplateResult } from 'lit';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/utils';
import { ToggleDownIcon } from '@blocksuite/icons/lit';
import { signal } from '@preact/signals-core';
import { baseTheme } from '@toeverything/theme';
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { ErrorTipIcon } from '../_common/icons';
import { AIProvider } from '../provider';
export class AIErrorWrapper extends WithDisposable(LitElement) {
@property({ attribute: false })
accessor text!: TemplateResult<1>;
protected override render() {
return html` <style>
.answer-tip {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 4px;
align-self: stretch;
border-radius: 4px;
padding: 8px;
background-color: var(--affine-background-error-color);
.bottom {
align-items: flex-start;
display: flex;
gap: 8px;
align-self: stretch;
color: var(--affine-error-color, #eb4335);
font-feature-settings:
'clig' off,
'liga' off;
/* light/sm */
font-size: var(--affine-font-sm);
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
margin-bottom: 4px;
a {
color: inherit;
}
div svg {
position: relative;
top: 3px;
}
}
}
</style>
<div class="answer-tip">
<div class="bottom">
<div>${ErrorTipIcon}</div>
<div>${this.text}</div>
</div>
<slot></slot>
</div>`;
}
}
export const PaymentRequiredErrorRenderer = (host: EditorHost) => html`
<style>
.upgrade {
cursor: pointer;
export class AIErrorWrapper extends SignalWatcher(WithDisposable(LitElement)) {
static override styles = css`
.error-wrapper {
display: flex;
padding: 4px 12px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 8px;
margin-left: auto;
border: 1px solid var(--affine-border-color, #e3e2e4);
background: var(--affine-primary-color);
align-items: flex-start;
gap: 12px;
align-self: stretch;
border-radius: 4px;
padding: 8px 8px 12px 8px;
background-color: ${unsafeCSSVarV2('layer/background/error')};
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
.content {
align-items: flex-start;
display: flex;
gap: 8px;
align-self: stretch;
color: ${unsafeCSSVarV2('status/error')};
font-feature-settings:
'clig' off,
'liga' off;
/* light/sm */
font-size: var(--affine-font-sm);
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
.icon svg {
position: relative;
top: 3px;
}
}
.text-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-container {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
}
.detail-title {
display: flex;
padding: 0px 4px;
justify-content: center;
align-items: center;
color: var(--affine-pure-white);
/* light/xsMedium */
}
.detail-title:hover {
cursor: pointer;
}
.detail-content {
padding: 4px;
border-radius: 4px;
background-color: ${unsafeCSSVarV2('layer/background/translucentUI')};
height: 66px;
overflow: auto;
}
${scrollbarStyle('.detail-content')}
.toggle {
display: flex;
align-items: center;
justify-content: center;
}
.toggle.up svg {
transform: rotate(180deg);
transition: all 0.2s ease-in-out;
}
.action {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
}
.action-button {
cursor: pointer;
color: ${unsafeCSSVarV2('text/primary')};
background: ${unsafeCSSVarV2('button/secondary')};
border-radius: 8px;
border: 1px solid ${unsafeCSSVarV2('button/innerBlackBorder')};
padding: 4px 12px;
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
line-height: 20px;
}
.action-button:hover {
transition: all 0.2s ease-in-out;
background-image: linear-gradient(
rgba(0, 0, 0, 0.04),
rgba(0, 0, 0, 0.04)
);
}
}
</style>
`;
private readonly _showDetailContent = signal(false);
protected override render() {
return html` <div class="error-wrapper">
<div class="content">
<div class="icon">${ErrorTipIcon}</div>
<div class="text-container">
<div>${this.text}</div>
${this.showDetailPanel
? html`<div class="detail-container">
<div
class="detail-title"
@click=${() =>
(this._showDetailContent.value =
!this._showDetailContent.value)}
>
<span>Show detail</span>
<span
class="toggle ${this._showDetailContent.value
? 'down'
: 'up'}"
>
${ToggleDownIcon({ width: '16px', height: '16px' })}
</span>
</div>
${this._showDetailContent.value
? html`<div class="detail-content">${this.errorMessage}</div>`
: nothing}
</div>`
: nothing}
</div>
</div>
<div class="action">
<span
class="action-button"
@click=${this.onClick}
data-testid="ai-error-action-button"
>
${this.actionText}
${this.actionTooltip
? html`<affine-tooltip tip-position="top"
>${this.actionTooltip}</affine-tooltip
>`
: nothing}
</span>
</div>
</div>`;
}
@property({ attribute: false })
accessor text: string = '';
@property({ attribute: false })
accessor onClick: () => void = () => {};
@property({ attribute: false })
accessor errorMessage: string = '';
@property({ attribute: false })
accessor actionText: string = 'Contact us';
@property({ attribute: false })
accessor actionTooltip: string = '';
@property({ attribute: false })
accessor showDetailPanel: boolean = false;
}
const PaymentRequiredErrorRenderer = (host: EditorHost) => html`
<ai-error-wrapper
.text=${html`You've reached the current usage cap for AFFiNE AI. You can
subscribe to AFFiNE AI to continue the AI experience!`}
>
<div
@click=${() => AIProvider.slots.requestUpgradePlan.emit({ host: host })}
class="upgrade"
>
<div class="content">Upgrade</div>
</div></ai-error-wrapper
>
.text=${"You've reached the current usage cap for AFFiNE AI. You can subscribe to AFFiNE AI to continue the AI experience!"}
.actionText=${'Upgrade'}
.onClick=${() => AIProvider.slots.requestUpgradePlan.emit({ host })}
></ai-error-wrapper>
`;
const LoginRequiredErrorRenderer = (host: EditorHost) => html`
<ai-error-wrapper
.text=${'You need to login to AFFiNE Cloud to continue using AFFiNE AI.'}
.actionText=${'Login'}
.onClick=${() => AIProvider.slots.requestLogin.emit({ host })}
></ai-error-wrapper>
`;
type ErrorProps = {
text?: TemplateResult<1>;
template?: TemplateResult<1>;
error?: TemplateResult<1>;
text?: string;
errorMessage?: string;
actionText?: string;
actionTooltip?: string;
};
const generateText = (error?: TemplateResult<1>) =>
html`${error || 'An error occurred'}, If this issue persists please let us
know.<a href="mailto:support@toeverything.info">
support@toeverything.info
</a>`;
const generalErrorText =
'An error occurred, If this issue persists please let us know.';
const nope = html`${nothing}`;
const GeneralErrorRenderer = (props: ErrorProps = {}) => {
const { text = generateText(props.error), template = nope } = props;
return html`<ai-error-wrapper .text=${text}>${template}</ai-error-wrapper>`;
const onClick = () => {
window.open('mailto:support@toeverything.info', '_blank');
};
return html`<ai-error-wrapper
.text=${props.text ?? generalErrorText}
.errorMessage=${props.errorMessage ?? ''}
.showDetailPanel=${!!props.errorMessage}
.actionText=${props.actionText ?? 'Contact us'}
.actionTooltip=${props.actionTooltip ?? 'support@toeverything.info'}
.onClick=${onClick}
></ai-error-wrapper>`;
};
export function AIChatErrorRenderer(host: EditorHost, error: AIError) {
if (error instanceof PaymentRequiredError) {
return PaymentRequiredErrorRenderer(host);
} else if (error instanceof UnauthorizedError) {
return LoginRequiredErrorRenderer(host);
} else {
return GeneralErrorRenderer({
errorMessage: error.message,
});
}
}
declare global {
interface HTMLElementTagNameMap {
'ai-error-wrapper': AIErrorWrapper;
}
}
export function AIChatErrorRenderer(host: EditorHost, error: AIError) {
console.error(error);
if (error instanceof PaymentRequiredError) {
return PaymentRequiredErrorRenderer(host);
} else if (error instanceof UnauthorizedError) {
return GeneralErrorRenderer({
text: html`You need to login to AFFiNE Cloud to continue using AFFiNE AI.`,
template: html`<div
style=${styleMap({
padding: '4px 12px',
borderRadius: '8px',
border: '1px solid var(--affine-border-color)',
cursor: 'pointer',
backgroundColor: 'var(--affine-hover-color)',
})}
@click=${() => AIProvider.slots.requestLogin.emit({ host })}
>
Login
</div>`,
});
} else {
const tip = error.message;
return GeneralErrorRenderer({
error: html` <style>
.tip {
text-decoration: underline;
}
</style>
<span class="tip"
>An error occurred<affine-tooltip
tip-position="bottom-start"
.arrow=${false}
>${tip}</affine-tooltip
></span
>`,
});
}
}