mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(core): support open doc in ai session history (#13035)
Close [AI-240 <img width="533" alt="截屏2025-07-04 18 04 39" src="https://github.com/user-attachments/assets/726a54b6-3bdb-4e70-9cda-4671d83ae5bd" /> ](https://linear.app/affine-design/issue/AI-240) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Enhanced chat toolbar and session history with the ability to open specific documents directly from the chat interface. * Added tooltips and improved click handling for clearer user interactions in chat session and document lists. * **Bug Fixes** * Prevented redundant actions when attempting to open already active sessions or documents. * **Style** * Improved tooltip formatting and visual styling for error messages and tooltips. * Refined hover effects and layout in chat session history for better clarity. * **Refactor** * Updated tooltip configuration for more precise positioning and behavior. * **Chores** * Minor updates to property defaults for tooltips and chat panel components. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: fengmk2 <fengmk2@gmail.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type {
|
||||
ContextEmbedStatus,
|
||||
CopilotSessionType,
|
||||
@@ -98,6 +99,9 @@ export class ChatPanel extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineWorkbenchService!: WorkbenchService;
|
||||
|
||||
@state()
|
||||
accessor session: CopilotSessionType | null | undefined;
|
||||
|
||||
@@ -137,34 +141,72 @@ export class ChatPanel extends SignalWatcher(
|
||||
<ai-chat-toolbar
|
||||
.session=${this.session}
|
||||
.workspaceId=${this.doc.workspace.id}
|
||||
.docId=${this.doc.id}
|
||||
.onNewSession=${this.newSession}
|
||||
.onTogglePin=${this.togglePin}
|
||||
.onOpenSession=${this.openSession}
|
||||
.onOpenDoc=${this.openDoc}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.notification=${notification}
|
||||
></ai-chat-toolbar>
|
||||
`;
|
||||
}
|
||||
|
||||
private readonly getSessionIdFromUrl = () => {
|
||||
if (this.affineWorkbenchService) {
|
||||
const { workbench } = this.affineWorkbenchService;
|
||||
const location = workbench.location$.value;
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const sessionId = searchParams.get('sessionId');
|
||||
if (sessionId) {
|
||||
workbench.activeView$.value.updateQueryString(
|
||||
{ sessionId: undefined },
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
return sessionId;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private readonly setSession = (
|
||||
session: CopilotSessionType | null | undefined
|
||||
) => {
|
||||
this.session = session ?? null;
|
||||
};
|
||||
|
||||
private readonly initSession = async () => {
|
||||
if (!AIProvider.session) {
|
||||
return;
|
||||
}
|
||||
const sessionId = this.getSessionIdFromUrl();
|
||||
const pinSessions = await AIProvider.session.getSessions(
|
||||
this.doc.workspace.id,
|
||||
undefined,
|
||||
{ pinned: true, limit: 1 }
|
||||
);
|
||||
|
||||
if (Array.isArray(pinSessions) && pinSessions[0]) {
|
||||
// pinned session
|
||||
this.session = pinSessions[0];
|
||||
} else if (sessionId) {
|
||||
// sessionId from url
|
||||
const session = await AIProvider.session.getSession(
|
||||
this.doc.workspace.id,
|
||||
sessionId
|
||||
);
|
||||
this.setSession(session);
|
||||
} else {
|
||||
// latest doc session
|
||||
const docSessions = await AIProvider.session.getSessions(
|
||||
this.doc.workspace.id,
|
||||
this.doc.id,
|
||||
{ action: false, fork: false, limit: 1 }
|
||||
);
|
||||
// sessions is descending ordered by updatedAt
|
||||
// the first item is the latest session
|
||||
this.session = docSessions?.[0] ?? null;
|
||||
const session = docSessions?.[0];
|
||||
this.setSession(session);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -186,7 +228,7 @@ export class ChatPanel extends SignalWatcher(
|
||||
this.doc.workspace.id,
|
||||
sessionId
|
||||
);
|
||||
this.session = session ?? null;
|
||||
this.setSession(session);
|
||||
}
|
||||
return this.session;
|
||||
};
|
||||
@@ -197,7 +239,7 @@ export class ChatPanel extends SignalWatcher(
|
||||
this.doc.workspace.id,
|
||||
options.sessionId
|
||||
);
|
||||
this.session = session ?? null;
|
||||
this.setSession(session);
|
||||
};
|
||||
|
||||
private readonly newSession = () => {
|
||||
@@ -208,12 +250,31 @@ export class ChatPanel extends SignalWatcher(
|
||||
};
|
||||
|
||||
private readonly openSession = async (sessionId: string) => {
|
||||
if (this.session?.id === sessionId) {
|
||||
return;
|
||||
}
|
||||
this.resetPanel();
|
||||
const session = await AIProvider.session?.getSession(
|
||||
this.doc.workspace.id,
|
||||
sessionId
|
||||
);
|
||||
this.session = session ?? null;
|
||||
this.setSession(session);
|
||||
};
|
||||
|
||||
private readonly openDoc = async (docId: string, sessionId: string) => {
|
||||
if (this.doc.id === docId) {
|
||||
if (this.session?.id === sessionId || this.session?.pinned) {
|
||||
return;
|
||||
}
|
||||
await this.openSession(sessionId);
|
||||
} else if (this.affineWorkbenchService) {
|
||||
const { workbench } = this.affineWorkbenchService;
|
||||
if (this.session?.pinned) {
|
||||
workbench.open(`/${docId}`, { at: 'active' });
|
||||
} else {
|
||||
workbench.open(`/${docId}?sessionId=${sessionId}`, { at: 'active' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private readonly togglePin = async () => {
|
||||
|
||||
@@ -23,6 +23,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor workspaceId!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docId: string | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onNewSession!: () => void;
|
||||
|
||||
@@ -32,6 +35,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor onOpenSession!: (sessionId: string) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onOpenDoc!: (docId: string, sessionId: string) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docDisplayConfig!: DocDisplayConfig;
|
||||
|
||||
@@ -80,9 +86,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
||||
</div>
|
||||
<div class="chat-toolbar-icon" @click=${this.onTogglePin}>
|
||||
${pinned ? PinedIcon() : PinIcon()}
|
||||
<affine-tooltip
|
||||
>${pinned ? 'Unpin this Chat' : 'Pin this Chat'}</affine-tooltip
|
||||
>
|
||||
<affine-tooltip>
|
||||
${pinned ? 'Unpin this Chat' : 'Pin this Chat'}
|
||||
</affine-tooltip>
|
||||
</div>
|
||||
<div
|
||||
class="chat-toolbar-icon history-button"
|
||||
@@ -126,12 +132,24 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
||||
};
|
||||
|
||||
private readonly onSessionClick = async (sessionId: string) => {
|
||||
if (this.session?.id === sessionId) {
|
||||
this.notification?.toast('You are already in this chat');
|
||||
return;
|
||||
}
|
||||
const confirm = await this.unpinConfirm();
|
||||
if (confirm) {
|
||||
this.onOpenSession(sessionId);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly onDocClick = async (docId: string, sessionId: string) => {
|
||||
if (this.docId === docId && this.session?.id === sessionId) {
|
||||
this.notification?.toast('You are already in this chat');
|
||||
return;
|
||||
}
|
||||
this.onOpenDoc(docId, sessionId);
|
||||
};
|
||||
|
||||
private readonly toggleHistoryMenu = () => {
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
@@ -150,6 +168,7 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
||||
.workspaceId=${this.workspaceId}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.onSessionClick=${this.onSessionClick}
|
||||
.onDocClick=${this.onDocClick}
|
||||
.notification=${this.notification}
|
||||
></ai-session-history>
|
||||
`,
|
||||
|
||||
@@ -45,17 +45,22 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
||||
}
|
||||
|
||||
.ai-session-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 24px;
|
||||
padding: 2px 4px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ai-session-item:hover {
|
||||
.ai-session-item:hover:not(:has(.ai-session-doc:hover)) {
|
||||
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||
}
|
||||
|
||||
.ai-session-doc:hover {
|
||||
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||
border-color: ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
}
|
||||
|
||||
.ai-session-title {
|
||||
@@ -75,6 +80,7 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
@@ -110,6 +116,9 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor onSessionClick!: (sessionId: string) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onDocClick!: (docId: string, sessionId: string) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor notification: NotificationService | null | undefined;
|
||||
|
||||
@@ -194,10 +203,20 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
||||
return html`
|
||||
<div
|
||||
class="ai-session-item"
|
||||
@click=${() => this.onSessionClick(session.sessionId)}
|
||||
@click=${(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
this.onSessionClick(session.sessionId);
|
||||
}}
|
||||
>
|
||||
<div class="ai-session-title">${session.sessionId}</div>
|
||||
${session.docId ? this.renderSessionDoc(session.docId) : nothing}
|
||||
<div class="ai-session-title">
|
||||
${session.sessionId}
|
||||
<affine-tooltip .offsetX=${60}>
|
||||
Click to open this chat
|
||||
</affine-tooltip>
|
||||
</div>
|
||||
${session.docId
|
||||
? this.renderSessionDoc(session.docId, session.sessionId)
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
@@ -205,12 +224,19 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSessionDoc(docId: string) {
|
||||
private renderSessionDoc(docId: string, sessionId: string) {
|
||||
const getIcon = this.docDisplayConfig.getIcon(docId);
|
||||
const docIcon = typeof getIcon === 'function' ? getIcon() : getIcon;
|
||||
return html`<div class="ai-session-doc">
|
||||
return html`<div
|
||||
class="ai-session-doc"
|
||||
@click=${(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
this.onDocClick(docId, sessionId);
|
||||
}}
|
||||
>
|
||||
${docIcon}
|
||||
<span class="doc-title">${this.docDisplayConfig.getTitle(docId)}</span>
|
||||
<span class="doc-title"> ${this.docDisplayConfig.getTitle(docId)} </span>
|
||||
<affine-tooltip>Open this doc</affine-tooltip>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -155,9 +155,9 @@ export class AIErrorWrapper extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
>
|
||||
${this.actionText}
|
||||
${this.actionTooltip
|
||||
? html`<affine-tooltip tip-position="top"
|
||||
>${this.actionTooltip}</affine-tooltip
|
||||
>`
|
||||
? html`<affine-tooltip tip-position="top">
|
||||
${this.actionTooltip}
|
||||
</affine-tooltip>`
|
||||
: nothing}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -182,13 +182,12 @@ export class AIPanelError extends WithDisposable(LitElement) {
|
||||
() => {
|
||||
const tip = this.config.error?.message;
|
||||
const error = tip
|
||||
? html`<span class="error-tip"
|
||||
>An error occurred<affine-tooltip
|
||||
tip-position="bottom-start"
|
||||
.arrow=${false}
|
||||
>${tip}</affine-tooltip
|
||||
></span
|
||||
>`
|
||||
? html`<span class="error-tip">
|
||||
An error occurred
|
||||
<affine-tooltip tip-position="bottom-start">
|
||||
${tip}
|
||||
</affine-tooltip>
|
||||
</span>`
|
||||
: 'An error occurred';
|
||||
return html`
|
||||
<style>
|
||||
|
||||
@@ -251,9 +251,9 @@ export class AIPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
@pointerdown=${stopPropagation}
|
||||
>
|
||||
${PublishIcon()}
|
||||
<affine-tooltip .offset=${12}
|
||||
>Toggle Network Search</affine-tooltip
|
||||
>
|
||||
<affine-tooltip .offsetY=${12}>
|
||||
Toggle Network Search
|
||||
</affine-tooltip>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
@@ -265,7 +265,7 @@ export class AIPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
>
|
||||
${SendIcon()}
|
||||
${this._hasContent
|
||||
? html`<affine-tooltip .offset=${12}>Send to AI</affine-tooltip>`
|
||||
? html`<affine-tooltip .offsetY=${12}>Send to AI</affine-tooltip>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,6 +84,8 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
||||
chatPanelRef.current.affineWorkspaceDialogService = framework.get(
|
||||
WorkspaceDialogService
|
||||
);
|
||||
chatPanelRef.current.affineWorkbenchService =
|
||||
framework.get(WorkbenchService);
|
||||
|
||||
containerRef.current?.append(chatPanelRef.current);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user