@nextclaw/ui 0.14.1-beta.0 → 0.14.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/assets/{api-C6EDHBDR.js → api-dQ6uQSZG.js} +3 -3
- package/dist/assets/{channels-list-page-DKj6m1Hc.js → channels-list-page-DfpjB8XB.js} +2 -2
- package/dist/assets/chat-page-D3QaEqjs.js +105 -0
- package/dist/assets/{config-split-page-CjlmJy8X.js → config-split-page-CeFfbaqc.js} +1 -1
- package/dist/assets/{confirm-dialog-B39X0pYp.js → confirm-dialog-C95Y4QPC.js} +1 -1
- package/dist/assets/{desktop-update-config-DfUQ8nly.js → desktop-update-config-CcK6jiEb.js} +1 -1
- package/dist/assets/{dist-CmMro3nV.js → dist-DTGzeil6.js} +1 -1
- package/dist/assets/{dist-BIYV2-Ia.js → dist-fGogBmPA.js} +1 -1
- package/dist/assets/doc-browser-CuBTcBEZ.js +1 -0
- package/dist/assets/{doc-browser-CDtUtcim.js → doc-browser-DVHhEb96.js} +1 -1
- package/dist/assets/{doc-browser-context-DZbC9zVB.js → doc-browser-context-BfMXwnrz.js} +1 -1
- package/dist/assets/{ellipsis-DDpgTzgx.js → ellipsis-D2F4KMiU.js} +1 -1
- package/dist/assets/{external-link-D2cBOCqn.js → external-link-CcgtBK26.js} +1 -1
- package/dist/assets/index-ByJCbQqr.css +1 -0
- package/dist/assets/index-DjXqiloY.js +103 -0
- package/dist/assets/mcp-marketplace-page-98-V__3u.js +1 -0
- package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +40 -0
- package/dist/assets/model-config-page-aAUkZkSX.js +1 -0
- package/dist/assets/plus-1Rxl5FlV.js +1 -0
- package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +1 -0
- package/dist/assets/providers-config-page-CtMkPSyu.js +1 -0
- package/dist/assets/{react-DYQXXGbH.js → react-CTqdhFMk.js} +1 -1
- package/dist/assets/remote-fQ7N_r8u.js +1 -0
- package/dist/assets/rotate-cw-CzfIeAJj.js +1 -0
- package/dist/assets/runtime-config-page-TD1UrTbG.js +1 -0
- package/dist/assets/{save-BNVTgBVn.js → save-C9fKWfVB.js} +1 -1
- package/dist/assets/{search-CKCzwLId.js → search-D5FQ5q0X.js} +1 -1
- package/dist/assets/{search-config-page-DG5Q2hF1.js → search-config-page-AMEJJNIS.js} +1 -1
- package/dist/assets/secrets-config-page-CcgJnvBO.js +3 -0
- package/dist/assets/{select-CIRwTNP4.js → select-Cu5n0E-3.js} +2 -2
- package/dist/assets/{tag-chip-DKhVFtTE.js → tag-chip-C5aOtjxu.js} +1 -1
- package/dist/assets/use-config-D8TESeo6.js +1 -0
- package/dist/assets/x-1qETdQ70.js +1 -0
- package/dist/index.html +14 -14
- package/package.json +9 -9
- package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
- package/src/features/chat/features/input/components/chat-input-bar.container.tsx +84 -6
- package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +2 -0
- package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +87 -0
- package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +64 -24
- package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +21 -12
- package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -25
- package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
- package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +10 -9
- package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
- package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
- package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
- package/src/features/chat/features/session-type/components/__tests__/chat-sidebar-create-menu.test.tsx +24 -0
- package/src/features/chat/features/session-type/components/chat-sidebar-create-menu.tsx +10 -1
- package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
- package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
- package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
- package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
- package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
- package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +65 -0
- package/src/features/chat/managers/chat-input.manager.ts +24 -2
- package/src/features/chat/stores/chat-input.store.ts +2 -0
- package/src/features/chat/types/chat-input-bar.types.ts +5 -0
- package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
- package/src/features/settings/components/config/provider-form.tsx +284 -387
- package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
- package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
- package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
- package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
- package/src/features/settings/pages/secrets-config-page.tsx +2 -1
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
- package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
- package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
- package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
- package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
- package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
- package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
- package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
- package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
- package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
- package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
- package/src/features/system-status/components/runtime-security-card.tsx +79 -25
- package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
- package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
- package/src/shared/components/common/searchable-model-input.tsx +102 -87
- package/src/shared/components/ui/README.md +7 -0
- package/src/shared/components/ui/popover.tsx +19 -3
- package/src/shared/components/ui/select.tsx +14 -4
- package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
- package/src/shared/lib/api/index.ts +2 -0
- package/src/shared/lib/api/preferences/preference.types.ts +22 -0
- package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +16 -1
- package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -1
- package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
- package/dist/assets/chat-page-BXngLJY6.js +0 -105
- package/dist/assets/doc-browser-B6Wc_TIY.js +0 -1
- package/dist/assets/index-DTb_XVIH.js +0 -103
- package/dist/assets/index-ayiPYhf1.css +0 -1
- package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +0 -1
- package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +0 -40
- package/dist/assets/model-config-page-Bcb35AVv.js +0 -1
- package/dist/assets/plus-CYE1onU2.js +0 -1
- package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +0 -1
- package/dist/assets/providers-config-page-CQSibSwO.js +0 -1
- package/dist/assets/remote-BlJDbXOI.js +0 -1
- package/dist/assets/rotate-cw-BWbvFDAX.js +0 -1
- package/dist/assets/runtime-config-page-Tn2yLQ0c.js +0 -1
- package/dist/assets/secrets-config-page-BXn5Uuwp.js +0 -3
- package/dist/assets/use-config-DgKM9VIU.js +0 -1
- package/dist/assets/x-Cm3JjYut.js +0 -1
- package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
- package/src/features/chat/components/chat-welcome.tsx +0 -102
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { ChatWelcome } from '@/features/chat/features/welcome/components/chat-welcome';
|
|
4
|
+
|
|
5
|
+
vi.mock(
|
|
6
|
+
'@/features/chat/features/session/components/session-header/chat-session-project-dialog',
|
|
7
|
+
() => ({
|
|
8
|
+
ChatSessionProjectDialog: () => null,
|
|
9
|
+
}),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const sessionTypeOptions = [
|
|
13
|
+
{ value: 'native', label: 'Native', ready: true },
|
|
14
|
+
{ value: 'codex', label: 'Codex', ready: true },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function renderWelcome(
|
|
18
|
+
overrides: Partial<Parameters<typeof ChatWelcome>[0]> = {},
|
|
19
|
+
) {
|
|
20
|
+
return render(
|
|
21
|
+
<ChatWelcome
|
|
22
|
+
agents={[
|
|
23
|
+
{ id: 'main', displayName: 'Main' },
|
|
24
|
+
{ id: 'engineer', displayName: 'Engineer' },
|
|
25
|
+
]}
|
|
26
|
+
projectOptions={[]}
|
|
27
|
+
selectedAgentId="main"
|
|
28
|
+
selectedSessionType="native"
|
|
29
|
+
sessionTypeOptions={sessionTypeOptions}
|
|
30
|
+
onSelectAgent={vi.fn()}
|
|
31
|
+
onSelectPrompt={vi.fn()}
|
|
32
|
+
onSelectSessionType={vi.fn()}
|
|
33
|
+
{...overrides}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('ChatWelcome', () => {
|
|
39
|
+
it('renders a lightweight draft agent select and allows switching', () => {
|
|
40
|
+
const onSelectAgent = vi.fn();
|
|
41
|
+
|
|
42
|
+
renderWelcome({ onSelectAgent });
|
|
43
|
+
|
|
44
|
+
const trigger = screen.getByRole('combobox', { name: 'Draft agent' });
|
|
45
|
+
expect(screen.getByText('Main')).toBeTruthy();
|
|
46
|
+
fireEvent.keyDown(trigger, { key: 'ArrowDown' });
|
|
47
|
+
fireEvent.click(screen.getByText('Engineer'));
|
|
48
|
+
|
|
49
|
+
expect(onSelectAgent).toHaveBeenCalledWith('engineer');
|
|
50
|
+
expect(screen.queryByText('Current Agent:')).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('uses an adaptive capability grid for narrow docked layouts', () => {
|
|
54
|
+
renderWelcome({ agents: [{ id: 'main', displayName: 'Main' }] });
|
|
55
|
+
|
|
56
|
+
expect(
|
|
57
|
+
screen.getByRole('button', { name: /Smart Conversations/ }).parentElement
|
|
58
|
+
?.className,
|
|
59
|
+
).toContain('auto-fit');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('fills the composer with a prompt suggestion when clicking a capability card', () => {
|
|
63
|
+
const onSelectPrompt = vi.fn();
|
|
64
|
+
|
|
65
|
+
renderWelcome({ onSelectPrompt });
|
|
66
|
+
|
|
67
|
+
fireEvent.click(screen.getByRole('button', { name: /Smart Conversations/ }));
|
|
68
|
+
|
|
69
|
+
expect(onSelectPrompt).toHaveBeenCalledWith(
|
|
70
|
+
expect.stringContaining('next three concrete things'),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('lets users switch the welcome session type', () => {
|
|
75
|
+
const onSelectSessionType = vi.fn();
|
|
76
|
+
|
|
77
|
+
renderWelcome({ onSelectSessionType });
|
|
78
|
+
|
|
79
|
+
fireEvent.click(screen.getByRole('button', { name: 'Choose session type' }));
|
|
80
|
+
fireEvent.click(screen.getByText('Codex'));
|
|
81
|
+
|
|
82
|
+
expect(onSelectSessionType).toHaveBeenCalledWith('codex');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('shows recent projects in a scrollable menu with a fixed folder action', async () => {
|
|
86
|
+
const onSelectProjectRoot = vi.fn();
|
|
87
|
+
|
|
88
|
+
renderWelcome({
|
|
89
|
+
defaultProjectRoot: '/Users/demo/.nextclaw/workspace',
|
|
90
|
+
onSelectProjectRoot,
|
|
91
|
+
projectOptions: [
|
|
92
|
+
{
|
|
93
|
+
projectRoot: '/Users/demo/.nextclaw/workspace',
|
|
94
|
+
projectName: 'workspace',
|
|
95
|
+
sessionCount: 0,
|
|
96
|
+
isDefault: true,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
projectRoot: '/tmp/project-alpha',
|
|
100
|
+
projectName: 'project-alpha',
|
|
101
|
+
sessionCount: 3,
|
|
102
|
+
isDefault: false,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
fireEvent.click(
|
|
108
|
+
screen.getByRole('button', { name: 'Choose working directory' }),
|
|
109
|
+
);
|
|
110
|
+
expect(screen.getByText('Recent projects')).toBeTruthy();
|
|
111
|
+
expect(screen.getByText('/tmp/project-alpha')).toBeTruthy();
|
|
112
|
+
expect(screen.getByText('Open folder')).toBeTruthy();
|
|
113
|
+
expect(screen.getByText('/tmp/project-alpha').closest('.overflow-y-auto'))
|
|
114
|
+
.toBeTruthy();
|
|
115
|
+
|
|
116
|
+
fireEvent.click(screen.getByText('project-alpha'));
|
|
117
|
+
|
|
118
|
+
expect(onSelectProjectRoot).toHaveBeenCalledWith('/tmp/project-alpha');
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(screen.queryByText('Recent projects')).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
|
|
3
|
+
import {
|
|
4
|
+
buildSessionTypeOptions,
|
|
5
|
+
DEFAULT_SESSION_TYPE,
|
|
6
|
+
normalizeSessionType,
|
|
7
|
+
} from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
8
|
+
import { ChatWelcome } from "@/features/chat/features/welcome/components/chat-welcome";
|
|
9
|
+
import {
|
|
10
|
+
resolveChatWelcomeAgents,
|
|
11
|
+
resolveChatWelcomeSelectedSessionType,
|
|
12
|
+
} from "@/features/chat/features/welcome/utils/chat-welcome-selection.utils";
|
|
13
|
+
import { buildChatWelcomeProjectOptions } from "@/features/chat/features/welcome/utils/chat-welcome-project-options.utils";
|
|
14
|
+
import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
|
|
15
|
+
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
16
|
+
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
17
|
+
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
18
|
+
import { useAgents } from "@/shared/hooks/use-agents";
|
|
19
|
+
import type { NcpSessionSummaryView } from "@/shared/lib/api";
|
|
20
|
+
import { normalizeSessionProjectRootValue } from "@/shared/lib/session-project";
|
|
21
|
+
|
|
22
|
+
const EMPTY_NCP_SESSION_SUMMARIES: NcpSessionSummaryView[] = [];
|
|
23
|
+
|
|
24
|
+
type ChatConversationWelcomeProps = {
|
|
25
|
+
inputSlot: ReactNode;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function ChatConversationWelcome({
|
|
29
|
+
inputSlot,
|
|
30
|
+
}: ChatConversationWelcomeProps) {
|
|
31
|
+
const presenter = usePresenter();
|
|
32
|
+
const inputSnapshot = useChatInputStore((state) => state.snapshot);
|
|
33
|
+
const selectedAgentId = useChatSessionListStore(
|
|
34
|
+
(state) => state.snapshot.selectedAgentId,
|
|
35
|
+
);
|
|
36
|
+
const threadSnapshot = useChatThreadStore((state) => state.snapshot);
|
|
37
|
+
const config = useChatQueryStore(
|
|
38
|
+
(state) => state.snapshot.configQuery?.data ?? null,
|
|
39
|
+
);
|
|
40
|
+
const sessionSummaries = useChatQueryStore(
|
|
41
|
+
(state) => state.snapshot.sessionsQuery?.data?.sessions ?? EMPTY_NCP_SESSION_SUMMARIES,
|
|
42
|
+
);
|
|
43
|
+
const sessionTypesData = useChatQueryStore(
|
|
44
|
+
(state) => state.snapshot.sessionTypesQuery?.data ?? null,
|
|
45
|
+
);
|
|
46
|
+
const agentsQuery = useAgents();
|
|
47
|
+
const draftAgentId = threadSnapshot.agentId ?? selectedAgentId;
|
|
48
|
+
const availableAgents = resolveChatWelcomeAgents({
|
|
49
|
+
agents: agentsQuery.data?.agents,
|
|
50
|
+
fallbackAgentId: draftAgentId,
|
|
51
|
+
});
|
|
52
|
+
const defaultProjectRoot = normalizeSessionProjectRootValue(
|
|
53
|
+
config?.agents.defaults.workspace,
|
|
54
|
+
);
|
|
55
|
+
const selectedProjectRoot = normalizeSessionProjectRootValue(
|
|
56
|
+
inputSnapshot.pendingProjectRoot,
|
|
57
|
+
);
|
|
58
|
+
const sessionTypeOptions = buildSessionTypeOptions(
|
|
59
|
+
sessionTypesData?.options ?? [],
|
|
60
|
+
);
|
|
61
|
+
const defaultSessionType = normalizeSessionType(
|
|
62
|
+
sessionTypesData?.defaultType ?? inputSnapshot.defaultSessionType ?? DEFAULT_SESSION_TYPE,
|
|
63
|
+
);
|
|
64
|
+
const selectedSessionType = resolveChatWelcomeSelectedSessionType({
|
|
65
|
+
defaultSessionType,
|
|
66
|
+
pendingSessionType: inputSnapshot.pendingSessionType,
|
|
67
|
+
selectedSessionType: inputSnapshot.selectedSessionType,
|
|
68
|
+
sessionTypeOptions,
|
|
69
|
+
});
|
|
70
|
+
const projectOptions = buildChatWelcomeProjectOptions({
|
|
71
|
+
defaultProjectRoot,
|
|
72
|
+
sessionSummaries,
|
|
73
|
+
});
|
|
74
|
+
const selectDraftAgent = (agentId: string) => {
|
|
75
|
+
presenter.chatSessionListManager.setSelectedAgentId(agentId);
|
|
76
|
+
presenter.chatInputManager.setPendingSessionType(
|
|
77
|
+
resolveChatWelcomeSelectedSessionType({
|
|
78
|
+
agents: availableAgents,
|
|
79
|
+
agentId,
|
|
80
|
+
defaultSessionType,
|
|
81
|
+
pendingSessionType: null,
|
|
82
|
+
selectedSessionType: null,
|
|
83
|
+
sessionTypeOptions,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<ChatWelcome
|
|
90
|
+
agents={availableAgents}
|
|
91
|
+
defaultProjectRoot={defaultProjectRoot}
|
|
92
|
+
inputSlot={inputSlot}
|
|
93
|
+
projectOptions={projectOptions}
|
|
94
|
+
selectedAgentId={draftAgentId}
|
|
95
|
+
selectedProjectRoot={selectedProjectRoot}
|
|
96
|
+
selectedSessionType={selectedSessionType}
|
|
97
|
+
sessionTypeOptions={sessionTypeOptions}
|
|
98
|
+
onSelectAgent={selectDraftAgent}
|
|
99
|
+
onSelectPrompt={presenter.chatInputManager.applyPromptSuggestion}
|
|
100
|
+
onSelectProjectRoot={presenter.chatInputManager.setPendingProjectRoot}
|
|
101
|
+
onSelectSessionType={presenter.chatInputManager.setPendingSessionType}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { AgentAvatar } from '@/shared/components/common/agent-avatar';
|
|
2
|
+
import {
|
|
3
|
+
Select,
|
|
4
|
+
SelectContent,
|
|
5
|
+
SelectItem,
|
|
6
|
+
SelectTrigger,
|
|
7
|
+
createSelectAvailableHeightLimit,
|
|
8
|
+
} from '@/shared/components/ui/select';
|
|
9
|
+
import type { AgentProfileView } from '@/shared/lib/api';
|
|
10
|
+
import { t } from '@/shared/lib/i18n';
|
|
11
|
+
|
|
12
|
+
type ChatWelcomeAgentPickerProps = {
|
|
13
|
+
agents: AgentProfileView[];
|
|
14
|
+
selectedAgent: AgentProfileView | null;
|
|
15
|
+
selectedAgentId: string;
|
|
16
|
+
onSelectAgent: (agentId: string) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const AGENT_PICKER_MAX_HEIGHT = createSelectAvailableHeightLimit('18rem');
|
|
20
|
+
|
|
21
|
+
export function ChatWelcomeAgentPicker({
|
|
22
|
+
agents,
|
|
23
|
+
selectedAgent,
|
|
24
|
+
selectedAgentId,
|
|
25
|
+
onSelectAgent,
|
|
26
|
+
}: ChatWelcomeAgentPickerProps) {
|
|
27
|
+
return (
|
|
28
|
+
<Select value={selectedAgentId} onValueChange={onSelectAgent}>
|
|
29
|
+
<SelectTrigger
|
|
30
|
+
aria-label={t('chatDraftAgentTitle')}
|
|
31
|
+
className="h-auto w-auto min-w-0 gap-1 rounded-lg border-0 bg-transparent px-2 py-1.5 text-gray-500 shadow-none hover:bg-gray-100 hover:text-gray-800 focus:ring-0"
|
|
32
|
+
>
|
|
33
|
+
<span className="sr-only">{t('chatDraftAgentTitle')}</span>
|
|
34
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
35
|
+
{selectedAgent ? (
|
|
36
|
+
<>
|
|
37
|
+
<AgentAvatar
|
|
38
|
+
agentId={selectedAgent.id}
|
|
39
|
+
displayName={selectedAgent.displayName}
|
|
40
|
+
avatarUrl={selectedAgent.avatarUrl}
|
|
41
|
+
className="h-5 w-5 shrink-0"
|
|
42
|
+
/>
|
|
43
|
+
<span className="max-w-28 truncate text-sm font-medium">
|
|
44
|
+
{selectedAgent.displayName?.trim() || selectedAgent.id}
|
|
45
|
+
</span>
|
|
46
|
+
</>
|
|
47
|
+
) : null}
|
|
48
|
+
</div>
|
|
49
|
+
</SelectTrigger>
|
|
50
|
+
<SelectContent
|
|
51
|
+
className="rounded-xl border-gray-200/80 shadow-lg"
|
|
52
|
+
style={{ maxHeight: AGENT_PICKER_MAX_HEIGHT }}
|
|
53
|
+
>
|
|
54
|
+
{agents.map((agent) => (
|
|
55
|
+
<SelectItem key={agent.id} value={agent.id} className="rounded-lg pr-10">
|
|
56
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
57
|
+
<AgentAvatar
|
|
58
|
+
agentId={agent.id}
|
|
59
|
+
displayName={agent.displayName}
|
|
60
|
+
avatarUrl={agent.avatarUrl}
|
|
61
|
+
className="h-5 w-5 shrink-0"
|
|
62
|
+
/>
|
|
63
|
+
<span className="truncate text-sm font-medium text-gray-700">
|
|
64
|
+
{agent.displayName?.trim() || agent.id}
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
</SelectItem>
|
|
68
|
+
))}
|
|
69
|
+
</SelectContent>
|
|
70
|
+
</Select>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AlarmClock, BrainCircuit, MessageCircle } from 'lucide-react';
|
|
2
|
+
import { t } from '@/shared/lib/i18n';
|
|
3
|
+
|
|
4
|
+
const WELCOME_CAPABILITIES = [
|
|
5
|
+
{
|
|
6
|
+
icon: MessageCircle,
|
|
7
|
+
titleKey: 'chatWelcomeCapability1Title' as const,
|
|
8
|
+
descKey: 'chatWelcomeCapability1Desc' as const,
|
|
9
|
+
promptKey: 'chatWelcomeCapability1Prompt' as const,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
icon: BrainCircuit,
|
|
13
|
+
titleKey: 'chatWelcomeCapability2Title' as const,
|
|
14
|
+
descKey: 'chatWelcomeCapability2Desc' as const,
|
|
15
|
+
promptKey: 'chatWelcomeCapability2Prompt' as const,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
icon: AlarmClock,
|
|
19
|
+
titleKey: 'chatWelcomeCapability3Title' as const,
|
|
20
|
+
descKey: 'chatWelcomeCapability3Desc' as const,
|
|
21
|
+
promptKey: 'chatWelcomeCapability3Prompt' as const,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export function ChatWelcomeCapabilityGrid({
|
|
26
|
+
onSelectPrompt,
|
|
27
|
+
}: {
|
|
28
|
+
onSelectPrompt: (prompt: string) => void;
|
|
29
|
+
}) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="mt-8 grid grid-cols-[repeat(auto-fit,minmax(min(100%,9.5rem),1fr))] gap-3">
|
|
32
|
+
{WELCOME_CAPABILITIES.map((capability) => {
|
|
33
|
+
const Icon = capability.icon;
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
key={capability.titleKey}
|
|
37
|
+
onClick={() => onSelectPrompt(t(capability.promptKey))}
|
|
38
|
+
className="min-w-0 rounded-2xl border border-gray-200 bg-white p-3 text-left shadow-card transition-shadow hover:shadow-card-hover sm:p-4"
|
|
39
|
+
>
|
|
40
|
+
<div className="mb-3 flex h-9 w-9 items-center justify-center rounded-xl bg-primary/8">
|
|
41
|
+
<Icon className="h-4.5 w-4.5 text-primary" />
|
|
42
|
+
</div>
|
|
43
|
+
<div className="mb-1 text-sm font-semibold text-gray-900">
|
|
44
|
+
{t(capability.titleKey)}
|
|
45
|
+
</div>
|
|
46
|
+
<div className="text-[11px] leading-relaxed text-gray-500">
|
|
47
|
+
{t(capability.descKey)}
|
|
48
|
+
</div>
|
|
49
|
+
</button>
|
|
50
|
+
);
|
|
51
|
+
})}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { ChevronDown, FolderOpen } from 'lucide-react';
|
|
3
|
+
import type { ChatWelcomeProjectOption } from '@/features/chat/features/welcome/utils/chat-welcome-project-options.utils';
|
|
4
|
+
import {
|
|
5
|
+
Popover,
|
|
6
|
+
PopoverContent,
|
|
7
|
+
PopoverTrigger,
|
|
8
|
+
createPopoverAvailableHeightLimit,
|
|
9
|
+
} from '@/shared/components/ui/popover';
|
|
10
|
+
import { t } from '@/shared/lib/i18n';
|
|
11
|
+
import { getSessionProjectName } from '@/shared/lib/session-project';
|
|
12
|
+
|
|
13
|
+
type ChatWelcomeProjectPickerProps = {
|
|
14
|
+
defaultProjectRoot?: string | null;
|
|
15
|
+
isSaving: boolean;
|
|
16
|
+
projectOptions: readonly ChatWelcomeProjectOption[];
|
|
17
|
+
projectRoot: string | null;
|
|
18
|
+
selectable: boolean;
|
|
19
|
+
onOpenProjectDialog: () => void;
|
|
20
|
+
onSelectProjectRoot: (projectRoot: string) => Promise<void> | void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const PROJECT_PICKER_MAX_HEIGHT = createPopoverAvailableHeightLimit('20rem');
|
|
24
|
+
|
|
25
|
+
export function ChatWelcomeProjectPicker({
|
|
26
|
+
defaultProjectRoot,
|
|
27
|
+
isSaving,
|
|
28
|
+
projectOptions,
|
|
29
|
+
projectRoot,
|
|
30
|
+
selectable,
|
|
31
|
+
onOpenProjectDialog,
|
|
32
|
+
onSelectProjectRoot,
|
|
33
|
+
}: ChatWelcomeProjectPickerProps) {
|
|
34
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
35
|
+
const projectLabel =
|
|
36
|
+
getSessionProjectName(projectRoot) ?? t('chatWelcomeProjectPickerPlaceholder');
|
|
37
|
+
const isDefaultProject =
|
|
38
|
+
defaultProjectRoot !== null &&
|
|
39
|
+
defaultProjectRoot !== undefined &&
|
|
40
|
+
projectRoot === defaultProjectRoot;
|
|
41
|
+
|
|
42
|
+
const selectProjectRoot = async (nextProjectRoot: string) => {
|
|
43
|
+
setIsOpen(false);
|
|
44
|
+
await onSelectProjectRoot(nextProjectRoot);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
49
|
+
<PopoverTrigger asChild>
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
|
|
53
|
+
title={projectRoot ?? undefined}
|
|
54
|
+
aria-label={t('chatWelcomeProjectPickerLabel')}
|
|
55
|
+
disabled={!selectable || isSaving}
|
|
56
|
+
>
|
|
57
|
+
<FolderOpen className="h-4 w-4 shrink-0" />
|
|
58
|
+
<span className="truncate">{projectLabel}</span>
|
|
59
|
+
{isDefaultProject ? (
|
|
60
|
+
<span className="shrink-0 text-xs font-normal text-gray-400">
|
|
61
|
+
{t('chatWelcomeProjectDefaultBadge')}
|
|
62
|
+
</span>
|
|
63
|
+
) : null}
|
|
64
|
+
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400" />
|
|
65
|
+
</button>
|
|
66
|
+
</PopoverTrigger>
|
|
67
|
+
<PopoverContent
|
|
68
|
+
align="start"
|
|
69
|
+
className="flex w-[min(20rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-gray-200/80 bg-white p-0 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
|
|
70
|
+
style={{ maxHeight: PROJECT_PICKER_MAX_HEIGHT }}
|
|
71
|
+
>
|
|
72
|
+
<div className="shrink-0 px-3 pb-2 pt-3 text-[10px] font-semibold uppercase tracking-[0.18em] text-gray-400">
|
|
73
|
+
{t('chatWelcomeProjectRecentTitle')}
|
|
74
|
+
</div>
|
|
75
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-1.5 pb-1.5">
|
|
76
|
+
{projectOptions.length > 0 ? (
|
|
77
|
+
projectOptions.map((option) => (
|
|
78
|
+
<button
|
|
79
|
+
key={option.projectRoot}
|
|
80
|
+
type="button"
|
|
81
|
+
className="flex w-full min-w-0 items-start gap-2 rounded-xl px-2 py-2 text-left transition-colors hover:bg-gray-50"
|
|
82
|
+
title={option.projectRoot}
|
|
83
|
+
onClick={() => {
|
|
84
|
+
void selectProjectRoot(option.projectRoot);
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<FolderOpen className="mt-0.5 h-4 w-4 shrink-0 text-gray-400" />
|
|
88
|
+
<span className="min-w-0 flex-1">
|
|
89
|
+
<span className="flex min-w-0 items-center gap-1.5">
|
|
90
|
+
<span className="truncate text-[13px] font-semibold text-gray-800">
|
|
91
|
+
{option.projectName}
|
|
92
|
+
</span>
|
|
93
|
+
{option.isDefault ? (
|
|
94
|
+
<span className="shrink-0 text-[10px] font-medium text-gray-400">
|
|
95
|
+
{t('chatWelcomeProjectDefaultBadge')}
|
|
96
|
+
</span>
|
|
97
|
+
) : null}
|
|
98
|
+
</span>
|
|
99
|
+
<span className="block truncate text-[11px] leading-4 text-gray-500">
|
|
100
|
+
{option.projectRoot}
|
|
101
|
+
</span>
|
|
102
|
+
</span>
|
|
103
|
+
{option.sessionCount > 0 ? (
|
|
104
|
+
<span className="mt-0.5 shrink-0 rounded-full bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-500">
|
|
105
|
+
{option.sessionCount}
|
|
106
|
+
</span>
|
|
107
|
+
) : null}
|
|
108
|
+
</button>
|
|
109
|
+
))
|
|
110
|
+
) : (
|
|
111
|
+
<div className="px-2 py-5 text-center text-xs text-gray-400">
|
|
112
|
+
{t('chatWelcomeProjectNoRecent')}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
<div className="shrink-0 border-t border-gray-100 p-1.5">
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
className="flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-left text-[13px] font-semibold text-gray-700 transition-colors hover:bg-gray-50"
|
|
120
|
+
onClick={() => {
|
|
121
|
+
setIsOpen(false);
|
|
122
|
+
onOpenProjectDialog();
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
<FolderOpen className="h-4 w-4 shrink-0 text-gray-400" />
|
|
126
|
+
<span>{t('chatWelcomeProjectOpenFolder')}</span>
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
</PopoverContent>
|
|
130
|
+
</Popover>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Bot, ChevronDown } from 'lucide-react';
|
|
3
|
+
import { SessionContextIconNode } from '@/features/chat/features/session/components/session-context-icon';
|
|
4
|
+
import { ChatSessionTypeOptionItem } from '@/features/chat/features/session-type/components/chat-session-type-option-item';
|
|
5
|
+
import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
|
|
6
|
+
import {
|
|
7
|
+
Popover,
|
|
8
|
+
PopoverContent,
|
|
9
|
+
PopoverTrigger,
|
|
10
|
+
createPopoverAvailableHeightLimit,
|
|
11
|
+
} from '@/shared/components/ui/popover';
|
|
12
|
+
import { t } from '@/shared/lib/i18n';
|
|
13
|
+
|
|
14
|
+
type SessionTypeOption = ChatInputSnapshot['sessionTypeOptions'][number];
|
|
15
|
+
|
|
16
|
+
type ChatWelcomeSessionTypePickerProps = {
|
|
17
|
+
options: readonly SessionTypeOption[];
|
|
18
|
+
selectedSessionType: string;
|
|
19
|
+
onSelectSessionType: (sessionType: string) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SESSION_TYPE_PICKER_MAX_HEIGHT = createPopoverAvailableHeightLimit('18rem');
|
|
23
|
+
|
|
24
|
+
export function ChatWelcomeSessionTypePicker({
|
|
25
|
+
options,
|
|
26
|
+
selectedSessionType,
|
|
27
|
+
onSelectSessionType,
|
|
28
|
+
}: ChatWelcomeSessionTypePickerProps) {
|
|
29
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
30
|
+
const selectedOption =
|
|
31
|
+
options.find((option) => option.value === selectedSessionType) ?? options[0] ?? null;
|
|
32
|
+
|
|
33
|
+
if (!selectedOption) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
39
|
+
<PopoverTrigger asChild>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
|
|
43
|
+
aria-label={t('chatWelcomeSessionTypePickerLabel')}
|
|
44
|
+
>
|
|
45
|
+
{selectedOption.icon?.src ? (
|
|
46
|
+
<SessionContextIconNode
|
|
47
|
+
icon={{
|
|
48
|
+
kind: 'runtime-image',
|
|
49
|
+
src: selectedOption.icon.src,
|
|
50
|
+
alt: selectedOption.icon.alt ?? null,
|
|
51
|
+
name: selectedOption.label,
|
|
52
|
+
}}
|
|
53
|
+
className="h-4 w-4"
|
|
54
|
+
/>
|
|
55
|
+
) : (
|
|
56
|
+
<Bot className="h-4 w-4 shrink-0" />
|
|
57
|
+
)}
|
|
58
|
+
<span className="truncate">{selectedOption.label}</span>
|
|
59
|
+
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400" />
|
|
60
|
+
</button>
|
|
61
|
+
</PopoverTrigger>
|
|
62
|
+
<PopoverContent
|
|
63
|
+
align="start"
|
|
64
|
+
className="flex w-[min(16rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-gray-200/80 bg-white p-1.5 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
|
|
65
|
+
style={{ maxHeight: SESSION_TYPE_PICKER_MAX_HEIGHT }}
|
|
66
|
+
>
|
|
67
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
68
|
+
{options.map((option) => (
|
|
69
|
+
<ChatSessionTypeOptionItem
|
|
70
|
+
key={option.value}
|
|
71
|
+
option={option}
|
|
72
|
+
onSelect={() => {
|
|
73
|
+
onSelectSessionType(option.value);
|
|
74
|
+
setIsOpen(false);
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
</PopoverContent>
|
|
80
|
+
</Popover>
|
|
81
|
+
);
|
|
82
|
+
}
|