diff --git a/packages/presets/src/ai/_common/icons.ts b/packages/presets/src/ai/_common/icons.ts index a22b5e609a7d..ba93b90e00b5 100644 --- a/packages/presets/src/ai/_common/icons.ts +++ b/packages/presets/src/ai/_common/icons.ts @@ -891,6 +891,28 @@ export const CopyIcon = html` `; +export const RetryIcon = html` + + + + + + + + + `; + export const MoreIcon = html` void; + + @property({ attribute: false }) + updateStatus!: (status: ChatStatus) => void; + + @property({ attribute: false }) + updateError!: (error: AIError | null) => void; + + @property({ attribute: false }) + abortController!: AbortController | null; + + @property({ attribute: false }) + updateAbortController!: (abortController: AbortController | null) => void; + @state() private _showMoreMenu = false; @@ -111,6 +131,52 @@ export class ChatCopyMore extends WithDisposable(LitElement) { } } + private async _retry() { + const { doc } = this.host; + try { + const abortController = new AbortController(); + const items = [...this.items]; + const last = items[items.length - 1]; + if ('content' in last) { + last.content = ''; + last.createdAt = new Date().toISOString(); + } + + this.updateItems(items); + this.updateStatus('loading'); + this.updateError(null); + + const stream = AIProvider.actions.chat?.({ + retry: true, + docId: doc.id, + workspaceId: doc.collection.id, + host: this.host, + stream: true, + signal: abortController.signal, + where: 'chat-panel', + control: 'chat-send', + }); + + if (stream) { + this.updateAbortController(abortController); + for await (const text of stream) { + this.updateStatus('transmitting'); + const items = [...this.items]; + const last = items[items.length - 1] as ChatMessage; + last.content += text; + this.updateItems(items); + } + + this.updateStatus('success'); + } + } catch (error) { + this.updateStatus('error'); + this.updateError(error as AIError); + } finally { + this.updateAbortController(null); + } + } + override render() { const { host, content, isLast } = this; return html`
this._retry()}> + ${RetryIcon} + Retry +
` + : nothing} ${isLast ? nothing : html`
diff --git a/packages/presets/src/ai/chat-panel/chat-panel-input.ts b/packages/presets/src/ai/chat-panel/chat-panel-input.ts index a403913369b2..dcd32407a63b 100644 --- a/packages/presets/src/ai/chat-panel/chat-panel-input.ts +++ b/packages/presets/src/ai/chat-panel/chat-panel-input.ts @@ -160,8 +160,11 @@ export class ChatPanelInput extends WithDisposable(LitElement) { @state() focused = false; - @state() - abortController?: AbortController; + @property({ attribute: false }) + abortController!: AbortController | null; + + @property({ attribute: false }) + updateAbortController!: (abortController: AbortController | null) => void; send = async () => { if (this.status === 'loading' || this.status === 'transmitting') return; @@ -205,7 +208,7 @@ export class ChatPanelInput extends WithDisposable(LitElement) { }); if (stream) { - this.abortController = abortController; + this.updateAbortController(abortController); for await (const text of stream) { this.updateStatus('transmitting'); @@ -221,7 +224,7 @@ export class ChatPanelInput extends WithDisposable(LitElement) { this.updateStatus('error'); this.updateError(error as AIError); } finally { - this.abortController = undefined; + this.updateAbortController(null); } }; diff --git a/packages/presets/src/ai/chat-panel/chat-panel-messages.ts b/packages/presets/src/ai/chat-panel/chat-panel-messages.ts index 5ad0bdafc197..377a5979a3e3 100644 --- a/packages/presets/src/ai/chat-panel/chat-panel-messages.ts +++ b/packages/presets/src/ai/chat-panel/chat-panel-messages.ts @@ -150,6 +150,21 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) isLoading!: boolean; + @property({ attribute: false }) + updateItems!: (items: ChatItem[]) => void; + + @property({ attribute: false }) + updateStatus!: (status: ChatStatus) => void; + + @property({ attribute: false }) + updateError!: (error: AIError | null) => void; + + @property({ attribute: false }) + abortController!: AbortController | null; + + @property({ attribute: false }) + updateAbortController!: (abortController: AbortController | null) => void; + @query('.chat-panel-messages') messagesContainer!: HTMLDivElement; @@ -359,6 +374,12 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { .isLast=${isLast} .curTextSelection=${this._currentTextSelection} .curBlockSelections=${this._currentBlockSelections} + .items=${this.items} + .abortController=${this.abortController} + .updateItems=${this.updateItems} + .updateStatus=${this.updateStatus} + .updateError=${this.updateError} + .updateAbortController=${this.updateAbortController} > ${isLast ? html`
diff --git a/packages/presets/src/ai/chat-panel/index.ts b/packages/presets/src/ai/chat-panel/index.ts index a207d8040371..bab362814968 100644 --- a/packages/presets/src/ai/chat-panel/index.ts +++ b/packages/presets/src/ai/chat-panel/index.ts @@ -112,6 +112,9 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) { @state() isLoading = false; + @state() + abortController: AbortController | null = null; + private _chatMessages: Ref = createRef(); @@ -192,6 +195,10 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) { this.error = error; }; + updateAbortController = (abortController: AbortController | null) => { + this.abortController = abortController; + }; + scrollToDown() { requestAnimationFrame(() => this._chatMessages.value?.scrollToDown()); } @@ -206,14 +213,21 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) { .status=${this.status} .error=${this.error} .isLoading=${this.isLoading} + .updateItems=${this.updateItems} + .updateStatus=${this.updateStatus} + .updateError=${this.updateError} + .abortController=${this.abortController} + .updateAbortController=${this.updateAbortController} > diff --git a/packages/presets/src/ai/provider.ts b/packages/presets/src/ai/provider.ts index 34857581e7a6..6c8bca7a147a 100644 --- a/packages/presets/src/ai/provider.ts +++ b/packages/presets/src/ai/provider.ts @@ -59,6 +59,8 @@ export class AIProvider { // add more if needed }; + static LAST_ACTION_SESSIONID = ''; + static MAX_LOCAL_HISTORY = 10; // track the history of triggered actions (in memory only) private readonly actionHistory: {