feat(ai-chat): show creation time and origin document in chat list
Each chat row in the AI-chat history now shows a dimmed second line with
how long ago the chat was created and the document it was created in
("N ago / <document>", or "No document" when started outside a page).
Server:
- New migration: nullable ai_chats.page_id (FK pages.id, ON DELETE SET NULL).
- Capture the origin page at chat creation from the client-supplied openPage,
but validate it first: it must be a real page in the same workspace that the
user may read (PageAccessService.validateCanView), else null. This keeps the
"openPage.id is attacker-controllable but harmless" invariant - preventing a
cross-workspace/cross-space page-title leak and a post-hijack FK crash.
- findByCreator left-joins pages (scoped by workspace, defense-in-depth) and
returns pageTitle.
Client:
- IAiChat gains pageId/pageTitle; ConversationList renders a ChatMetaLine
(useTimeAgo + origin document) as a dimmed second line.
- Add i18n key "No document" (en-US, ru-RU).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { type Kysely } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
// The document a chat was created in (the user's open page at first message).
|
||||
// Informational provenance shown in the chat-history list. NULL => the chat
|
||||
// was started outside any document. ON DELETE SET NULL: a hard-deleted page
|
||||
// degrades the chat to "no document" instead of breaking it.
|
||||
await db.schema
|
||||
.alterTable('ai_chats')
|
||||
.addColumn('page_id', 'uuid', (col) =>
|
||||
col.references('pages.id').onDelete('set null'),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.alterTable('ai_chats').dropColumn('page_id').execute();
|
||||
}
|
||||
@@ -43,10 +43,21 @@ export class AiChatRepo {
|
||||
.on('aiAgentRoles.deletedAt', 'is', null)
|
||||
.on('aiAgentRoles.enabled', '=', true),
|
||||
)
|
||||
// Left-join the origin page for its title (provenance shown in the list).
|
||||
// Scoped to the chat's workspace as defense-in-depth so a page id can only
|
||||
// ever surface a same-workspace title. No deletedAt filter: a soft-deleted
|
||||
// page keeps showing its historical title; a hard-deleted page already
|
||||
// nulls aiChats.pageId via the FK.
|
||||
.leftJoin('pages', (join) =>
|
||||
join
|
||||
.onRef('pages.id', '=', 'aiChats.pageId')
|
||||
.onRef('pages.workspaceId', '=', 'aiChats.workspaceId'),
|
||||
)
|
||||
.selectAll('aiChats')
|
||||
.select([
|
||||
'aiAgentRoles.name as roleName',
|
||||
'aiAgentRoles.emoji as roleEmoji',
|
||||
'pages.title as pageTitle',
|
||||
])
|
||||
.where('aiChats.creatorId', '=', creatorId)
|
||||
.where('aiChats.workspaceId', '=', workspaceId)
|
||||
|
||||
+3
@@ -575,6 +575,9 @@ export interface AiChats {
|
||||
// chat to universal instead of breaking it. Resolved from this column on every
|
||||
// turn — NOT from the request body.
|
||||
roleId: string | null;
|
||||
// The document the chat was created in (open page at first message). NULL =>
|
||||
// started outside any document. ON DELETE SET NULL on the page FK.
|
||||
pageId: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
deletedAt: Timestamp | null;
|
||||
|
||||
Reference in New Issue
Block a user