@nextclaw/ui 0.6.10 → 0.6.11
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/.eslintrc.cjs +10 -0
- package/CHANGELOG.md +9 -0
- package/dist/assets/{ChannelsList-TyMb5Mgz.js → ChannelsList-C49JQ-Zt.js} +1 -1
- package/dist/assets/ChatPage-DIx05c6s.js +36 -0
- package/dist/assets/{DocBrowser-CNtrA0ps.js → DocBrowser-CpOosDEI.js} +1 -1
- package/dist/assets/{LogoBadge-BLqiOM5D.js → LogoBadge-CL_8ZPXU.js} +1 -1
- package/dist/assets/MarketplacePage-BOzko5s9.js +49 -0
- package/dist/assets/{ModelConfig-CCsQ8KFq.js → ModelConfig-BZ4ZfaQB.js} +1 -1
- package/dist/assets/ProvidersList-fPpJ5gl6.js +1 -0
- package/dist/assets/{RuntimeConfig-BO6s-ls-.js → RuntimeConfig-Dt9pLB9P.js} +1 -1
- package/dist/assets/{SecretsConfig-mayFdxpM.js → SecretsConfig-C1PU0Yy8.js} +2 -2
- package/dist/assets/{SessionsConfig-DAIczdBj.js → SessionsConfig-EskBOofQ.js} +2 -2
- package/dist/assets/{card-BP5YnL-G.js → card-C7Gtw2Vs.js} +1 -1
- package/dist/assets/index-Cn6_2To7.js +8 -0
- package/dist/assets/{index-BUiahmWm.css → index-nEYGCJTC.css} +1 -1
- package/dist/assets/{input-B1D2QX0O.js → input-oBvxsnV9.js} +1 -1
- package/dist/assets/{label-DW0j-fXA.js → label-C7F8lMpQ.js} +1 -1
- package/dist/assets/{page-layout-Ch-H9gD-.js → page-layout-DO8BlScF.js} +1 -1
- package/dist/assets/session-run-status-Kg0FwAPn.js +3 -0
- package/dist/assets/{switch-_cZHlGKB.js → switch-C6a5GyZB.js} +1 -1
- package/dist/assets/{tabs-custom-ARxqYYjG.js → tabs-custom-BatFap5k.js} +1 -1
- package/dist/assets/{useConfirmDialog-BaU7nIat.js → useConfirmDialog-zJzVKMdu.js} +2 -2
- package/dist/assets/{vendor-C--HHaLf.js → vendor-TlME1INH.js} +84 -84
- package/dist/index.html +3 -3
- package/package.json +4 -2
- package/src/App.tsx +1 -2
- package/src/api/config.ts +199 -200
- package/src/api/types.ts +36 -24
- package/src/components/chat/ChatConversationPanel.tsx +102 -121
- package/src/components/chat/ChatPage.tsx +165 -437
- package/src/components/chat/ChatSidebar.tsx +30 -36
- package/src/components/chat/ChatThread.tsx +73 -131
- package/src/components/chat/chat-input/ChatInputBarView.tsx +82 -0
- package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +71 -0
- package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +39 -0
- package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +31 -0
- package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +112 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +24 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +58 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +56 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +40 -0
- package/src/components/chat/chat-input/useChatInputBarController.ts +313 -0
- package/src/components/chat/chat-input.types.ts +15 -0
- package/src/components/chat/chat-page-data.ts +121 -0
- package/src/components/chat/chat-page-runtime.ts +221 -0
- package/src/components/chat/chat-session-route.ts +59 -0
- package/src/components/chat/chat-stream/nextbot-parsers.ts +52 -0
- package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +413 -0
- package/src/components/chat/chat-stream/stream-event-adapter.ts +98 -0
- package/src/components/chat/chat-stream/transport.ts +159 -0
- package/src/components/chat/chat-stream/types.ts +76 -0
- package/src/components/chat/managers/chat-input.manager.ts +142 -0
- package/src/components/chat/managers/chat-run-status.manager.ts +32 -0
- package/src/components/chat/managers/chat-session-list.manager.ts +77 -0
- package/src/components/chat/managers/chat-stream-actions.manager.ts +34 -0
- package/src/components/chat/managers/chat-thread.manager.ts +86 -0
- package/src/components/chat/managers/chat-ui.manager.ts +65 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +25 -0
- package/src/components/chat/presenter/chat.presenter.ts +32 -0
- package/src/components/chat/stores/chat-input.store.ts +62 -0
- package/src/components/chat/stores/chat-run-status.store.ts +30 -0
- package/src/components/chat/stores/chat-session-list.store.ts +34 -0
- package/src/components/chat/stores/chat-thread.store.ts +52 -0
- package/src/components/chat/useChatRuntimeController.ts +134 -0
- package/src/components/chat/useChatSessionTypeState.ts +148 -0
- package/src/components/common/MaskedInput.tsx +1 -1
- package/src/hooks/useConfig.ts +31 -1
- package/src/hooks/useObservable.ts +20 -0
- package/src/lib/chat-message.ts +2 -202
- package/src/lib/chat-runtime-utils.ts +250 -0
- package/src/lib/i18n.ts +9 -0
- package/tsconfig.json +2 -1
- package/vite.config.ts +2 -1
- package/dist/assets/ChatPage-CQerYqvy.js +0 -34
- package/dist/assets/MarketplacePage-CotZxxNe.js +0 -49
- package/dist/assets/ProvidersList-BYYX5K_g.js +0 -1
- package/dist/assets/index-D6_5HaDl.js +0 -7
- package/dist/assets/session-run-status-BUYsQeWs.js +0 -5
- package/src/components/chat/ChatInputBar.tsx +0 -590
- package/src/components/chat/useChatStreamController.ts +0 -591
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
ChatRunView,
|
|
4
|
+
ChatTurnStreamDeltaEvent,
|
|
5
|
+
ChatTurnStreamReadyEvent,
|
|
6
|
+
ChatTurnStreamSessionEvent,
|
|
7
|
+
SessionMessageView
|
|
8
|
+
} from '@/api/types';
|
|
9
|
+
|
|
10
|
+
export type SendMessageParams = {
|
|
11
|
+
runId?: string;
|
|
12
|
+
message: string;
|
|
13
|
+
sessionKey: string;
|
|
14
|
+
agentId: string;
|
|
15
|
+
sessionType?: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
requestedSkills?: string[];
|
|
18
|
+
stopSupported?: boolean;
|
|
19
|
+
stopReason?: string;
|
|
20
|
+
restoreDraftOnError?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ActiveRunState = {
|
|
24
|
+
localRunId: number;
|
|
25
|
+
sessionKey: string;
|
|
26
|
+
agentId?: string;
|
|
27
|
+
backendRunId?: string;
|
|
28
|
+
backendStopSupported: boolean;
|
|
29
|
+
backendStopReason?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type StreamReadyPayload = {
|
|
33
|
+
sessionKey: string;
|
|
34
|
+
runId?: string;
|
|
35
|
+
stopSupported?: boolean;
|
|
36
|
+
stopReason?: string;
|
|
37
|
+
requestedAt?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type StreamReadyEvent = ChatTurnStreamReadyEvent;
|
|
41
|
+
export type StreamDeltaEvent = ChatTurnStreamDeltaEvent;
|
|
42
|
+
export type StreamSessionEvent = ChatTurnStreamSessionEvent;
|
|
43
|
+
|
|
44
|
+
export type NextbotAgentRunMetadata =
|
|
45
|
+
| {
|
|
46
|
+
driver: 'nextbot-stream';
|
|
47
|
+
mode: 'send';
|
|
48
|
+
payload: SendMessageParams;
|
|
49
|
+
requestedSkills: string[];
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
driver: 'nextbot-stream';
|
|
53
|
+
mode: 'resume';
|
|
54
|
+
runId: string;
|
|
55
|
+
fromEventIndex?: number;
|
|
56
|
+
sessionKey?: string;
|
|
57
|
+
agentId?: string;
|
|
58
|
+
stopSupported?: boolean;
|
|
59
|
+
stopReason?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type UseChatStreamControllerParams = {
|
|
63
|
+
selectedSessionKeyRef: MutableRefObject<string | null>;
|
|
64
|
+
setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
|
|
65
|
+
setDraft: Dispatch<SetStateAction<string>>;
|
|
66
|
+
refetchSessions: () => Promise<unknown>;
|
|
67
|
+
refetchHistory: () => Promise<unknown>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type ChatStreamActions = {
|
|
71
|
+
sendMessage: (payload: SendMessageParams) => Promise<void>;
|
|
72
|
+
stopCurrentRun: () => Promise<void>;
|
|
73
|
+
resumeRun: (run: ChatRunView) => Promise<void>;
|
|
74
|
+
resetStreamState: () => void;
|
|
75
|
+
applyHistoryMessages: (messages: SessionMessageView[], options?: { isLoading?: boolean }) => void;
|
|
76
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { updateSession } from '@/api/config';
|
|
2
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
3
|
+
import { buildNewSessionKey } from '@/components/chat/chat-session-route';
|
|
4
|
+
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
5
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
6
|
+
import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
|
|
7
|
+
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
8
|
+
import type { SetStateAction } from 'react';
|
|
9
|
+
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
10
|
+
|
|
11
|
+
export class ChatInputManager {
|
|
12
|
+
constructor(
|
|
13
|
+
private uiManager: ChatUiManager,
|
|
14
|
+
private streamActionsManager: ChatStreamActionsManager
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
private hasSnapshotChanges = (patch: Partial<ChatInputSnapshot>): boolean => {
|
|
18
|
+
const current = useChatInputStore.getState().snapshot;
|
|
19
|
+
for (const [key, value] of Object.entries(patch) as Array<[keyof ChatInputSnapshot, ChatInputSnapshot[keyof ChatInputSnapshot]]>) {
|
|
20
|
+
if (!Object.is(current[key], value)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
|
|
28
|
+
if (typeof next === 'function') {
|
|
29
|
+
return (next as (value: T) => T)(prev);
|
|
30
|
+
}
|
|
31
|
+
return next;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
syncSnapshot = (patch: Partial<ChatInputSnapshot>) => {
|
|
35
|
+
if (!this.hasSnapshotChanges(patch)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
useChatInputStore.getState().setSnapshot(patch);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
setDraft = (next: SetStateAction<string>) => {
|
|
42
|
+
const prev = useChatInputStore.getState().snapshot.draft;
|
|
43
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
44
|
+
if (value === prev) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
useChatInputStore.getState().setSnapshot({ draft: value });
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
setPendingSessionType = (next: SetStateAction<string>) => {
|
|
51
|
+
const prev = useChatInputStore.getState().snapshot.pendingSessionType;
|
|
52
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
53
|
+
if (value === prev) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
useChatInputStore.getState().setSnapshot({ pendingSessionType: value });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
send = async () => {
|
|
60
|
+
const inputSnapshot = useChatInputStore.getState().snapshot;
|
|
61
|
+
const sessionSnapshot = useChatSessionListStore.getState().snapshot;
|
|
62
|
+
const message = inputSnapshot.draft.trim();
|
|
63
|
+
if (!message) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const requestedSkills = inputSnapshot.selectedSkills;
|
|
67
|
+
const hasSelectedSession = Boolean(sessionSnapshot.selectedSessionKey);
|
|
68
|
+
const sessionKey = sessionSnapshot.selectedSessionKey ?? buildNewSessionKey(sessionSnapshot.selectedAgentId);
|
|
69
|
+
if (!hasSelectedSession) {
|
|
70
|
+
this.uiManager.goToSession(sessionKey, { replace: true });
|
|
71
|
+
}
|
|
72
|
+
this.setDraft('');
|
|
73
|
+
this.setSelectedSkills([]);
|
|
74
|
+
await this.streamActionsManager.sendMessage({
|
|
75
|
+
message,
|
|
76
|
+
sessionKey,
|
|
77
|
+
agentId: sessionSnapshot.selectedAgentId,
|
|
78
|
+
sessionType: inputSnapshot.selectedSessionType,
|
|
79
|
+
model: inputSnapshot.selectedModel || undefined,
|
|
80
|
+
stopSupported: inputSnapshot.stopSupported,
|
|
81
|
+
stopReason: inputSnapshot.stopReason,
|
|
82
|
+
requestedSkills,
|
|
83
|
+
restoreDraftOnError: true
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
stop = async () => {
|
|
88
|
+
await this.streamActionsManager.stopCurrentRun();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
goToProviders = () => {
|
|
92
|
+
this.uiManager.goToProviders();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
setSelectedModel = (next: SetStateAction<string>) => {
|
|
96
|
+
const prev = useChatInputStore.getState().snapshot.selectedModel;
|
|
97
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
98
|
+
if (value === prev) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
useChatInputStore.getState().setSnapshot({ selectedModel: value });
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
selectSessionType = (value: string) => {
|
|
105
|
+
const normalized = normalizeSessionType(value);
|
|
106
|
+
useChatInputStore.getState().setSnapshot({ selectedSessionType: normalized, pendingSessionType: normalized });
|
|
107
|
+
void this.syncRemoteSessionType(normalized);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
setSelectedSkills = (next: SetStateAction<string[]>) => {
|
|
111
|
+
const prev = useChatInputStore.getState().snapshot.selectedSkills;
|
|
112
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
113
|
+
if (Object.is(value, prev)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
useChatInputStore.getState().setSnapshot({ selectedSkills: value });
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
selectModel = (value: string) => {
|
|
120
|
+
this.setSelectedModel(value);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
selectSkills = (next: string[]) => {
|
|
124
|
+
this.setSelectedSkills(next);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
private syncRemoteSessionType = async (normalizedType: string) => {
|
|
128
|
+
const sessionSnapshot = useChatSessionListStore.getState().snapshot;
|
|
129
|
+
const selectedSessionKey = sessionSnapshot.selectedSessionKey;
|
|
130
|
+
if (!selectedSessionKey) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const selectedSession = sessionSnapshot.sessions.find((session) => session.key === selectedSessionKey);
|
|
134
|
+
if (!selectedSession?.sessionTypeMutable) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (normalizeSessionType(selectedSession.sessionType) === normalizedType) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
await updateSession(selectedSessionKey, { sessionType: normalizedType });
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useChatRunStatusStore } from '@/components/chat/stores/chat-run-status.store';
|
|
2
|
+
import type { ChatRunStatusSnapshot } from '@/components/chat/stores/chat-run-status.store';
|
|
3
|
+
|
|
4
|
+
function isMapEqual<T>(left: ReadonlyMap<string, T>, right: ReadonlyMap<string, T>): boolean {
|
|
5
|
+
if (left === right) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if (left.size !== right.size) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
for (const [key, value] of left.entries()) {
|
|
12
|
+
if (!Object.is(value, right.get(key))) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ChatRunStatusManager {
|
|
20
|
+
syncSnapshot = (patch: Partial<ChatRunStatusSnapshot>) => {
|
|
21
|
+
const current = useChatRunStatusStore.getState().snapshot;
|
|
22
|
+
const nextMap = patch.sessionRunStatusByKey;
|
|
23
|
+
if (
|
|
24
|
+
(nextMap ? isMapEqual(current.sessionRunStatusByKey, nextMap) : true) &&
|
|
25
|
+
(patch.isLocallyRunning === undefined || Object.is(current.isLocallyRunning, patch.isLocallyRunning)) &&
|
|
26
|
+
(patch.activeBackendRunId === undefined || Object.is(current.activeBackendRunId, patch.activeBackendRunId))
|
|
27
|
+
) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
useChatRunStatusStore.getState().setSnapshot(patch);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
2
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
3
|
+
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
4
|
+
import type { ChatSessionListSnapshot } from '@/components/chat/stores/chat-session-list.store';
|
|
5
|
+
import type { SetStateAction } from 'react';
|
|
6
|
+
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
7
|
+
|
|
8
|
+
export class ChatSessionListManager {
|
|
9
|
+
constructor(
|
|
10
|
+
private uiManager: ChatUiManager,
|
|
11
|
+
private streamActionsManager: ChatStreamActionsManager
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
private hasSnapshotChanges = (patch: Partial<ChatSessionListSnapshot>): boolean => {
|
|
15
|
+
const current = useChatSessionListStore.getState().snapshot;
|
|
16
|
+
for (const [key, value] of Object.entries(patch) as Array<[keyof ChatSessionListSnapshot, ChatSessionListSnapshot[keyof ChatSessionListSnapshot]]>) {
|
|
17
|
+
if (!Object.is(current[key], value)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
|
|
25
|
+
if (typeof next === 'function') {
|
|
26
|
+
return (next as (value: T) => T)(prev);
|
|
27
|
+
}
|
|
28
|
+
return next;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
syncSnapshot = (patch: Partial<ChatSessionListSnapshot>) => {
|
|
32
|
+
if (!this.hasSnapshotChanges(patch)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
useChatSessionListStore.getState().setSnapshot(patch);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
setSelectedAgentId = (next: SetStateAction<string>) => {
|
|
39
|
+
const prev = useChatSessionListStore.getState().snapshot.selectedAgentId;
|
|
40
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
41
|
+
if (value === prev) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
useChatSessionListStore.getState().setSnapshot({ selectedAgentId: value });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
setSelectedSessionKey = (next: SetStateAction<string | null>) => {
|
|
48
|
+
const prev = useChatSessionListStore.getState().snapshot.selectedSessionKey;
|
|
49
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
50
|
+
if (value === prev) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
useChatSessionListStore.getState().setSnapshot({ selectedSessionKey: value });
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
createSession = () => {
|
|
57
|
+
const defaultSessionType = useChatInputStore.getState().snapshot.defaultSessionType || 'native';
|
|
58
|
+
this.streamActionsManager.resetStreamState();
|
|
59
|
+
this.setSelectedSessionKey(null);
|
|
60
|
+
useChatInputStore.getState().setSnapshot({ pendingSessionType: defaultSessionType });
|
|
61
|
+
this.uiManager.goToChatRoot();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
selectSession = (sessionKey: string) => {
|
|
65
|
+
this.setSelectedSessionKey(sessionKey);
|
|
66
|
+
this.uiManager.goToSession(sessionKey);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
setQuery = (next: SetStateAction<string>) => {
|
|
70
|
+
const prev = useChatSessionListStore.getState().snapshot.query;
|
|
71
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
72
|
+
if (value === prev) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
useChatSessionListStore.getState().setSnapshot({ query: value });
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ChatStreamActions } from '@/components/chat/chat-stream/types';
|
|
2
|
+
|
|
3
|
+
const noopAsync = async () => {};
|
|
4
|
+
const noop = () => {};
|
|
5
|
+
|
|
6
|
+
export class ChatStreamActionsManager {
|
|
7
|
+
private actions: ChatStreamActions = {
|
|
8
|
+
sendMessage: noopAsync,
|
|
9
|
+
stopCurrentRun: noopAsync,
|
|
10
|
+
resumeRun: noopAsync,
|
|
11
|
+
resetStreamState: noop,
|
|
12
|
+
applyHistoryMessages: noop
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
bind = (patch: Partial<ChatStreamActions>) => {
|
|
16
|
+
this.actions = {
|
|
17
|
+
...this.actions,
|
|
18
|
+
...patch
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
sendMessage = (payload: Parameters<ChatStreamActions['sendMessage']>[0]) => this.actions.sendMessage(payload);
|
|
23
|
+
|
|
24
|
+
stopCurrentRun = () => this.actions.stopCurrentRun();
|
|
25
|
+
|
|
26
|
+
resumeRun = (run: Parameters<ChatStreamActions['resumeRun']>[0]) => this.actions.resumeRun(run);
|
|
27
|
+
|
|
28
|
+
resetStreamState = () => this.actions.resetStreamState();
|
|
29
|
+
|
|
30
|
+
applyHistoryMessages = (
|
|
31
|
+
messages: Parameters<ChatStreamActions['applyHistoryMessages']>[0],
|
|
32
|
+
options?: Parameters<ChatStreamActions['applyHistoryMessages']>[1]
|
|
33
|
+
) => this.actions.applyHistoryMessages(messages, options);
|
|
34
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { deleteSession as deleteSessionApi } from '@/api/config';
|
|
2
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
3
|
+
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
4
|
+
import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
5
|
+
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
6
|
+
import type { ChatThreadSnapshot } from '@/components/chat/stores/chat-thread.store';
|
|
7
|
+
import { t } from '@/lib/i18n';
|
|
8
|
+
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
9
|
+
|
|
10
|
+
export type ChatThreadManagerActions = {
|
|
11
|
+
refetchSessions: () => Promise<unknown>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const noopAsync = async () => {};
|
|
15
|
+
export class ChatThreadManager {
|
|
16
|
+
private actions: ChatThreadManagerActions = {
|
|
17
|
+
refetchSessions: noopAsync
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private uiManager: ChatUiManager,
|
|
22
|
+
private sessionListManager: ChatSessionListManager,
|
|
23
|
+
private streamActionsManager: ChatStreamActionsManager
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
bindActions = (patch: Partial<ChatThreadManagerActions>) => {
|
|
27
|
+
this.actions = {
|
|
28
|
+
...this.actions,
|
|
29
|
+
...patch
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
private hasSnapshotChanges = (patch: Partial<ChatThreadSnapshot>): boolean => {
|
|
34
|
+
const current = useChatThreadStore.getState().snapshot;
|
|
35
|
+
for (const [key, value] of Object.entries(patch) as Array<[keyof ChatThreadSnapshot, ChatThreadSnapshot[keyof ChatThreadSnapshot]]>) {
|
|
36
|
+
if (!Object.is(current[key], value)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
syncSnapshot = (patch: Partial<ChatThreadSnapshot>) => {
|
|
44
|
+
if (!this.hasSnapshotChanges(patch)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
useChatThreadStore.getState().setSnapshot(patch);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
deleteSession = () => {
|
|
51
|
+
void this.deleteCurrentSession();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
createSession = () => {
|
|
55
|
+
this.sessionListManager.createSession();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
goToProviders = () => {
|
|
59
|
+
this.uiManager.goToProviders();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
private deleteCurrentSession = async () => {
|
|
63
|
+
const selectedSessionKey = useChatSessionListStore.getState().snapshot.selectedSessionKey;
|
|
64
|
+
if (!selectedSessionKey) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const confirmed = await this.uiManager.confirm({
|
|
68
|
+
title: t('chatDeleteSessionConfirm'),
|
|
69
|
+
variant: 'destructive',
|
|
70
|
+
confirmLabel: t('delete')
|
|
71
|
+
});
|
|
72
|
+
if (!confirmed) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
useChatThreadStore.getState().setSnapshot({ isDeletePending: true });
|
|
76
|
+
try {
|
|
77
|
+
await deleteSessionApi(selectedSessionKey);
|
|
78
|
+
this.streamActionsManager.resetStreamState();
|
|
79
|
+
useChatSessionListStore.getState().setSnapshot({ selectedSessionKey: null });
|
|
80
|
+
this.uiManager.goToChatRoot({ replace: true });
|
|
81
|
+
await this.actions.refetchSessions();
|
|
82
|
+
} finally {
|
|
83
|
+
useChatThreadStore.getState().setSnapshot({ isDeletePending: false });
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { buildSessionPath } from '@/components/chat/chat-session-route';
|
|
2
|
+
import type { NavigateFunction, NavigateOptions } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
type ChatUiState = {
|
|
5
|
+
pathname: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type ChatUiActions = {
|
|
9
|
+
navigate: NavigateFunction | null;
|
|
10
|
+
confirm: (params: { title: string; variant: 'destructive'; confirmLabel: string }) => Promise<boolean>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const noopConfirm = async (_params: { title: string; variant: 'destructive'; confirmLabel: string }) => false;
|
|
14
|
+
|
|
15
|
+
export class ChatUiManager {
|
|
16
|
+
private state: ChatUiState = {
|
|
17
|
+
pathname: ''
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
private actions: ChatUiActions = {
|
|
21
|
+
navigate: null,
|
|
22
|
+
confirm: noopConfirm
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
syncState = (patch: Partial<ChatUiState>) => {
|
|
26
|
+
this.state = {
|
|
27
|
+
...this.state,
|
|
28
|
+
...patch
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
bindActions = (patch: Partial<ChatUiActions>) => {
|
|
33
|
+
this.actions = {
|
|
34
|
+
...this.actions,
|
|
35
|
+
...patch
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
confirm = async (params: { title: string; variant: 'destructive'; confirmLabel: string }) => {
|
|
40
|
+
return this.actions.confirm(params);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
navigateTo = (to: string, options?: NavigateOptions) => {
|
|
44
|
+
if (!this.actions.navigate) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (this.state.pathname === to && !options?.replace) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.actions.navigate(to, options);
|
|
51
|
+
this.state.pathname = to;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
goToProviders = () => {
|
|
55
|
+
this.navigateTo('/providers');
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
goToChatRoot = (options?: NavigateOptions) => {
|
|
59
|
+
this.navigateTo('/chat', options);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
goToSession = (sessionKey: string, options?: NavigateOptions) => {
|
|
63
|
+
this.navigateTo(buildSessionPath(sessionKey), options);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { ChatPresenter } from '@/components/chat/presenter/chat.presenter';
|
|
4
|
+
|
|
5
|
+
const ChatPresenterContext = createContext<ChatPresenter | null>(null);
|
|
6
|
+
|
|
7
|
+
type ChatPresenterProviderProps = {
|
|
8
|
+
presenter: ChatPresenter;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function ChatPresenterProvider({ presenter, children }: ChatPresenterProviderProps) {
|
|
13
|
+
return <ChatPresenterContext.Provider value={presenter}>{children}</ChatPresenterContext.Provider>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function usePresenter(): ChatPresenter {
|
|
17
|
+
const presenter = useContext(ChatPresenterContext);
|
|
18
|
+
if (!presenter) {
|
|
19
|
+
throw new Error('usePresenter must be used inside ChatPresenterProvider');
|
|
20
|
+
}
|
|
21
|
+
return presenter;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Backward-compatible alias with the name from project notes.
|
|
25
|
+
export const usePresneter = usePresenter;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ChatInputManager } from '@/components/chat/managers/chat-input.manager';
|
|
2
|
+
import { nextbotParsers } from '@/components/chat/chat-stream/nextbot-parsers';
|
|
3
|
+
import { ChatRunStatusManager } from '@/components/chat/managers/chat-run-status.manager';
|
|
4
|
+
import { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
5
|
+
import { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
6
|
+
import { ChatThreadManager } from '@/components/chat/managers/chat-thread.manager';
|
|
7
|
+
import { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
8
|
+
import { NextbotRuntimeAgent } from '@/components/chat/chat-stream/nextbot-runtime-agent';
|
|
9
|
+
import { AgentChatController } from '@nextclaw/agent-chat';
|
|
10
|
+
|
|
11
|
+
export class ChatPresenter {
|
|
12
|
+
chatUiManager = new ChatUiManager();
|
|
13
|
+
runtimeAgent = new NextbotRuntimeAgent();
|
|
14
|
+
chatController = new AgentChatController(
|
|
15
|
+
{
|
|
16
|
+
agent: this.runtimeAgent,
|
|
17
|
+
getToolDefs: () => [],
|
|
18
|
+
getContexts: () => [],
|
|
19
|
+
getToolExecutor: () => undefined
|
|
20
|
+
},
|
|
21
|
+
{ metadataParsers: nextbotParsers }
|
|
22
|
+
);
|
|
23
|
+
chatStreamActionsManager = new ChatStreamActionsManager();
|
|
24
|
+
chatInputManager = new ChatInputManager(this.chatUiManager, this.chatStreamActionsManager);
|
|
25
|
+
chatSessionListManager = new ChatSessionListManager(this.chatUiManager, this.chatStreamActionsManager);
|
|
26
|
+
chatRunStatusManager = new ChatRunStatusManager();
|
|
27
|
+
chatThreadManager = new ChatThreadManager(
|
|
28
|
+
this.chatUiManager,
|
|
29
|
+
this.chatSessionListManager,
|
|
30
|
+
this.chatStreamActionsManager
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { MarketplaceInstalledRecord } from '@/api/types';
|
|
3
|
+
|
|
4
|
+
export type ChatInputSnapshot = {
|
|
5
|
+
isProviderStateResolved: boolean;
|
|
6
|
+
draft: string;
|
|
7
|
+
pendingSessionType: string;
|
|
8
|
+
defaultSessionType: string;
|
|
9
|
+
canStopGeneration: boolean;
|
|
10
|
+
stopDisabledReason: string | null;
|
|
11
|
+
sendError: string | null;
|
|
12
|
+
isSending: boolean;
|
|
13
|
+
modelOptions: Array<{ value: string; modelLabel: string; providerLabel: string }>;
|
|
14
|
+
selectedModel: string;
|
|
15
|
+
sessionTypeOptions: Array<{ value: string; label: string }>;
|
|
16
|
+
selectedSessionType?: string;
|
|
17
|
+
stopSupported: boolean;
|
|
18
|
+
stopReason?: string;
|
|
19
|
+
canEditSessionType: boolean;
|
|
20
|
+
sessionTypeUnavailable: boolean;
|
|
21
|
+
skillRecords: MarketplaceInstalledRecord[];
|
|
22
|
+
isSkillsLoading: boolean;
|
|
23
|
+
selectedSkills: string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ChatInputStore = {
|
|
27
|
+
snapshot: ChatInputSnapshot;
|
|
28
|
+
setSnapshot: (patch: Partial<ChatInputSnapshot>) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const initialSnapshot: ChatInputSnapshot = {
|
|
32
|
+
isProviderStateResolved: false,
|
|
33
|
+
draft: '',
|
|
34
|
+
pendingSessionType: 'native',
|
|
35
|
+
defaultSessionType: 'native',
|
|
36
|
+
canStopGeneration: false,
|
|
37
|
+
stopDisabledReason: null,
|
|
38
|
+
sendError: null,
|
|
39
|
+
isSending: false,
|
|
40
|
+
modelOptions: [],
|
|
41
|
+
selectedModel: '',
|
|
42
|
+
sessionTypeOptions: [],
|
|
43
|
+
selectedSessionType: undefined,
|
|
44
|
+
stopSupported: false,
|
|
45
|
+
stopReason: undefined,
|
|
46
|
+
canEditSessionType: false,
|
|
47
|
+
sessionTypeUnavailable: false,
|
|
48
|
+
skillRecords: [],
|
|
49
|
+
isSkillsLoading: false,
|
|
50
|
+
selectedSkills: []
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const useChatInputStore = create<ChatInputStore>((set) => ({
|
|
54
|
+
snapshot: initialSnapshot,
|
|
55
|
+
setSnapshot: (patch) =>
|
|
56
|
+
set((state) => ({
|
|
57
|
+
snapshot: {
|
|
58
|
+
...state.snapshot,
|
|
59
|
+
...patch
|
|
60
|
+
}
|
|
61
|
+
}))
|
|
62
|
+
}));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { SessionRunStatus } from '@/lib/session-run-status';
|
|
3
|
+
|
|
4
|
+
export type ChatRunStatusSnapshot = {
|
|
5
|
+
sessionRunStatusByKey: ReadonlyMap<string, SessionRunStatus>;
|
|
6
|
+
isLocallyRunning: boolean;
|
|
7
|
+
activeBackendRunId: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ChatRunStatusStore = {
|
|
11
|
+
snapshot: ChatRunStatusSnapshot;
|
|
12
|
+
setSnapshot: (patch: Partial<ChatRunStatusSnapshot>) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const initialSnapshot: ChatRunStatusSnapshot = {
|
|
16
|
+
sessionRunStatusByKey: new Map(),
|
|
17
|
+
isLocallyRunning: false,
|
|
18
|
+
activeBackendRunId: null
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const useChatRunStatusStore = create<ChatRunStatusStore>((set) => ({
|
|
22
|
+
snapshot: initialSnapshot,
|
|
23
|
+
setSnapshot: (patch) =>
|
|
24
|
+
set((state) => ({
|
|
25
|
+
snapshot: {
|
|
26
|
+
...state.snapshot,
|
|
27
|
+
...patch
|
|
28
|
+
}
|
|
29
|
+
}))
|
|
30
|
+
}));
|