@tuturuuu/ui 0.1.0 → 0.2.0
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/package.json +6 -6
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +43 -13
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +138 -74
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +60 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +13 -5
- package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
- package/src/components/ui/chat/chat-sidebar-panel.tsx +13 -3
- package/src/components/ui/legacy/meet/planId/page.tsx +10 -4
- package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +203 -6
- package/src/components/ui/text-editor/task-mention-chip.tsx +29 -7
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-realtime-sync.test.tsx +37 -9
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +89 -70
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0](https://github.com/tutur3u/platform/compare/ui-v0.1.0...ui-v0.2.0) (2026-06-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **chat:** add generated titles and personal sections ([10234b4](https://github.com/tutur3u/platform/commit/10234b4b8d48eb44828b89f86b7fcf59d587432e))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **chat:** restore title generation and pagination ([f85df59](https://github.com/tutur3u/platform/commit/f85df59fba274c694fd38a991607e8d263ae1af3))
|
|
14
|
+
* **chat:** support ai-agent title and gateway verification ([296cd07](https://github.com/tutur3u/platform/commit/296cd0727b56b8b2440e6877932c74fcad07e800))
|
|
15
|
+
* **meet:** protect workspace plan detail pages ([2574e45](https://github.com/tutur3u/platform/commit/2574e45d5a44db3425438233794bd620abfec778))
|
|
16
|
+
* **release:** repair package publishing metadata ([88d9a6d](https://github.com/tutur3u/platform/commit/88d9a6dcc3556b1d1aa677c0592a1e1901a389e3))
|
|
17
|
+
* **ui:** bind stale task mentions to route workspace ([344773f](https://github.com/tutur3u/platform/commit/344773f7b3b04634a11423c8c31c95fefa0ec437))
|
|
18
|
+
* **ui:** refetch task dialog broadcast updates ([d7259b6](https://github.com/tutur3u/platform/commit/d7259b61daa33cb933df0c68222f43352e31b743))
|
|
19
|
+
* **ui:** scope task mention resolution cache ([ef60769](https://github.com/tutur3u/platform/commit/ef607698f220a8e798ef797ffc90690b1711b3bd))
|
|
20
|
+
|
|
3
21
|
## [0.1.0](https://github.com/tutur3u/platform/compare/ui-v0.0.4...ui-v0.1.0) (2026-06-02)
|
|
4
22
|
|
|
5
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@tanstack/react-pacer": "^0.22.1",
|
|
60
60
|
"@tanstack/react-query": "^5.100.11",
|
|
61
61
|
"@tanstack/react-table": "^8.21.3",
|
|
62
|
-
"@tanstack/react-virtual": "^3.
|
|
62
|
+
"@tanstack/react-virtual": "^3.14.1",
|
|
63
63
|
"@tiptap/core": "3.24.0",
|
|
64
64
|
"@tiptap/extension-collaboration": "3.24.0",
|
|
65
65
|
"@tiptap/extension-collaboration-caret": "3.24.0",
|
|
@@ -109,16 +109,16 @@
|
|
|
109
109
|
"lodash": "^4.18.1",
|
|
110
110
|
"moment": "^2.30.1",
|
|
111
111
|
"motion": "^12.40.0",
|
|
112
|
-
"next": "^16.2.
|
|
112
|
+
"next": "^16.2.7",
|
|
113
113
|
"next-intl": "^4.13.0",
|
|
114
114
|
"next-themes": "^0.4.6",
|
|
115
115
|
"nuqs": "^2.8.9",
|
|
116
116
|
"prosemirror-state": "^1.4.4",
|
|
117
117
|
"qrcode.react": "^4.2.0",
|
|
118
|
-
"react": "^19.2.
|
|
118
|
+
"react": "^19.2.7",
|
|
119
119
|
"react-colorful": "^5.7.0",
|
|
120
120
|
"react-day-picker": "^10.0.1",
|
|
121
|
-
"react-dom": "^19.2.
|
|
121
|
+
"react-dom": "^19.2.7",
|
|
122
122
|
"react-dropzone": "^15.0.0",
|
|
123
123
|
"react-hook-form": "^7.77.0",
|
|
124
124
|
"react-markdown": "^10.1.0",
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
"@types/html2canvas": "^1.0.0",
|
|
154
154
|
"@types/lodash": "^4.17.24",
|
|
155
155
|
"@types/node": "^25.9.1",
|
|
156
|
-
"@types/react": "^19.2.
|
|
156
|
+
"@types/react": "^19.2.16",
|
|
157
157
|
"@types/react-dom": "^19.2.3",
|
|
158
158
|
"@types/react-resizable": "^4.0.0",
|
|
159
159
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
@@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
|
2
2
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { AgentExternalThreadPanel } from './chat-agent-details-external-thread-panel';
|
|
5
|
+
import type { AgentConversationMetadata } from './chat-agent-details-utils';
|
|
5
6
|
|
|
6
7
|
const mocks = vi.hoisted(() => ({
|
|
7
8
|
draftAiAgentExternalResponse: vi.fn(),
|
|
@@ -35,7 +36,18 @@ vi.mock('../sonner', () => ({
|
|
|
35
36
|
},
|
|
36
37
|
}));
|
|
37
38
|
|
|
38
|
-
function renderPanel(
|
|
39
|
+
function renderPanel(
|
|
40
|
+
onRefresh = vi.fn(),
|
|
41
|
+
metadata: AgentConversationMetadata = {
|
|
42
|
+
agentId: 'agent-1',
|
|
43
|
+
channelId: 'channel-1',
|
|
44
|
+
externalChannelId: 'external-channel-1',
|
|
45
|
+
externalThreadId: 'discord-thread-1',
|
|
46
|
+
externalThreadUuid: 'thread-uuid-1',
|
|
47
|
+
messageCount: 2,
|
|
48
|
+
source: 'ai-agent-external-thread',
|
|
49
|
+
}
|
|
50
|
+
) {
|
|
39
51
|
const queryClient = new QueryClient({
|
|
40
52
|
defaultOptions: {
|
|
41
53
|
mutations: { retry: false },
|
|
@@ -45,18 +57,7 @@ function renderPanel(onRefresh = vi.fn()) {
|
|
|
45
57
|
|
|
46
58
|
render(
|
|
47
59
|
<QueryClientProvider client={queryClient}>
|
|
48
|
-
<AgentExternalThreadPanel
|
|
49
|
-
metadata={{
|
|
50
|
-
agentId: 'agent-1',
|
|
51
|
-
channelId: 'channel-1',
|
|
52
|
-
externalChannelId: 'external-channel-1',
|
|
53
|
-
externalThreadId: 'discord-thread-1',
|
|
54
|
-
externalThreadUuid: 'thread-uuid-1',
|
|
55
|
-
messageCount: 2,
|
|
56
|
-
source: 'ai-agent-external-thread',
|
|
57
|
-
}}
|
|
58
|
-
onRefresh={onRefresh}
|
|
59
|
-
/>
|
|
60
|
+
<AgentExternalThreadPanel metadata={metadata} onRefresh={onRefresh} />
|
|
60
61
|
</QueryClientProvider>
|
|
61
62
|
);
|
|
62
63
|
|
|
@@ -72,8 +73,10 @@ describe('AgentExternalThreadPanel', () => {
|
|
|
72
73
|
mocks.listAiAgentExternalThreads.mockResolvedValue({
|
|
73
74
|
threads: [
|
|
74
75
|
{
|
|
76
|
+
externalThreadId: 'discord-thread-1',
|
|
75
77
|
id: 'thread-uuid-1',
|
|
76
78
|
lastSyncedAt: '2026-06-02T01:02:03.000Z',
|
|
79
|
+
messageCount: 2,
|
|
77
80
|
},
|
|
78
81
|
],
|
|
79
82
|
});
|
|
@@ -100,6 +103,33 @@ describe('AgentExternalThreadPanel', () => {
|
|
|
100
103
|
expect(onRefresh).toHaveBeenCalled();
|
|
101
104
|
});
|
|
102
105
|
|
|
106
|
+
it('lists and syncs recent threads from an agent setup conversation', async () => {
|
|
107
|
+
const { onRefresh } = renderPanel(vi.fn(), {
|
|
108
|
+
agentId: 'agent-1',
|
|
109
|
+
channelId: 'channel-1',
|
|
110
|
+
source: 'ai-agent' as const,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await waitFor(() => {
|
|
114
|
+
expect(mocks.listAiAgentExternalThreads).toHaveBeenCalledWith({
|
|
115
|
+
agentId: 'agent-1',
|
|
116
|
+
channelId: 'channel-1',
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(screen.getByText('discord-thread-1')).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
fireEvent.click(screen.getByText('agent_external_sync'));
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(mocks.syncAiAgentExternalThread).toHaveBeenCalledWith(
|
|
127
|
+
'thread-uuid-1'
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
expect(onRefresh).toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
|
|
103
133
|
it('drafts and sends an external response', async () => {
|
|
104
134
|
const { onRefresh } = renderPanel();
|
|
105
135
|
|
|
@@ -36,29 +36,34 @@ export function AgentExternalThreadPanel({
|
|
|
36
36
|
const [draft, setDraft] = useState('');
|
|
37
37
|
const [prompt, setPrompt] = useState('');
|
|
38
38
|
const threadId = metadata.externalThreadUuid;
|
|
39
|
-
const
|
|
40
|
-
enabled: Boolean(
|
|
39
|
+
const threadsQuery = useQuery({
|
|
40
|
+
enabled: Boolean(metadata.agentId && metadata.channelId),
|
|
41
41
|
queryFn: async () => {
|
|
42
42
|
const result = await listAiAgentExternalThreads({
|
|
43
43
|
agentId: metadata.agentId,
|
|
44
44
|
channelId: metadata.channelId,
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
return result.threads
|
|
47
|
+
return result.threads;
|
|
48
48
|
},
|
|
49
49
|
queryKey: [
|
|
50
50
|
'chat',
|
|
51
|
-
'ai-agent-external-
|
|
51
|
+
'ai-agent-external-threads',
|
|
52
52
|
metadata.agentId,
|
|
53
53
|
metadata.channelId,
|
|
54
|
-
threadId,
|
|
55
54
|
],
|
|
56
55
|
staleTime: 30_000,
|
|
57
56
|
});
|
|
57
|
+
const threads = threadsQuery.data ?? [];
|
|
58
|
+
const selectedThread = threadId
|
|
59
|
+
? threads.find((thread) => thread.id === threadId)
|
|
60
|
+
: null;
|
|
58
61
|
const syncMutation = useMutation({
|
|
59
|
-
mutationFn: () => {
|
|
60
|
-
if (!
|
|
61
|
-
|
|
62
|
+
mutationFn: (targetThreadId?: string) => {
|
|
63
|
+
if (!targetThreadId) {
|
|
64
|
+
throw new Error(t('agent_external_thread_missing'));
|
|
65
|
+
}
|
|
66
|
+
return syncAiAgentExternalThread(targetThreadId);
|
|
62
67
|
},
|
|
63
68
|
onError: (error) =>
|
|
64
69
|
toast.error(error.message || t('agent_external_sync_failed')),
|
|
@@ -73,7 +78,7 @@ export function AgentExternalThreadPanel({
|
|
|
73
78
|
? t('agent_external_sync_no_new')
|
|
74
79
|
: t('agent_external_sync_success', { count: result.synced })
|
|
75
80
|
);
|
|
76
|
-
void
|
|
81
|
+
void threadsQuery.refetch();
|
|
77
82
|
onRefresh();
|
|
78
83
|
},
|
|
79
84
|
});
|
|
@@ -128,77 +133,136 @@ export function AgentExternalThreadPanel({
|
|
|
128
133
|
/>
|
|
129
134
|
<KeyValue
|
|
130
135
|
label={t('agent_external_last_sync')}
|
|
131
|
-
value={
|
|
136
|
+
value={selectedThread?.lastSyncedAt ?? t('unknown')}
|
|
132
137
|
/>
|
|
133
138
|
</div>
|
|
134
139
|
</PanelSection>
|
|
135
140
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
141
|
+
{threadId ? (
|
|
142
|
+
<Button
|
|
143
|
+
className="w-full"
|
|
144
|
+
disabled={syncMutation.isPending}
|
|
145
|
+
onClick={() => syncMutation.mutate(threadId)}
|
|
146
|
+
type="button"
|
|
147
|
+
variant="outline"
|
|
148
|
+
>
|
|
149
|
+
{syncMutation.isPending ? (
|
|
150
|
+
<LoaderCircle className="size-4 animate-spin" />
|
|
151
|
+
) : (
|
|
152
|
+
<RefreshCw className="size-4" />
|
|
153
|
+
)}
|
|
154
|
+
{t('agent_external_sync')}
|
|
155
|
+
</Button>
|
|
156
|
+
) : (
|
|
157
|
+
<PanelSection
|
|
158
|
+
icon={<RefreshCw className="size-4" />}
|
|
159
|
+
title={t('agent_external_recent_threads')}
|
|
160
|
+
>
|
|
161
|
+
<div className="space-y-2">
|
|
162
|
+
{threadsQuery.isLoading ? (
|
|
163
|
+
<p className="flex items-center gap-2 text-muted-foreground text-xs">
|
|
164
|
+
<LoaderCircle className="size-3.5 animate-spin" />
|
|
165
|
+
{t('loading_ai_settings')}
|
|
166
|
+
</p>
|
|
167
|
+
) : threads.length > 0 ? (
|
|
168
|
+
threads.map((thread) => (
|
|
169
|
+
<div
|
|
170
|
+
className="space-y-2 rounded-md border bg-muted/20 p-2"
|
|
171
|
+
key={thread.id}
|
|
172
|
+
>
|
|
173
|
+
<div className="min-w-0 text-xs">
|
|
174
|
+
<div className="truncate font-medium">
|
|
175
|
+
{thread.title || thread.externalThreadId}
|
|
176
|
+
</div>
|
|
177
|
+
{thread.title ? (
|
|
178
|
+
<div className="truncate text-muted-foreground">
|
|
179
|
+
{thread.externalThreadId}
|
|
180
|
+
</div>
|
|
181
|
+
) : null}
|
|
182
|
+
</div>
|
|
183
|
+
<div className="flex items-center justify-between gap-2 text-muted-foreground text-xs">
|
|
184
|
+
<span>
|
|
185
|
+
{t('agent_external_message_count')}: {thread.messageCount}
|
|
186
|
+
</span>
|
|
187
|
+
<Button
|
|
188
|
+
disabled={syncMutation.isPending}
|
|
189
|
+
onClick={() => syncMutation.mutate(thread.id)}
|
|
190
|
+
size="sm"
|
|
191
|
+
type="button"
|
|
192
|
+
variant="outline"
|
|
193
|
+
>
|
|
194
|
+
{syncMutation.isPending ? (
|
|
195
|
+
<LoaderCircle className="size-3.5 animate-spin" />
|
|
196
|
+
) : (
|
|
197
|
+
<RefreshCw className="size-3.5" />
|
|
198
|
+
)}
|
|
199
|
+
{t('agent_external_sync')}
|
|
200
|
+
</Button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
))
|
|
204
|
+
) : (
|
|
205
|
+
<p className="text-muted-foreground text-xs">
|
|
206
|
+
{t('agent_external_no_threads')}
|
|
207
|
+
</p>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</PanelSection>
|
|
211
|
+
)}
|
|
150
212
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
213
|
+
{!threadId ? null : (
|
|
214
|
+
<PanelSection
|
|
215
|
+
icon={<Sparkles className="size-4" />}
|
|
216
|
+
title={t('agent_external_response')}
|
|
217
|
+
>
|
|
218
|
+
<div className="space-y-2">
|
|
219
|
+
<Textarea
|
|
220
|
+
onChange={(event) => setPrompt(event.target.value)}
|
|
221
|
+
placeholder={t('agent_external_prompt_placeholder')}
|
|
222
|
+
rows={3}
|
|
223
|
+
value={prompt}
|
|
224
|
+
/>
|
|
225
|
+
<Textarea
|
|
226
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
227
|
+
placeholder={t('agent_external_draft_placeholder')}
|
|
228
|
+
rows={5}
|
|
229
|
+
value={draft}
|
|
230
|
+
/>
|
|
231
|
+
<div className="grid grid-cols-2 gap-2">
|
|
232
|
+
<Button
|
|
233
|
+
disabled={!threadId || draftMutation.isPending}
|
|
234
|
+
onClick={() => draftMutation.mutate()}
|
|
235
|
+
type="button"
|
|
236
|
+
variant="secondary"
|
|
237
|
+
>
|
|
238
|
+
{draftMutation.isPending ? (
|
|
239
|
+
<LoaderCircle className="size-4 animate-spin" />
|
|
240
|
+
) : (
|
|
241
|
+
<Sparkles className="size-4" />
|
|
242
|
+
)}
|
|
243
|
+
{t('agent_external_draft')}
|
|
244
|
+
</Button>
|
|
245
|
+
<Button
|
|
246
|
+
disabled={!threadId || !draft.trim() || sendMutation.isPending}
|
|
247
|
+
onClick={() => sendMutation.mutate()}
|
|
248
|
+
type="button"
|
|
249
|
+
>
|
|
250
|
+
{sendMutation.isPending ? (
|
|
251
|
+
<LoaderCircle className="size-4 animate-spin" />
|
|
252
|
+
) : (
|
|
253
|
+
<Send className="size-4" />
|
|
254
|
+
)}
|
|
255
|
+
{t('agent_external_send')}
|
|
256
|
+
</Button>
|
|
257
|
+
</div>
|
|
258
|
+
{isPending ? (
|
|
259
|
+
<p className="text-muted-foreground text-xs">
|
|
260
|
+
{t('agent_external_refresh_after_action')}
|
|
261
|
+
</p>
|
|
262
|
+
) : null}
|
|
194
263
|
</div>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
{t('agent_external_refresh_after_action')}
|
|
198
|
-
</p>
|
|
199
|
-
) : null}
|
|
200
|
-
</div>
|
|
201
|
-
</PanelSection>
|
|
264
|
+
</PanelSection>
|
|
265
|
+
)}
|
|
202
266
|
</div>
|
|
203
267
|
);
|
|
204
268
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { AgentOperationsPanel } from './chat-agent-details-operations-panel';
|
|
4
|
+
|
|
5
|
+
vi.mock('next-intl', () => ({
|
|
6
|
+
useTranslations: () => (key: string) => key,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const channel = {
|
|
10
|
+
adapter: 'discord' as const,
|
|
11
|
+
autoRespond: true,
|
|
12
|
+
discordGuildId: 'guild-1',
|
|
13
|
+
displayName: 'Discord',
|
|
14
|
+
enabled: true,
|
|
15
|
+
externalChannelId: 'channel-1',
|
|
16
|
+
historySyncEnabled: true,
|
|
17
|
+
id: 'channel-1',
|
|
18
|
+
lastDeployedAt: '2026-06-02T00:00:00.000Z',
|
|
19
|
+
lastError: null,
|
|
20
|
+
lastEventAt: '2026-06-02T00:01:00.000Z',
|
|
21
|
+
mentionRoleIds: [],
|
|
22
|
+
secrets: [],
|
|
23
|
+
status: 'deployed' as const,
|
|
24
|
+
webhookUrl: 'https://example.com/webhook',
|
|
25
|
+
workspaceId: 'workspace-1',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('AgentOperationsPanel', () => {
|
|
29
|
+
it('renders structured diagnostics after a channel test', () => {
|
|
30
|
+
render(
|
|
31
|
+
<AgentOperationsPanel
|
|
32
|
+
channel={channel}
|
|
33
|
+
isPending={false}
|
|
34
|
+
onCopySecret={vi.fn()}
|
|
35
|
+
onDeploy={vi.fn()}
|
|
36
|
+
onPause={vi.fn()}
|
|
37
|
+
onRotateSecret={vi.fn()}
|
|
38
|
+
onTest={vi.fn()}
|
|
39
|
+
secretPreview={null}
|
|
40
|
+
testResult={{
|
|
41
|
+
checks: [
|
|
42
|
+
{
|
|
43
|
+
detail: 'All required channel secrets are configured.',
|
|
44
|
+
id: 'required_secrets',
|
|
45
|
+
label: 'Required secrets',
|
|
46
|
+
ok: true,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
detail: 'Set the Discord guild ID.',
|
|
50
|
+
id: 'adapter_account',
|
|
51
|
+
label: 'Discord guild mapping',
|
|
52
|
+
ok: false,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
ok: false,
|
|
56
|
+
response: 'AI agent channel needs attention.',
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(screen.getByText('agent_diagnostics')).toBeInTheDocument();
|
|
62
|
+
expect(
|
|
63
|
+
screen.getByText('agent_diagnostic_required_secrets')
|
|
64
|
+
).toBeInTheDocument();
|
|
65
|
+
expect(
|
|
66
|
+
screen.getByText('agent_diagnostic_adapter_account')
|
|
67
|
+
).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByText('Set the Discord guild ID.')).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
Check,
|
|
4
5
|
Copy,
|
|
5
6
|
FlaskConical,
|
|
6
7
|
LoaderCircle,
|
|
@@ -8,8 +9,12 @@ import {
|
|
|
8
9
|
Play,
|
|
9
10
|
RotateCw,
|
|
10
11
|
Webhook,
|
|
12
|
+
X,
|
|
11
13
|
} from '@tuturuuu/icons';
|
|
12
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
AiAgentChannelConfig,
|
|
16
|
+
AiAgentTestResponse,
|
|
17
|
+
} from '@tuturuuu/internal-api/infrastructure';
|
|
13
18
|
import { useTranslations } from 'next-intl';
|
|
14
19
|
import { useState } from 'react';
|
|
15
20
|
import { Button } from '../button';
|
|
@@ -20,6 +25,18 @@ import {
|
|
|
20
25
|
PanelSection,
|
|
21
26
|
} from './chat-agent-details-utils';
|
|
22
27
|
|
|
28
|
+
const DIAGNOSTIC_LABEL_KEYS = {
|
|
29
|
+
adapter_account: 'agent_diagnostic_adapter_account',
|
|
30
|
+
agent_enabled: 'agent_diagnostic_agent_enabled',
|
|
31
|
+
channel_deployed: 'agent_diagnostic_channel_deployed',
|
|
32
|
+
channel_enabled: 'agent_diagnostic_channel_enabled',
|
|
33
|
+
last_error: 'agent_diagnostic_last_error',
|
|
34
|
+
last_event: 'agent_diagnostic_last_event',
|
|
35
|
+
required_secrets: 'agent_diagnostic_required_secrets',
|
|
36
|
+
webhook_url: 'agent_diagnostic_webhook_url',
|
|
37
|
+
workspace_mapping: 'agent_diagnostic_workspace_mapping',
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
23
40
|
export function AgentOperationsPanel({
|
|
24
41
|
channel,
|
|
25
42
|
isPending,
|
|
@@ -29,6 +46,7 @@ export function AgentOperationsPanel({
|
|
|
29
46
|
onRotateSecret,
|
|
30
47
|
onTest,
|
|
31
48
|
secretPreview,
|
|
49
|
+
testResult,
|
|
32
50
|
}: {
|
|
33
51
|
channel: AiAgentChannelConfig;
|
|
34
52
|
isPending: boolean;
|
|
@@ -38,10 +56,19 @@ export function AgentOperationsPanel({
|
|
|
38
56
|
onRotateSecret: () => void;
|
|
39
57
|
onTest: (prompt?: string) => void;
|
|
40
58
|
secretPreview: { label: string; value: string } | null;
|
|
59
|
+
testResult?: AiAgentTestResponse | null;
|
|
41
60
|
}) {
|
|
42
61
|
const t = useTranslations('chat');
|
|
43
62
|
const [testPrompt, setTestPrompt] = useState('');
|
|
44
63
|
const webhookUrl = channel.webhookUrl;
|
|
64
|
+
const diagnosticLabel = (
|
|
65
|
+
check: NonNullable<AiAgentTestResponse['checks']>[number]
|
|
66
|
+
) => {
|
|
67
|
+
const key =
|
|
68
|
+
DIAGNOSTIC_LABEL_KEYS[check.id as keyof typeof DIAGNOSTIC_LABEL_KEYS];
|
|
69
|
+
|
|
70
|
+
return key ? t(key) : check.label;
|
|
71
|
+
};
|
|
45
72
|
|
|
46
73
|
return (
|
|
47
74
|
<div className="space-y-4">
|
|
@@ -135,6 +162,38 @@ export function AgentOperationsPanel({
|
|
|
135
162
|
</div>
|
|
136
163
|
</PanelSection>
|
|
137
164
|
|
|
165
|
+
{testResult?.checks?.length ? (
|
|
166
|
+
<PanelSection
|
|
167
|
+
icon={<FlaskConical className="size-4" />}
|
|
168
|
+
title={t('agent_diagnostics')}
|
|
169
|
+
>
|
|
170
|
+
<ul className="space-y-2">
|
|
171
|
+
{testResult.checks.map((check) => (
|
|
172
|
+
<li
|
|
173
|
+
className="rounded-md border bg-muted/20 p-2 text-xs"
|
|
174
|
+
key={check.id}
|
|
175
|
+
>
|
|
176
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
177
|
+
{check.ok ? (
|
|
178
|
+
<Check className="size-3.5 shrink-0 text-dynamic-green" />
|
|
179
|
+
) : (
|
|
180
|
+
<X className="size-3.5 shrink-0 text-dynamic-red" />
|
|
181
|
+
)}
|
|
182
|
+
<span className="min-w-0 flex-1 truncate font-medium">
|
|
183
|
+
{diagnosticLabel(check)}
|
|
184
|
+
</span>
|
|
185
|
+
</div>
|
|
186
|
+
{check.detail ? (
|
|
187
|
+
<p className="mt-1 break-words text-muted-foreground">
|
|
188
|
+
{check.detail}
|
|
189
|
+
</p>
|
|
190
|
+
) : null}
|
|
191
|
+
</li>
|
|
192
|
+
))}
|
|
193
|
+
</ul>
|
|
194
|
+
</PanelSection>
|
|
195
|
+
) : null}
|
|
196
|
+
|
|
138
197
|
{channel.adapter === 'zalo' ? (
|
|
139
198
|
<PanelSection
|
|
140
199
|
icon={<RotateCw className="size-4" />}
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
4
|
import { Bot, LoaderCircle } from '@tuturuuu/icons';
|
|
5
5
|
import type { ChatConversation } from '@tuturuuu/internal-api';
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
AiAgentTestResponse,
|
|
8
|
+
SaveAiAgentPayload,
|
|
9
|
+
} from '@tuturuuu/internal-api/infrastructure';
|
|
7
10
|
import {
|
|
8
11
|
deployAiAgentChannel,
|
|
9
12
|
listAiAgents,
|
|
@@ -45,6 +48,9 @@ export function ChatAgentDetailsSidebar({
|
|
|
45
48
|
label: string;
|
|
46
49
|
value: string;
|
|
47
50
|
} | null>(null);
|
|
51
|
+
const [testResult, setTestResult] = useState<AiAgentTestResponse | null>(
|
|
52
|
+
null
|
|
53
|
+
);
|
|
48
54
|
const agentsQuery = useQuery({
|
|
49
55
|
enabled: open && Boolean(metadata),
|
|
50
56
|
queryFn: () => listAiAgents(),
|
|
@@ -65,10 +71,7 @@ export function ChatAgentDetailsSidebar({
|
|
|
65
71
|
: undefined,
|
|
66
72
|
[agent, metadata]
|
|
67
73
|
);
|
|
68
|
-
const tabs: AgentTab[] =
|
|
69
|
-
metadata?.source === 'ai-agent-external-thread'
|
|
70
|
-
? ['setup', 'operations', 'thread']
|
|
71
|
-
: ['setup', 'operations'];
|
|
74
|
+
const tabs: AgentTab[] = ['setup', 'operations', 'thread'];
|
|
72
75
|
const refreshAgent = () =>
|
|
73
76
|
queryClient.invalidateQueries({ queryKey: AGENT_QUERY_KEY });
|
|
74
77
|
const refreshChat = () => {
|
|
@@ -122,6 +125,7 @@ export function ChatAgentDetailsSidebar({
|
|
|
122
125
|
: Promise.reject(new Error(t('agent_not_found'))),
|
|
123
126
|
onError: (error) => toast.error(error.message || t('agent_test_failed')),
|
|
124
127
|
onSuccess: (result) => {
|
|
128
|
+
setTestResult(result);
|
|
125
129
|
if (result.ok) toast.success(result.response || t('agent_test_success'));
|
|
126
130
|
else toast.error(result.response || t('agent_test_failed'));
|
|
127
131
|
},
|
|
@@ -204,6 +208,7 @@ export function ChatAgentDetailsSidebar({
|
|
|
204
208
|
onTest: (prompt) => testMutation.mutate(prompt),
|
|
205
209
|
secretPreview,
|
|
206
210
|
tabs,
|
|
211
|
+
testResult,
|
|
207
212
|
t,
|
|
208
213
|
agent,
|
|
209
214
|
})}
|
|
@@ -229,6 +234,7 @@ function renderContent({
|
|
|
229
234
|
onTest,
|
|
230
235
|
secretPreview,
|
|
231
236
|
tabs,
|
|
237
|
+
testResult,
|
|
232
238
|
t,
|
|
233
239
|
}: {
|
|
234
240
|
agent?: Awaited<ReturnType<typeof listAiAgents>>['agents'][number];
|
|
@@ -248,6 +254,7 @@ function renderContent({
|
|
|
248
254
|
onTest: (prompt?: string) => void;
|
|
249
255
|
secretPreview: { label: string; value: string } | null;
|
|
250
256
|
tabs: AgentTab[];
|
|
257
|
+
testResult: AiAgentTestResponse | null;
|
|
251
258
|
t: ReturnType<typeof useTranslations>;
|
|
252
259
|
}) {
|
|
253
260
|
if (!metadata) {
|
|
@@ -296,6 +303,7 @@ function renderContent({
|
|
|
296
303
|
onRotateSecret={onRotateSecret}
|
|
297
304
|
onTest={onTest}
|
|
298
305
|
secretPreview={secretPreview}
|
|
306
|
+
testResult={testResult}
|
|
299
307
|
/>
|
|
300
308
|
</TabsContent>
|
|
301
309
|
{tabs.includes('thread') ? (
|