mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 09:37:28 +00:00
✨ feat(topic): add completed status with dropdown action and filter (#14005)
Some checks are pending
E2E CI / Check Duplicate Run (push) Waiting to run
E2E CI / Test Web App (push) Blocked by required conditions
Release Desktop Canary / Calculate Canary Version (push) Waiting to run
Release Desktop Canary / Code quality check (push) Blocked by required conditions
Release Desktop Canary / Build Desktop App (push) Blocked by required conditions
Release Desktop Canary / Merge macOS Release Files (push) Blocked by required conditions
Release Desktop Canary / Publish Canary Release (push) Blocked by required conditions
Release Desktop Canary / Publish to S3 (push) Blocked by required conditions
Release Desktop Canary / Cleanup Old Canary Releases (push) Blocked by required conditions
Release ModelBank / Build ModelBank (push) Waiting to run
Release ModelBank / Publish ModelBank (push) Blocked by required conditions
Test CI / Check Duplicate Run (push) Waiting to run
Test CI / Test Packages (push) Blocked by required conditions
Test CI / Test App (shard 1/3) (push) Blocked by required conditions
Test CI / Test App (shard 2/3) (push) Blocked by required conditions
Test CI / Test App (shard 3/3) (push) Blocked by required conditions
Test CI / Merge and Upload App Coverage (push) Blocked by required conditions
Test CI / Test Desktop App (push) Blocked by required conditions
Test CI / Test Database (push) Blocked by required conditions
Some checks are pending
E2E CI / Check Duplicate Run (push) Waiting to run
E2E CI / Test Web App (push) Blocked by required conditions
Release Desktop Canary / Calculate Canary Version (push) Waiting to run
Release Desktop Canary / Code quality check (push) Blocked by required conditions
Release Desktop Canary / Build Desktop App (push) Blocked by required conditions
Release Desktop Canary / Merge macOS Release Files (push) Blocked by required conditions
Release Desktop Canary / Publish Canary Release (push) Blocked by required conditions
Release Desktop Canary / Publish to S3 (push) Blocked by required conditions
Release Desktop Canary / Cleanup Old Canary Releases (push) Blocked by required conditions
Release ModelBank / Build ModelBank (push) Waiting to run
Release ModelBank / Publish ModelBank (push) Blocked by required conditions
Test CI / Check Duplicate Run (push) Waiting to run
Test CI / Test Packages (push) Blocked by required conditions
Test CI / Test App (shard 1/3) (push) Blocked by required conditions
Test CI / Test App (shard 2/3) (push) Blocked by required conditions
Test CI / Test App (shard 3/3) (push) Blocked by required conditions
Test CI / Merge and Upload App Coverage (push) Blocked by required conditions
Test CI / Test Desktop App (push) Blocked by required conditions
Test CI / Test Database (push) Blocked by required conditions
* ✨ feat(topic): add completed status with dropdown action and filter - Surface ChatTopicStatus (active/completed/archived) on topic list items and pass to dropdown menu - Add markTopicCompleted / unmarkTopicCompleted store actions wired into the topic item dropdown - Show CheckCircle2 icon on completed topics in the sidebar list - Add topicIncludeCompleted user preference (default false) and an "Include Completed" toggle in the topic filter menu (agent + group routes) - Wire excludeStatuses and triggers filters through TopicModel, TRPC router, service, and store SWR keys so completed topics are excluded by default Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🌐 i18n(topic): add zh-CN/en-US for completed status keys Translate actions.markCompleted / actions.unmarkCompleted and filter.filter / filter.showCompleted for dev preview. CI's pnpm i18n will fill in remaining locales. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(topic): scope completed exclusion to routes with the toggle Move the topicIncludeCompleted preference read out of the chat-store useFetchTopics action and into the (main) agent/group sidebars where the "Include Completed" filter actually lives. Popup and mobile topic views call useFetchTopics without excludeStatuses, so completed topics remain reachable on surfaces that don't expose the toggle (e.g. the popup window for a deep-linked completed topic, the mobile TopicModal). Also switch ChatTopicStatus imports in the topic item / dropdown files to @lobechat/types to match the rest of the topic-feature imports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(topic-model): cover excludeStatuses + triggers filters Add cases to the TopicModel.query suite for the new params introduced alongside the topic.status column: - triggers (positive trigger filter) on the container branch - excludeStatuses on the container, agent, and groupId branches (verifies null status rows are still returned) - status / completedAt are populated on returned items Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(topic): move "Mark Completed" to top of agent topic dropdown Promote the completed-status toggle to the first menu item, with a divider before favorite, so the most-used status action sits at the top of the dropdown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
61224fe76c
commit
c0db58e622
31 changed files with 452 additions and 41 deletions
|
|
@ -15,20 +15,24 @@
|
|||
"actions.export": "Export Topics",
|
||||
"actions.favorite": "Favorite",
|
||||
"actions.import": "Import Conversation",
|
||||
"actions.markCompleted": "Mark as Completed",
|
||||
"actions.openInNewTab": "Open in New Tab",
|
||||
"actions.openInNewWindow": "Open in a new window",
|
||||
"actions.removeAll": "Delete All Topics",
|
||||
"actions.removeUnstarred": "Delete Unstarred Topics",
|
||||
"actions.unfavorite": "Unfavorite",
|
||||
"actions.unmarkCompleted": "Mark as Active",
|
||||
"defaultTitle": "Default Topic",
|
||||
"displayItems": "Display Items",
|
||||
"duplicateLoading": "Copying Topic...",
|
||||
"duplicateSuccess": "Topic Copied Successfully",
|
||||
"favorite": "Favorite",
|
||||
"filter.filter": "Filter",
|
||||
"filter.groupMode.byProject": "By project",
|
||||
"filter.groupMode.byTime": "By time",
|
||||
"filter.groupMode.flat": "Flat",
|
||||
"filter.organize": "Organize",
|
||||
"filter.showCompleted": "Include Completed",
|
||||
"filter.sort": "Sort by",
|
||||
"filter.sortBy.createdAt": "Created time",
|
||||
"filter.sortBy.updatedAt": "Updated time",
|
||||
|
|
|
|||
|
|
@ -15,20 +15,24 @@
|
|||
"actions.export": "导出话题",
|
||||
"actions.favorite": "收藏",
|
||||
"actions.import": "导入对话",
|
||||
"actions.markCompleted": "标为已完成",
|
||||
"actions.openInNewTab": "在新标签页中打开",
|
||||
"actions.openInNewWindow": "打开独立窗口",
|
||||
"actions.removeAll": "删除全部话题",
|
||||
"actions.removeUnstarred": "删除未收藏话题",
|
||||
"actions.unfavorite": "取消收藏",
|
||||
"actions.unmarkCompleted": "标为进行中",
|
||||
"defaultTitle": "默认话题",
|
||||
"displayItems": "显示条目",
|
||||
"duplicateLoading": "话题复制中…",
|
||||
"duplicateSuccess": "话题复制成功",
|
||||
"favorite": "收藏",
|
||||
"filter.filter": "筛选",
|
||||
"filter.groupMode.byProject": "按项目",
|
||||
"filter.groupMode.byTime": "按时间阶段",
|
||||
"filter.groupMode.flat": "平铺",
|
||||
"filter.organize": "整理",
|
||||
"filter.showCompleted": "显示已完成",
|
||||
"filter.sort": "排序",
|
||||
"filter.sortBy.createdAt": "按创建时间",
|
||||
"filter.sortBy.updatedAt": "按更新时间",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export const DEFAULT_PREFERENCE: UserPreference = {
|
|||
enableInputMarkdown: true,
|
||||
},
|
||||
topicGroupMode: 'byTime',
|
||||
topicIncludeCompleted: false,
|
||||
topicSortBy: 'updatedAt',
|
||||
useCmdEnterToSend: false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -207,6 +207,134 @@ describe('TopicModel - Query', () => {
|
|||
expect(ids).toContain('null-trigger');
|
||||
expect(ids).not.toContain('cron-topic');
|
||||
});
|
||||
|
||||
it('should only return topics with matching triggers when triggers is set', async () => {
|
||||
await serverDB.insert(topics).values([
|
||||
{ id: 'normal-topic', sessionId, userId, title: 'Normal' },
|
||||
{ id: 'cron-topic', sessionId, userId, title: 'Cron', trigger: 'cron' },
|
||||
{ id: 'eval-topic', sessionId, userId, title: 'Eval', trigger: 'eval' },
|
||||
]);
|
||||
|
||||
const result = await topicModel.query({
|
||||
containerId: sessionId,
|
||||
triggers: ['cron'],
|
||||
});
|
||||
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.items[0].id).toBe('cron-topic');
|
||||
});
|
||||
|
||||
it('should exclude topics with matching status via excludeStatuses, keeping null status', async () => {
|
||||
const completedAt = new Date('2024-01-05');
|
||||
await serverDB.insert(topics).values([
|
||||
{ id: 'active-topic', sessionId, userId, title: 'Active', status: 'active' },
|
||||
{
|
||||
id: 'completed-topic',
|
||||
sessionId,
|
||||
userId,
|
||||
title: 'Completed',
|
||||
status: 'completed',
|
||||
completedAt,
|
||||
},
|
||||
{ id: 'archived-topic', sessionId, userId, title: 'Archived', status: 'archived' },
|
||||
{ id: 'null-status-topic', sessionId, userId, title: 'No status' },
|
||||
]);
|
||||
|
||||
const result = await topicModel.query({
|
||||
containerId: sessionId,
|
||||
excludeStatuses: ['completed'],
|
||||
});
|
||||
|
||||
const ids = result.items.map((t) => t.id);
|
||||
expect(ids).toHaveLength(3);
|
||||
expect(ids).toContain('active-topic');
|
||||
expect(ids).toContain('archived-topic');
|
||||
expect(ids).toContain('null-status-topic');
|
||||
expect(ids).not.toContain('completed-topic');
|
||||
});
|
||||
|
||||
it('should select status and completedAt on returned topics', async () => {
|
||||
const completedAt = new Date('2024-02-01T10:00:00Z');
|
||||
await serverDB.insert(topics).values([
|
||||
{
|
||||
id: 'with-status',
|
||||
sessionId,
|
||||
userId,
|
||||
title: 'With Status',
|
||||
status: 'completed',
|
||||
completedAt,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await topicModel.query({ containerId: sessionId });
|
||||
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.items[0].status).toBe('completed');
|
||||
expect(result.items[0].completedAt?.toISOString()).toBe(completedAt.toISOString());
|
||||
});
|
||||
|
||||
it('should apply excludeStatuses on the agent query branch', async () => {
|
||||
await serverDB.transaction(async (trx) => {
|
||||
await trx.insert(agents).values([{ id: 'status-agent', userId, title: 'Status Agent' }]);
|
||||
await trx.insert(topics).values([
|
||||
{
|
||||
id: 'agent-active',
|
||||
userId,
|
||||
agentId: 'status-agent',
|
||||
status: 'active',
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
},
|
||||
{
|
||||
id: 'agent-completed',
|
||||
userId,
|
||||
agentId: 'status-agent',
|
||||
status: 'completed',
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const result = await topicModel.query({
|
||||
agentId: 'status-agent',
|
||||
excludeStatuses: ['completed'],
|
||||
});
|
||||
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.items[0].id).toBe('agent-active');
|
||||
expect(result.total).toBe(1);
|
||||
});
|
||||
|
||||
it('should apply excludeStatuses on the groupId query branch', async () => {
|
||||
await serverDB.transaction(async (trx) => {
|
||||
await trx
|
||||
.insert(chatGroups)
|
||||
.values([{ id: 'status-group', title: 'Status Group', userId }]);
|
||||
await trx.insert(topics).values([
|
||||
{
|
||||
id: 'group-active',
|
||||
userId,
|
||||
groupId: 'status-group',
|
||||
status: 'active',
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
},
|
||||
{
|
||||
id: 'group-completed',
|
||||
userId,
|
||||
groupId: 'status-group',
|
||||
status: 'completed',
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const result = await topicModel.query({
|
||||
groupId: 'status-group',
|
||||
excludeStatuses: ['completed'],
|
||||
});
|
||||
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.items[0].id).toBe('group-active');
|
||||
});
|
||||
});
|
||||
|
||||
describe('query with agentId filter', () => {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ interface QueryTopicParams {
|
|||
*/
|
||||
containerId?: string | null;
|
||||
current?: number;
|
||||
/**
|
||||
* Exclude topics by status (e.g. ['completed'])
|
||||
*/
|
||||
excludeStatuses?: string[];
|
||||
/**
|
||||
* Exclude topics by trigger types (e.g. ['cron'])
|
||||
*/
|
||||
|
|
@ -42,6 +46,10 @@ interface QueryTopicParams {
|
|||
*/
|
||||
isInbox?: boolean;
|
||||
pageSize?: number;
|
||||
/**
|
||||
* Include only topics matching the given trigger types (positive filter)
|
||||
*/
|
||||
triggers?: string[];
|
||||
}
|
||||
|
||||
export interface ListTopicsForMemoryExtractorCursor {
|
||||
|
|
@ -63,16 +71,27 @@ export class TopicModel {
|
|||
agentId,
|
||||
containerId,
|
||||
current = 0,
|
||||
excludeStatuses,
|
||||
excludeTriggers,
|
||||
pageSize = 9999,
|
||||
groupId,
|
||||
isInbox,
|
||||
triggers,
|
||||
}: QueryTopicParams = {}) => {
|
||||
const offset = current * pageSize;
|
||||
const excludeTriggerCondition =
|
||||
excludeTriggers && excludeTriggers.length > 0
|
||||
? or(isNull(topics.trigger), not(inArray(topics.trigger, excludeTriggers)))
|
||||
: undefined;
|
||||
const triggerCondition =
|
||||
triggers && triggers.length > 0 ? inArray(topics.trigger, triggers) : undefined;
|
||||
const excludeStatusCondition =
|
||||
excludeStatuses && excludeStatuses.length > 0
|
||||
? or(
|
||||
isNull(topics.status),
|
||||
not(inArray(topics.status, excludeStatuses as ('active' | 'completed' | 'archived')[])),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
// If groupId is provided, query topics by groupId directly
|
||||
if (groupId) {
|
||||
|
|
@ -80,16 +99,20 @@ export class TopicModel {
|
|||
eq(topics.userId, this.userId),
|
||||
eq(topics.groupId, groupId),
|
||||
excludeTriggerCondition,
|
||||
triggerCondition,
|
||||
excludeStatusCondition,
|
||||
);
|
||||
|
||||
const [items, totalResult] = await Promise.all([
|
||||
this.db
|
||||
.select({
|
||||
completedAt: topics.completedAt,
|
||||
createdAt: topics.createdAt,
|
||||
favorite: topics.favorite,
|
||||
historySummary: topics.historySummary,
|
||||
id: topics.id,
|
||||
metadata: topics.metadata,
|
||||
status: topics.status,
|
||||
title: topics.title,
|
||||
updatedAt: topics.updatedAt,
|
||||
})
|
||||
|
|
@ -145,26 +168,36 @@ export class TopicModel {
|
|||
|
||||
// Fetch items and total count in parallel
|
||||
// Include sessionId and agentId for migration detection
|
||||
const agentWhere = and(
|
||||
eq(topics.userId, this.userId),
|
||||
agentCondition,
|
||||
excludeTriggerCondition,
|
||||
triggerCondition,
|
||||
excludeStatusCondition,
|
||||
);
|
||||
|
||||
const [items, totalResult] = await Promise.all([
|
||||
this.db
|
||||
.select({
|
||||
completedAt: topics.completedAt,
|
||||
createdAt: topics.createdAt,
|
||||
favorite: topics.favorite,
|
||||
historySummary: topics.historySummary,
|
||||
id: topics.id,
|
||||
metadata: topics.metadata,
|
||||
status: topics.status,
|
||||
title: topics.title,
|
||||
updatedAt: topics.updatedAt,
|
||||
})
|
||||
.from(topics)
|
||||
.where(and(eq(topics.userId, this.userId), agentCondition, excludeTriggerCondition))
|
||||
.where(agentWhere)
|
||||
.orderBy(desc(topics.favorite), desc(topics.updatedAt))
|
||||
.limit(pageSize)
|
||||
.offset(offset),
|
||||
this.db
|
||||
.select({ count: count(topics.id) })
|
||||
.from(topics)
|
||||
.where(and(eq(topics.userId, this.userId), agentCondition, excludeTriggerCondition)),
|
||||
.where(agentWhere),
|
||||
]);
|
||||
|
||||
return { items, total: totalResult[0].count };
|
||||
|
|
@ -175,18 +208,22 @@ export class TopicModel {
|
|||
eq(topics.userId, this.userId),
|
||||
this.matchContainer(containerId),
|
||||
excludeTriggerCondition,
|
||||
triggerCondition,
|
||||
excludeStatusCondition,
|
||||
);
|
||||
|
||||
const [items, totalResult] = await Promise.all([
|
||||
this.db
|
||||
.select({
|
||||
agentId: topics.agentId,
|
||||
completedAt: topics.completedAt,
|
||||
createdAt: topics.createdAt,
|
||||
favorite: topics.favorite,
|
||||
historySummary: topics.historySummary,
|
||||
id: topics.id,
|
||||
metadata: topics.metadata,
|
||||
sessionId: topics.sessionId,
|
||||
status: topics.status,
|
||||
title: topics.title,
|
||||
updatedAt: topics.updatedAt,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -104,11 +104,15 @@ export interface ChatTopicSummary {
|
|||
provider: string;
|
||||
}
|
||||
|
||||
export type ChatTopicStatus = 'active' | 'completed' | 'archived';
|
||||
|
||||
export interface ChatTopic extends Omit<BaseDataModel, 'meta'> {
|
||||
completedAt?: Date | null;
|
||||
favorite?: boolean;
|
||||
historySummary?: string;
|
||||
metadata?: ChatTopicMetadata;
|
||||
sessionId?: string;
|
||||
status?: ChatTopicStatus | null;
|
||||
title: string;
|
||||
trigger?: string | null;
|
||||
}
|
||||
|
|
@ -160,6 +164,10 @@ export interface CreateTopicParams {
|
|||
export interface QueryTopicParams {
|
||||
agentId?: string | null;
|
||||
current?: number;
|
||||
/**
|
||||
* Exclude topics by status (e.g. ['completed'])
|
||||
*/
|
||||
excludeStatuses?: string[];
|
||||
/**
|
||||
* Exclude topics by trigger types (e.g. ['cron'])
|
||||
*/
|
||||
|
|
@ -174,6 +182,10 @@ export interface QueryTopicParams {
|
|||
*/
|
||||
isInbox?: boolean;
|
||||
pageSize?: number;
|
||||
/**
|
||||
* Include only topics matching the given trigger types (positive filter)
|
||||
*/
|
||||
triggers?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ export interface UserPreference {
|
|||
*/
|
||||
telemetry?: boolean | null;
|
||||
topicGroupMode?: TopicGroupMode;
|
||||
/**
|
||||
* whether to include completed topics in the topic list
|
||||
*/
|
||||
topicIncludeCompleted?: boolean;
|
||||
topicSortBy?: TopicSortBy;
|
||||
/**
|
||||
* whether to use cmd + enter to send message
|
||||
|
|
@ -138,6 +142,7 @@ export const UserPreferenceSchema = z
|
|||
lab: UserLabSchema.optional(),
|
||||
telemetry: z.boolean().nullable(),
|
||||
topicGroupMode: z.enum(['byTime', 'byProject', 'flat']).optional(),
|
||||
topicIncludeCompleted: z.boolean().optional(),
|
||||
topicSortBy: z.enum(['createdAt', 'updatedAt']).optional(),
|
||||
useCmdEnterToSend: z.boolean().optional(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ import { systemStatusSelectors } from '@/store/global/selectors';
|
|||
/**
|
||||
* Fetch topics for the current session (agent or group)
|
||||
*/
|
||||
export const useFetchTopics = (options?: { excludeTriggers?: string[] }) => {
|
||||
export const useFetchTopics = (options?: {
|
||||
excludeStatuses?: string[];
|
||||
excludeTriggers?: string[];
|
||||
}) => {
|
||||
const isInbox = useAgentStore(builtinAgentSelectors.isInboxAgent);
|
||||
const [activeAgentId, activeGroupId, useFetchTopicsHook] = useChatStore((s) => [
|
||||
s.activeAgentId,
|
||||
|
|
@ -20,6 +23,9 @@ export const useFetchTopics = (options?: { excludeTriggers?: string[] }) => {
|
|||
// If in group session, use groupId; otherwise use agentId
|
||||
const { isValidating, data } = useFetchTopicsHook(true, {
|
||||
agentId: activeAgentId,
|
||||
...(options?.excludeStatuses && options.excludeStatuses.length > 0
|
||||
? { excludeStatuses: options.excludeStatuses }
|
||||
: {}),
|
||||
...(options?.excludeTriggers && options.excludeTriggers.length > 0
|
||||
? { excludeTriggers: options.excludeTriggers }
|
||||
: {}),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ export default {
|
|||
'actions.duplicate': 'Duplicate',
|
||||
'actions.favorite': 'Favorite',
|
||||
'actions.unfavorite': 'Unfavorite',
|
||||
'actions.markCompleted': 'Mark as Completed',
|
||||
'actions.unmarkCompleted': 'Mark as Active',
|
||||
'actions.export': 'Export Topics',
|
||||
'actions.import': 'Import Conversation',
|
||||
'actions.openInNewTab': 'Open in New Tab',
|
||||
|
|
@ -26,10 +28,12 @@ export default {
|
|||
'duplicateLoading': 'Copying Topic...',
|
||||
'duplicateSuccess': 'Topic Copied Successfully',
|
||||
'favorite': 'Favorite',
|
||||
'filter.filter': 'Filter',
|
||||
'filter.groupMode.byProject': 'By project',
|
||||
'filter.groupMode.byTime': 'By time',
|
||||
'filter.groupMode.flat': 'Flat',
|
||||
'filter.organize': 'Organize',
|
||||
'filter.showCompleted': 'Include Completed',
|
||||
'filter.sort': 'Sort by',
|
||||
'filter.sortBy.createdAt': 'Created time',
|
||||
'filter.sortBy.updatedAt': 'Updated time',
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ const Content = memo<ContentProps>(({ open, searchKeyword }) => {
|
|||
fav={topic.favorite}
|
||||
id={topic.id}
|
||||
metadata={topic.metadata}
|
||||
status={topic.status}
|
||||
threadId={activeThreadId}
|
||||
title={topic.title}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { ChatTopicMetadata, ChatTopicStatus } from '@lobechat/types';
|
||||
import { Flexbox, Icon, Skeleton, Tag } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar, keyframes, useTheme } from 'antd-style';
|
||||
import { HashIcon, MessageSquareDashed } from 'lucide-react';
|
||||
import { CheckCircle2, HashIcon, MessageSquareDashed } from 'lucide-react';
|
||||
import { memo, Suspense, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
|
@ -14,7 +15,6 @@ import { useAgentStore } from '@/store/agent';
|
|||
import { useChatStore } from '@/store/chat';
|
||||
import { operationSelectors } from '@/store/chat/selectors';
|
||||
import { useElectronStore } from '@/store/electron';
|
||||
import type { ChatTopicMetadata } from '@/types/topic';
|
||||
|
||||
import { useTopicNavigation } from '../../hooks/useTopicNavigation';
|
||||
import ThreadList from '../../TopicListContent/ThreadList';
|
||||
|
|
@ -75,11 +75,12 @@ interface TopicItemProps {
|
|||
fav?: boolean;
|
||||
id?: string;
|
||||
metadata?: ChatTopicMetadata;
|
||||
status?: ChatTopicStatus | null;
|
||||
threadId?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId, metadata }) => {
|
||||
const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId, metadata, status }) => {
|
||||
const { t } = useTranslation('topic');
|
||||
const { isDarkMode } = useTheme();
|
||||
const activeAgentId = useAgentStore((s) => s.activeAgentId);
|
||||
|
|
@ -147,9 +148,12 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId, meta
|
|||
const { dropdownMenu } = useTopicItemDropdownMenu({
|
||||
fav,
|
||||
id,
|
||||
status,
|
||||
title,
|
||||
});
|
||||
|
||||
const isCompleted = status === 'completed';
|
||||
|
||||
const hasUnread = id && isUnreadCompleted;
|
||||
const unreadIcon = (
|
||||
<span className={styles.unreadWrapper}>
|
||||
|
|
@ -212,6 +216,15 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId, meta
|
|||
/>
|
||||
);
|
||||
}
|
||||
if (isCompleted) {
|
||||
return (
|
||||
<Icon
|
||||
icon={CheckCircle2}
|
||||
size={'small'}
|
||||
style={{ color: cssVar.colorTextDescription }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (hasUnread) return unreadIcon;
|
||||
if (metadata?.bot?.platform) {
|
||||
const ProviderIcon = getPlatformIcon(metadata.bot!.platform);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import type { ChatTopicStatus } from '@lobechat/types';
|
||||
import { type MenuProps } from '@lobehub/ui';
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import {
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
ExternalLink,
|
||||
Link2,
|
||||
LucideCopy,
|
||||
|
|
@ -29,10 +32,16 @@ import { useGlobalStore } from '@/store/global';
|
|||
interface TopicItemDropdownMenuProps {
|
||||
fav?: boolean;
|
||||
id?: string;
|
||||
status?: ChatTopicStatus | null;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const useTopicItemDropdownMenu = ({ fav, id, title }: TopicItemDropdownMenuProps) => {
|
||||
export const useTopicItemDropdownMenu = ({
|
||||
fav,
|
||||
id,
|
||||
status,
|
||||
title,
|
||||
}: TopicItemDropdownMenuProps) => {
|
||||
const { t } = useTranslation(['topic', 'common']);
|
||||
const { modal, message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -42,14 +51,25 @@ export const useTopicItemDropdownMenu = ({ fav, id, title }: TopicItemDropdownMe
|
|||
const addTab = useElectronStore((s) => s.addTab);
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
const [autoRenameTopicTitle, duplicateTopic, removeTopic, favoriteTopic, updateTopicTitle] =
|
||||
useChatStore((s) => [
|
||||
s.autoRenameTopicTitle,
|
||||
s.duplicateTopic,
|
||||
s.removeTopic,
|
||||
s.favoriteTopic,
|
||||
s.updateTopicTitle,
|
||||
]);
|
||||
const [
|
||||
autoRenameTopicTitle,
|
||||
duplicateTopic,
|
||||
removeTopic,
|
||||
favoriteTopic,
|
||||
markTopicCompleted,
|
||||
unmarkTopicCompleted,
|
||||
updateTopicTitle,
|
||||
] = useChatStore((s) => [
|
||||
s.autoRenameTopicTitle,
|
||||
s.duplicateTopic,
|
||||
s.removeTopic,
|
||||
s.favoriteTopic,
|
||||
s.markTopicCompleted,
|
||||
s.unmarkTopicCompleted,
|
||||
s.updateTopicTitle,
|
||||
]);
|
||||
|
||||
const isCompleted = status === 'completed';
|
||||
const handleOpenShareModal = useCallback(() => {
|
||||
if (!id) return;
|
||||
|
||||
|
|
@ -60,6 +80,21 @@ export const useTopicItemDropdownMenu = ({ fav, id, title }: TopicItemDropdownMe
|
|||
if (!id) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
icon: <Icon icon={isCompleted ? Circle : CheckCircle2} />,
|
||||
key: 'markCompleted',
|
||||
label: isCompleted ? t('actions.unmarkCompleted') : t('actions.markCompleted'),
|
||||
onClick: () => {
|
||||
if (isCompleted) {
|
||||
unmarkTopicCompleted(id);
|
||||
} else {
|
||||
markTopicCompleted(id);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'divider' as const,
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={Star} />,
|
||||
key: 'favorite',
|
||||
|
|
@ -177,12 +212,15 @@ export const useTopicItemDropdownMenu = ({ fav, id, title }: TopicItemDropdownMe
|
|||
}, [
|
||||
id,
|
||||
fav,
|
||||
isCompleted,
|
||||
title,
|
||||
activeAgentId,
|
||||
appOrigin,
|
||||
autoRenameTopicTitle,
|
||||
duplicateTopic,
|
||||
favoriteTopic,
|
||||
markTopicCompleted,
|
||||
unmarkTopicCompleted,
|
||||
removeTopic,
|
||||
updateTopicTitle,
|
||||
openTopicInNewWindow,
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ const GroupItem = memo<GroupItemComponentProps>(({ group, activeTopicId, activeT
|
|||
id={topic.id}
|
||||
key={topic.id}
|
||||
metadata={topic.metadata}
|
||||
status={topic.status}
|
||||
threadId={activeThreadId}
|
||||
title={topic.title}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const GroupItem = memo<GroupItemComponentProps>(({ group, activeTopicId, activeT
|
|||
id={topic.id}
|
||||
key={topic.id}
|
||||
metadata={topic.metadata}
|
||||
status={topic.status}
|
||||
threadId={activeThreadId}
|
||||
title={topic.title}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const FlatMode = memo(() => {
|
|||
id={topic.id}
|
||||
key={topic.id}
|
||||
metadata={topic.metadata}
|
||||
status={topic.status}
|
||||
threadId={activeThreadId}
|
||||
title={topic.title}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const SearchResult = memo(() => {
|
|||
id={topic.id}
|
||||
key={topic.id}
|
||||
metadata={topic.metadata}
|
||||
status={topic.status}
|
||||
title={topic.title}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import SkeletonList from '@/features/NavPanel/components/SkeletonList';
|
|||
import { useFetchTopics } from '@/hooks/useFetchTopics';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { topicSelectors } from '@/store/chat/selectors';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { preferenceSelectors } from '@/store/user/selectors';
|
||||
|
||||
import Actions from './Actions';
|
||||
import Filter from './Filter';
|
||||
|
|
@ -22,8 +24,12 @@ interface TopicProps {
|
|||
const Topic = memo<TopicProps>(({ itemKey }) => {
|
||||
const { t } = useTranslation(['topic', 'common']);
|
||||
const [topicCount] = useChatStore((s) => [topicSelectors.currentTopicCount(s)]);
|
||||
const includeCompleted = useUserStore(preferenceSelectors.topicIncludeCompleted);
|
||||
const dropdownMenu = useTopicActionsDropdownMenu();
|
||||
const { isRevalidating } = useFetchTopics({ excludeTriggers: ['cron', 'eval'] });
|
||||
const { isRevalidating } = useFetchTopics({
|
||||
excludeStatuses: includeCompleted ? undefined : ['completed'],
|
||||
excludeTriggers: ['cron', 'eval'],
|
||||
});
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
|
|
|
|||
|
|
@ -11,11 +11,14 @@ import type { TopicGroupMode, TopicSortBy } from '@/types/topic';
|
|||
export const useTopicFilterDropdownMenu = (): DropdownItem[] => {
|
||||
const { t } = useTranslation('topic');
|
||||
|
||||
const [topicGroupMode, topicSortBy, updatePreference] = useUserStore((s) => [
|
||||
preferenceSelectors.topicGroupMode(s),
|
||||
preferenceSelectors.topicSortBy(s),
|
||||
s.updatePreference,
|
||||
]);
|
||||
const [topicGroupMode, topicSortBy, topicIncludeCompleted, updatePreference] = useUserStore(
|
||||
(s) => [
|
||||
preferenceSelectors.topicGroupMode(s),
|
||||
preferenceSelectors.topicSortBy(s),
|
||||
preferenceSelectors.topicIncludeCompleted(s),
|
||||
s.updatePreference,
|
||||
],
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
const groupModes: TopicGroupMode[] = ['byTime', 'byProject', 'flat'];
|
||||
|
|
@ -49,6 +52,22 @@ export const useTopicFilterDropdownMenu = (): DropdownItem[] => {
|
|||
label: t('filter.sort'),
|
||||
type: 'group' as const,
|
||||
},
|
||||
{ type: 'divider' as const },
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: topicIncludeCompleted ? <Icon icon={LucideCheck} /> : <div />,
|
||||
key: 'showCompleted',
|
||||
label: t('filter.showCompleted'),
|
||||
onClick: () => {
|
||||
updatePreference({ topicIncludeCompleted: !topicIncludeCompleted });
|
||||
},
|
||||
},
|
||||
],
|
||||
key: 'filter',
|
||||
label: t('filter.filter'),
|
||||
type: 'group' as const,
|
||||
},
|
||||
];
|
||||
}, [topicGroupMode, topicSortBy, updatePreference, t]);
|
||||
}, [topicGroupMode, topicSortBy, topicIncludeCompleted, updatePreference, t]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ const Content = memo<ContentProps>(({ open, searchKeyword }) => {
|
|||
active={activeTopicId === topic.id}
|
||||
fav={topic.favorite}
|
||||
id={topic.id}
|
||||
status={topic.status}
|
||||
threadId={activeThreadId}
|
||||
title={topic.title}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { ChatTopicStatus } from '@lobechat/types';
|
||||
import { Flexbox, Icon, Skeleton, Tag } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import { HashIcon, Loader2Icon, MessageSquareDashed } from 'lucide-react';
|
||||
import { CheckCircle2, HashIcon, Loader2Icon, MessageSquareDashed } from 'lucide-react';
|
||||
import { AnimatePresence, m } from 'motion/react';
|
||||
import { memo, Suspense, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -61,11 +62,12 @@ interface TopicItemProps {
|
|||
active?: boolean;
|
||||
fav?: boolean;
|
||||
id?: string;
|
||||
status?: ChatTopicStatus | null;
|
||||
threadId?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) => {
|
||||
const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId, status }) => {
|
||||
const { t } = useTranslation('topic');
|
||||
const toggleMobileTopic = useGlobalStore((s) => s.toggleMobileTopic);
|
||||
const [activeGroupId, switchTopic] = useAgentGroupStore((s) => [s.activeGroupId, s.switchTopic]);
|
||||
|
|
@ -137,9 +139,12 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
|
|||
|
||||
const dropdownMenu = useTopicItemDropdownMenu({
|
||||
id,
|
||||
status,
|
||||
toggleEditing,
|
||||
});
|
||||
|
||||
const isCompleted = status === 'completed';
|
||||
|
||||
const hasUnread = id && isUnreadCompleted;
|
||||
const infoColor = cssVar.colorInfo;
|
||||
const unreadNode = (
|
||||
|
|
@ -222,13 +227,25 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
|
|||
disabled={editing}
|
||||
href={!editing ? href : undefined}
|
||||
title={title === '...' ? <DotsLoading gap={3} size={4} /> : title}
|
||||
icon={
|
||||
isLoading ? (
|
||||
<Icon spin icon={Loader2Icon} size={'small'} style={{ color: cssVar.colorWarning }} />
|
||||
) : (
|
||||
icon={(() => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Icon spin icon={Loader2Icon} size={'small'} style={{ color: cssVar.colorWarning }} />
|
||||
);
|
||||
}
|
||||
if (isCompleted) {
|
||||
return (
|
||||
<Icon
|
||||
icon={CheckCircle2}
|
||||
size={'small'}
|
||||
style={{ color: cssVar.colorTextDescription }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Icon icon={HashIcon} size={'small'} style={{ color: cssVar.colorTextDescription }} />
|
||||
)
|
||||
}
|
||||
);
|
||||
})()}
|
||||
slots={{
|
||||
iconPostfix: unreadNode,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
import type { ChatTopicStatus } from '@lobechat/types';
|
||||
import { type MenuProps } from '@lobehub/ui';
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { ExternalLink, Link2, LucideCopy, PanelTop, PencilLine, Trash, Wand2 } from 'lucide-react';
|
||||
import {
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
ExternalLink,
|
||||
Link2,
|
||||
LucideCopy,
|
||||
PanelTop,
|
||||
PencilLine,
|
||||
Trash,
|
||||
Wand2,
|
||||
} from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
|
@ -16,11 +27,13 @@ import { useGlobalStore } from '@/store/global';
|
|||
|
||||
interface TopicItemDropdownMenuProps {
|
||||
id?: string;
|
||||
status?: ChatTopicStatus | null;
|
||||
toggleEditing: (visible?: boolean) => void;
|
||||
}
|
||||
|
||||
export const useTopicItemDropdownMenu = ({
|
||||
id,
|
||||
status,
|
||||
toggleEditing,
|
||||
}: TopicItemDropdownMenuProps): (() => MenuProps['items']) => {
|
||||
const { t } = useTranslation(['topic', 'common']);
|
||||
|
|
@ -32,16 +45,41 @@ export const useTopicItemDropdownMenu = ({
|
|||
const addTab = useElectronStore((s) => s.addTab);
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
const [autoRenameTopicTitle, duplicateTopic, removeTopic] = useChatStore((s) => [
|
||||
const [
|
||||
autoRenameTopicTitle,
|
||||
duplicateTopic,
|
||||
removeTopic,
|
||||
markTopicCompleted,
|
||||
unmarkTopicCompleted,
|
||||
] = useChatStore((s) => [
|
||||
s.autoRenameTopicTitle,
|
||||
s.duplicateTopic,
|
||||
s.removeTopic,
|
||||
s.markTopicCompleted,
|
||||
s.unmarkTopicCompleted,
|
||||
]);
|
||||
|
||||
const isCompleted = status === 'completed';
|
||||
|
||||
return useCallback(() => {
|
||||
if (!id) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
icon: <Icon icon={isCompleted ? Circle : CheckCircle2} />,
|
||||
key: 'markCompleted',
|
||||
label: isCompleted ? t('actions.unmarkCompleted') : t('actions.markCompleted'),
|
||||
onClick: () => {
|
||||
if (isCompleted) {
|
||||
unmarkTopicCompleted(id);
|
||||
} else {
|
||||
markTopicCompleted(id);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'divider' as const,
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={Wand2} />,
|
||||
key: 'autoRename',
|
||||
|
|
@ -131,10 +169,13 @@ export const useTopicItemDropdownMenu = ({
|
|||
].filter(Boolean) as MenuProps['items'];
|
||||
}, [
|
||||
id,
|
||||
isCompleted,
|
||||
activeGroupId,
|
||||
appOrigin,
|
||||
autoRenameTopicTitle,
|
||||
duplicateTopic,
|
||||
markTopicCompleted,
|
||||
unmarkTopicCompleted,
|
||||
removeTopic,
|
||||
openGroupTopicInNewWindow,
|
||||
addTab,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ const GroupItem = memo<GroupItemProps>(({ group, activeTopicId, activeThreadId }
|
|||
fav={topic.favorite}
|
||||
id={topic.id}
|
||||
key={topic.id}
|
||||
status={topic.status}
|
||||
threadId={activeThreadId}
|
||||
title={topic.title}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const FlatMode = memo(() => {
|
|||
fav={topic.favorite}
|
||||
id={topic.id}
|
||||
key={topic.id}
|
||||
status={topic.status}
|
||||
threadId={activeThreadId}
|
||||
title={topic.title}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const SearchResult = memo(() => {
|
|||
fav={topic.favorite}
|
||||
id={topic.id}
|
||||
key={topic.id}
|
||||
status={topic.status}
|
||||
title={topic.title}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import SkeletonList from '@/features/NavPanel/components/SkeletonList';
|
|||
import { useFetchTopics } from '@/hooks/useFetchTopics';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { topicSelectors } from '@/store/chat/selectors';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { preferenceSelectors } from '@/store/user/selectors';
|
||||
|
||||
import Actions from './Actions';
|
||||
import Filter from './Filter';
|
||||
|
|
@ -22,8 +24,11 @@ interface TopicProps {
|
|||
const Topic = memo<TopicProps>(({ itemKey }) => {
|
||||
const { t } = useTranslation(['topic', 'common']);
|
||||
const [topicCount] = useChatStore((s) => [topicSelectors.currentTopicCount(s)]);
|
||||
const includeCompleted = useUserStore(preferenceSelectors.topicIncludeCompleted);
|
||||
const dropdownMenu = useTopicActionsDropdownMenu();
|
||||
const { isRevalidating } = useFetchTopics();
|
||||
const { isRevalidating } = useFetchTopics({
|
||||
excludeStatuses: includeCompleted ? undefined : ['completed'],
|
||||
});
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
|
|
|
|||
|
|
@ -11,11 +11,14 @@ import type { TopicGroupMode, TopicSortBy } from '@/types/topic';
|
|||
export const useTopicFilterDropdownMenu = (): DropdownItem[] => {
|
||||
const { t } = useTranslation('topic');
|
||||
|
||||
const [topicGroupMode, topicSortBy, updatePreference] = useUserStore((s) => [
|
||||
preferenceSelectors.topicGroupMode(s),
|
||||
preferenceSelectors.topicSortBy(s),
|
||||
s.updatePreference,
|
||||
]);
|
||||
const [topicGroupMode, topicSortBy, topicIncludeCompleted, updatePreference] = useUserStore(
|
||||
(s) => [
|
||||
preferenceSelectors.topicGroupMode(s),
|
||||
preferenceSelectors.topicSortBy(s),
|
||||
preferenceSelectors.topicIncludeCompleted(s),
|
||||
s.updatePreference,
|
||||
],
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
const groupModes: TopicGroupMode[] = ['byTime', 'byProject', 'flat'];
|
||||
|
|
@ -49,6 +52,22 @@ export const useTopicFilterDropdownMenu = (): DropdownItem[] => {
|
|||
label: t('filter.sort'),
|
||||
type: 'group' as const,
|
||||
},
|
||||
{ type: 'divider' as const },
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: topicIncludeCompleted ? <Icon icon={LucideCheck} /> : <div />,
|
||||
key: 'showCompleted',
|
||||
label: t('filter.showCompleted'),
|
||||
onClick: () => {
|
||||
updatePreference({ topicIncludeCompleted: !topicIncludeCompleted });
|
||||
},
|
||||
},
|
||||
],
|
||||
key: 'filter',
|
||||
label: t('filter.filter'),
|
||||
type: 'group' as const,
|
||||
},
|
||||
];
|
||||
}, [topicGroupMode, topicSortBy, updatePreference, t]);
|
||||
}, [topicGroupMode, topicSortBy, topicIncludeCompleted, updatePreference, t]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -234,19 +234,28 @@ export const topicRouter = router({
|
|||
z.object({
|
||||
agentId: z.string().nullable().optional(),
|
||||
current: z.number().optional(),
|
||||
excludeStatuses: z.array(z.string()).optional(),
|
||||
excludeTriggers: z.array(z.string()).optional(),
|
||||
groupId: z.string().nullable().optional(),
|
||||
isInbox: z.boolean().optional(),
|
||||
pageSize: z.number().optional(),
|
||||
sessionId: z.string().nullable().optional(),
|
||||
triggers: z.array(z.string()).optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { sessionId, isInbox, groupId, excludeTriggers, ...rest } = input;
|
||||
const { sessionId, isInbox, groupId, excludeStatuses, excludeTriggers, triggers, ...rest } =
|
||||
input;
|
||||
|
||||
// If groupId is provided, query by groupId directly
|
||||
if (groupId) {
|
||||
const result = await ctx.topicModel.query({ excludeTriggers, groupId, ...rest });
|
||||
const result = await ctx.topicModel.query({
|
||||
excludeStatuses,
|
||||
excludeTriggers,
|
||||
groupId,
|
||||
triggers,
|
||||
...rest,
|
||||
});
|
||||
return { items: result.items, total: result.total };
|
||||
}
|
||||
|
||||
|
|
@ -259,8 +268,10 @@ export const topicRouter = router({
|
|||
const result = await ctx.topicModel.query({
|
||||
...rest,
|
||||
agentId: effectiveAgentId,
|
||||
excludeStatuses,
|
||||
excludeTriggers,
|
||||
isInbox,
|
||||
triggers,
|
||||
});
|
||||
|
||||
// Runtime migration: backfill agentId for ALL legacy topics and messages under this agent
|
||||
|
|
@ -524,6 +535,7 @@ export const topicRouter = router({
|
|||
id: z.string(),
|
||||
value: z.object({
|
||||
agentId: z.string().optional(),
|
||||
completedAt: z.date().nullable().optional(),
|
||||
favorite: z.boolean().optional(),
|
||||
historySummary: z.string().optional(),
|
||||
messages: z.array(z.string()).optional(),
|
||||
|
|
@ -534,6 +546,7 @@ export const topicRouter = router({
|
|||
})
|
||||
.optional(),
|
||||
sessionId: z.string().optional(),
|
||||
status: z.enum(['active', 'completed', 'archived']).nullable().optional(),
|
||||
title: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -37,10 +37,12 @@ export class TopicService {
|
|||
return lambdaClient.topic.getTopics.query({
|
||||
agentId: params.agentId,
|
||||
current: params.current,
|
||||
excludeStatuses: params.excludeStatuses,
|
||||
excludeTriggers: params.excludeTriggers,
|
||||
groupId: params.groupId,
|
||||
isInbox: params.isInbox,
|
||||
pageSize: params.pageSize,
|
||||
triggers: params.triggers,
|
||||
}) as any;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -234,6 +234,20 @@ export class ChatTopicActionImpl {
|
|||
});
|
||||
};
|
||||
|
||||
markTopicCompleted = async (id: string): Promise<void> => {
|
||||
await this.#get().internal_updateTopic(id, {
|
||||
completedAt: new Date(),
|
||||
status: 'completed',
|
||||
});
|
||||
};
|
||||
|
||||
unmarkTopicCompleted = async (id: string): Promise<void> => {
|
||||
await this.#get().internal_updateTopic(id, {
|
||||
completedAt: null,
|
||||
status: 'active',
|
||||
});
|
||||
};
|
||||
|
||||
favoriteTopic = async (id: string, favorite: boolean): Promise<void> => {
|
||||
const { activeAgentId } = this.#get();
|
||||
await this.#get().internal_updateTopic(id, { favorite });
|
||||
|
|
@ -303,12 +317,14 @@ export class ChatTopicActionImpl {
|
|||
enable: boolean,
|
||||
{
|
||||
agentId,
|
||||
excludeStatuses,
|
||||
excludeTriggers,
|
||||
groupId,
|
||||
pageSize: customPageSize,
|
||||
isInbox,
|
||||
}: {
|
||||
agentId?: string;
|
||||
excludeStatuses?: string[];
|
||||
excludeTriggers?: string[];
|
||||
groupId?: string;
|
||||
isInbox?: boolean;
|
||||
|
|
@ -318,6 +334,8 @@ export class ChatTopicActionImpl {
|
|||
const pageSize = customPageSize || 20;
|
||||
const effectiveExcludeTriggers =
|
||||
excludeTriggers && excludeTriggers.length > 0 ? excludeTriggers : undefined;
|
||||
const effectiveExcludeStatuses =
|
||||
excludeStatuses && excludeStatuses.length > 0 ? excludeStatuses : undefined;
|
||||
// Use topicMapKey to generate the container key for topic data map
|
||||
const containerKey = topicMapKey({ agentId, groupId });
|
||||
const hasValidContainer = !!(groupId || agentId);
|
||||
|
|
@ -331,6 +349,7 @@ export class ChatTopicActionImpl {
|
|||
isInbox,
|
||||
pageSize,
|
||||
...(effectiveExcludeTriggers ? { excludeTriggers: effectiveExcludeTriggers } : {}),
|
||||
...(effectiveExcludeStatuses ? { excludeStatuses: effectiveExcludeStatuses } : {}),
|
||||
},
|
||||
]
|
||||
: null,
|
||||
|
|
@ -353,6 +372,7 @@ export class ChatTopicActionImpl {
|
|||
const result = await topicService.getTopics({
|
||||
agentId,
|
||||
current: 0,
|
||||
excludeStatuses: effectiveExcludeStatuses,
|
||||
excludeTriggers: effectiveExcludeTriggers,
|
||||
groupId,
|
||||
isInbox,
|
||||
|
|
@ -385,6 +405,7 @@ export class ChatTopicActionImpl {
|
|||
...this.#get().topicDataMap,
|
||||
[containerKey]: {
|
||||
currentPage: 0,
|
||||
excludeStatuses: effectiveExcludeStatuses,
|
||||
excludeTriggers: effectiveExcludeTriggers,
|
||||
hasMore,
|
||||
isExpandingPageSize: false,
|
||||
|
|
@ -426,9 +447,11 @@ export class ChatTopicActionImpl {
|
|||
try {
|
||||
const pageSize = useGlobalStore.getState().status.topicPageSize || 20;
|
||||
const excludeTriggers = currentData?.excludeTriggers;
|
||||
const excludeStatuses = currentData?.excludeStatuses;
|
||||
const result = await topicService.getTopics({
|
||||
agentId: activeAgentId,
|
||||
current: nextPage,
|
||||
excludeStatuses,
|
||||
excludeTriggers,
|
||||
groupId: activeGroupId,
|
||||
pageSize,
|
||||
|
|
@ -443,6 +466,7 @@ export class ChatTopicActionImpl {
|
|||
...this.#get().topicDataMap,
|
||||
[key]: {
|
||||
currentPage: nextPage,
|
||||
excludeStatuses,
|
||||
excludeTriggers,
|
||||
hasMore,
|
||||
isLoadingMore: false,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { type ChatTopic } from '@/types/topic';
|
|||
*/
|
||||
export interface TopicData {
|
||||
currentPage: number;
|
||||
excludeStatuses?: string[];
|
||||
excludeTriggers?: string[];
|
||||
hasMore: boolean;
|
||||
isExpandingPageSize?: boolean;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ const useCmdEnterToSend = (s: UserStore): boolean => s.preference.useCmdEnterToS
|
|||
const topicGroupMode = (s: UserStore) =>
|
||||
s.preference.topicGroupMode || DEFAULT_PREFERENCE.topicGroupMode!;
|
||||
const topicSortBy = (s: UserStore) => s.preference.topicSortBy || DEFAULT_PREFERENCE.topicSortBy!;
|
||||
const topicIncludeCompleted = (s: UserStore): boolean =>
|
||||
s.preference.topicIncludeCompleted ?? false;
|
||||
|
||||
const hideSyncAlert = (s: UserStore) => s.preference.hideSyncAlert;
|
||||
|
||||
|
|
@ -26,6 +28,7 @@ export const preferenceSelectors = {
|
|||
shouldTriggerFileInKnowledgeBaseTip,
|
||||
showUploadFileInKnowledgeBaseTip,
|
||||
topicGroupMode,
|
||||
topicIncludeCompleted,
|
||||
topicSortBy,
|
||||
useCmdEnterToSend,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue