@newheap/platform-ai-chat 0.1.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/LICENSE +201 -0
- package/NOTICE +4 -0
- package/README.md +294 -0
- package/admin/index.d.ts +294 -0
- package/fesm2022/newheap-platform-ai-chat-admin.mjs +785 -0
- package/fesm2022/newheap-platform-ai-chat-admin.mjs.map +1 -0
- package/fesm2022/newheap-platform-ai-chat-testing.mjs +911 -0
- package/fesm2022/newheap-platform-ai-chat-testing.mjs.map +1 -0
- package/fesm2022/newheap-platform-ai-chat.mjs +3103 -0
- package/fesm2022/newheap-platform-ai-chat.mjs.map +1 -0
- package/i18n/en.json +376 -0
- package/i18n/nl.json +376 -0
- package/index.d.ts +937 -0
- package/package.json +54 -0
- package/testing/index.d.ts +189 -0
|
@@ -0,0 +1,3103 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, DestroyRef, Injectable, EnvironmentInjector, runInInjectionContext, signal, computed, Injector, afterNextRender, makeEnvironmentProviders, provideEnvironmentInitializer, input, ChangeDetectionStrategy, Component, effect, untracked, ChangeDetectorRef, Pipe, output, viewChild, ViewContainerRef } from '@angular/core';
|
|
3
|
+
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
|
4
|
+
import { merge, Observable, firstValueFrom, isObservable, Subscription } from 'rxjs';
|
|
5
|
+
import { BreakpointObserver } from '@angular/cdk/layout';
|
|
6
|
+
import { Overlay } from '@angular/cdk/overlay';
|
|
7
|
+
import { DOCUMENT, DatePipe, NgTemplateOutlet } from '@angular/common';
|
|
8
|
+
import { TemplatePortal } from '@angular/cdk/portal';
|
|
9
|
+
import { Router, NavigationEnd, RouterLink } from '@angular/router';
|
|
10
|
+
import * as i1 from '@angular/cdk/scrolling';
|
|
11
|
+
import { CdkVirtualScrollViewport, ScrollingModule } from '@angular/cdk/scrolling';
|
|
12
|
+
import createDOMPurify from 'dompurify';
|
|
13
|
+
import { Marked } from 'marked';
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Wire models of the NewHeap assistant HTTP API.
|
|
17
|
+
*
|
|
18
|
+
* These interfaces are a literal translation of the assistant API contract. Keep the
|
|
19
|
+
* field names identical to the JSON the server returns (camelCase, ISO-8601 UTC dates,
|
|
20
|
+
* GUID strings for ids).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const NH_ASSISTANT_SSE_EVENT_TYPES = [
|
|
24
|
+
'turn.started',
|
|
25
|
+
'message.delta',
|
|
26
|
+
'tool.started',
|
|
27
|
+
'tool.completed',
|
|
28
|
+
'approval.required',
|
|
29
|
+
'turn.completed',
|
|
30
|
+
'error'
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Stable client-side failure codes. The client emits these as `error` events when the
|
|
34
|
+
* transport fails before the server could send its own `error` event.
|
|
35
|
+
*/
|
|
36
|
+
const NhAssistantClientErrorCodes = {
|
|
37
|
+
network: 'assistant-network',
|
|
38
|
+
unauthenticated: 'assistant-unauthenticated',
|
|
39
|
+
forbidden: 'assistant-forbidden',
|
|
40
|
+
notFound: 'assistant-not-found',
|
|
41
|
+
conversationBusy: 'assistant-conversation-busy',
|
|
42
|
+
validation: 'assistant-validation',
|
|
43
|
+
versionConflict: 'assistant-version-conflict',
|
|
44
|
+
invalidResponse: 'assistant-invalid-response',
|
|
45
|
+
streamInterrupted: 'assistant-stream-interrupted',
|
|
46
|
+
server: 'assistant-server'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* Wire models of the assistant preference and administration endpoints.
|
|
51
|
+
*
|
|
52
|
+
* Literal translation of the assistant API contract (preferences and `admin/*`). Keep the
|
|
53
|
+
* field names identical to the JSON the server returns.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const NH_ASSISTANT_CONFIG = new InjectionToken('NH_ASSISTANT_CONFIG');
|
|
57
|
+
const NH_ASSISTANT_ACCESS_POLICY = new InjectionToken('NH_ASSISTANT_ACCESS_POLICY');
|
|
58
|
+
/**
|
|
59
|
+
* Transport used by the assistant client. It is deliberately independent of the host's
|
|
60
|
+
* `HttpClient` and interceptors, because streaming responses need `fetch` and the access
|
|
61
|
+
* token comes from `NhAssistantConfig.getAccessToken`. Tests and the mock API from
|
|
62
|
+
* `@newheap/platform-ai-chat/testing` replace it.
|
|
63
|
+
*/
|
|
64
|
+
const NH_ASSISTANT_FETCH = new InjectionToken('NH_ASSISTANT_FETCH', {
|
|
65
|
+
providedIn: 'root',
|
|
66
|
+
factory: () => (input, init) => globalThis.fetch(input, init)
|
|
67
|
+
});
|
|
68
|
+
/** Default policy: every user who can reach the host may use the assistant. */
|
|
69
|
+
class NhAssistantAllowAllAccessPolicy {
|
|
70
|
+
canUse() {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
var en = {
|
|
76
|
+
"nh-assistant": {
|
|
77
|
+
launcher: {
|
|
78
|
+
open: "Open assistant",
|
|
79
|
+
close: "Close assistant",
|
|
80
|
+
"pending-approval": "An action is waiting for your approval"
|
|
81
|
+
},
|
|
82
|
+
panel: {
|
|
83
|
+
title: "Assistant",
|
|
84
|
+
"region-label": "Assistant panel",
|
|
85
|
+
close: "Close assistant",
|
|
86
|
+
"show-conversations": "Show conversations",
|
|
87
|
+
"hide-conversations": "Hide conversations",
|
|
88
|
+
"new-conversation": "New conversation",
|
|
89
|
+
loading: "Loading the assistant",
|
|
90
|
+
"unavailable-title": "The assistant is not available",
|
|
91
|
+
"unavailable-description": "The assistant is switched off or you do not have access to it.",
|
|
92
|
+
"empty-title": "How can I help?",
|
|
93
|
+
"empty-description": "Ask a question or describe a task. Actions that change data always ask for your approval first.",
|
|
94
|
+
"conversation-closed": "This conversation cannot continue. Start a new conversation.",
|
|
95
|
+
"dismiss-error": "Dismiss message",
|
|
96
|
+
usage: "{{inputTokens}} input tokens, {{outputTokens}} output tokens, {{toolCalls}} tool calls",
|
|
97
|
+
"open-preferences": "Your assistant preferences",
|
|
98
|
+
"open-admin": "Assistant administration"
|
|
99
|
+
},
|
|
100
|
+
"agent-picker": {
|
|
101
|
+
label: "Agent",
|
|
102
|
+
"can-mutate": "Can propose changes",
|
|
103
|
+
"read-only": "Read only"
|
|
104
|
+
},
|
|
105
|
+
"conversation-list": {
|
|
106
|
+
title: "Conversations",
|
|
107
|
+
loading: "Loading conversations",
|
|
108
|
+
empty: "No conversations yet.",
|
|
109
|
+
untitled: "Untitled conversation",
|
|
110
|
+
"delete": "Delete conversation",
|
|
111
|
+
count: "{{count}} of {{total}}",
|
|
112
|
+
status: {
|
|
113
|
+
idle: "Ready",
|
|
114
|
+
running: "Running",
|
|
115
|
+
"waiting-for-approval": "Waiting for approval",
|
|
116
|
+
failed: "Failed",
|
|
117
|
+
archived: "Archived"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
thread: {
|
|
121
|
+
label: "Conversation",
|
|
122
|
+
you: "You",
|
|
123
|
+
assistant: "Assistant",
|
|
124
|
+
tool: "Tool",
|
|
125
|
+
working: "The assistant is working",
|
|
126
|
+
loading: "Loading conversation"
|
|
127
|
+
},
|
|
128
|
+
composer: {
|
|
129
|
+
label: "Message",
|
|
130
|
+
placeholder: "Ask the assistant",
|
|
131
|
+
hint: "Enter sends, Shift+Enter adds a new line",
|
|
132
|
+
send: "Send message",
|
|
133
|
+
stop: "Stop the assistant",
|
|
134
|
+
characters: "{{count}} of {{max}} characters",
|
|
135
|
+
"too-long": "The message is too long."
|
|
136
|
+
},
|
|
137
|
+
"tool-call": {
|
|
138
|
+
label: "Tool call: {{name}}",
|
|
139
|
+
"show-details": "Show details",
|
|
140
|
+
"hide-details": "Hide details",
|
|
141
|
+
"arguments": "Input",
|
|
142
|
+
result: "Result",
|
|
143
|
+
"result-code": "Result",
|
|
144
|
+
status: {
|
|
145
|
+
running: "Running",
|
|
146
|
+
succeeded: "Completed",
|
|
147
|
+
failed: "Failed",
|
|
148
|
+
"awaiting-approval": "Awaiting approval",
|
|
149
|
+
rejected: "Rejected"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
approval: {
|
|
153
|
+
title: "Approval required",
|
|
154
|
+
targets: "Affects",
|
|
155
|
+
"arguments": "Proposed change",
|
|
156
|
+
"expires-in": "Expires in {{time}}",
|
|
157
|
+
expired: "This approval has expired.",
|
|
158
|
+
approve: "Approve",
|
|
159
|
+
reject: "Reject",
|
|
160
|
+
"approve-label": "Approve: {{summary}}",
|
|
161
|
+
"reject-label": "Reject: {{summary}}",
|
|
162
|
+
deciding: "Submitting your decision",
|
|
163
|
+
status: {
|
|
164
|
+
pending: "Pending",
|
|
165
|
+
approved: "Approved",
|
|
166
|
+
rejected: "Rejected",
|
|
167
|
+
expired: "Expired"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"result-codes": {
|
|
171
|
+
"api-bridge-validation": "The request was not valid.",
|
|
172
|
+
"api-bridge-unauthenticated": "You are not signed in.",
|
|
173
|
+
"api-bridge-forbidden": "You do not have permission for this action.",
|
|
174
|
+
"api-bridge-not-found": "The item was not found.",
|
|
175
|
+
"api-bridge-conflict": "The item was changed in the meantime.",
|
|
176
|
+
"api-bridge-upstream": "The service is not available.",
|
|
177
|
+
"api-bridge-timeout": "The request took too long."
|
|
178
|
+
},
|
|
179
|
+
errors: {
|
|
180
|
+
generic: "Something went wrong. Please try again.",
|
|
181
|
+
"assistant-network": "The assistant cannot be reached. Check your connection and try again.",
|
|
182
|
+
"assistant-unauthenticated": "Your session has ended. Sign in again to use the assistant.",
|
|
183
|
+
"assistant-forbidden": "You do not have access to the assistant.",
|
|
184
|
+
"assistant-not-found": "The item no longer exists. Reload and try again.",
|
|
185
|
+
"assistant-conversation-busy": "The assistant is still busy with this conversation. Try again in a moment.",
|
|
186
|
+
"assistant-invalid-response": "The assistant sent an unexpected response.",
|
|
187
|
+
"assistant-stream-interrupted": "The connection to the assistant was interrupted.",
|
|
188
|
+
"assistant-server": "The assistant could not complete the request.",
|
|
189
|
+
"assistant-validation": "Some values are not valid. Check the fields and try again.",
|
|
190
|
+
"assistant-version-conflict": "Someone else changed this in the meantime. Reload and apply your change again.",
|
|
191
|
+
"assistant-mcp-unreachable": "The MCP server cannot be reached.",
|
|
192
|
+
"assistant-mcp-unauthorized": "The MCP server rejected the credentials.",
|
|
193
|
+
"assistant-mcp-host-blocked": "This host is not allowed for MCP servers or for forwarding the user's token.",
|
|
194
|
+
"assistant-mcp-schema-changed": "The tool's input changed on the server. Review it and enable it again.",
|
|
195
|
+
"assistant-mcp-tool-disabled": "This tool is disabled.",
|
|
196
|
+
"assistant-instructions-too-long": "The text is too long. Shorten it to at most 20,000 characters.",
|
|
197
|
+
"assistant-context-not-found": "The application context was not found. Reload the page.",
|
|
198
|
+
"assistant-mcp-server-not-found": "The MCP server no longer exists. Reload the list.",
|
|
199
|
+
"assistant-mcp-tool-not-found": "The MCP tool no longer exists. Sync the server and try again.",
|
|
200
|
+
"assistant-agent-exists": "An agent with this id already exists. Choose another id.",
|
|
201
|
+
"assistant-mcp-server-exists": "An MCP server with this id already exists. Choose another id.",
|
|
202
|
+
"assistant-code-agent-not-deletable": "Agents from the application code cannot be deleted. Disable the agent instead.",
|
|
203
|
+
"assistant-agent-not-code": "Only agents from the application code can be reset.",
|
|
204
|
+
"assistant-actor-mismatch": "This action belongs to another user's session. Reload the page and try again.",
|
|
205
|
+
"assistant-agent-forbidden": "You do not have access to this agent. Choose another agent.",
|
|
206
|
+
"assistant-agent-not-found": "This agent is no longer available. Choose another agent.",
|
|
207
|
+
"assistant-approval-decision-invalid": "The decision could not be processed. Approve or reject the action again.",
|
|
208
|
+
"assistant-approval-expired": "The approval has expired, so nothing was changed. Ask the assistant again.",
|
|
209
|
+
"assistant-approval-invalid": "The approval no longer matches the proposed change, so nothing was changed. Ask the assistant again.",
|
|
210
|
+
"assistant-approval-not-found": "This approval no longer exists. Reload the conversation.",
|
|
211
|
+
"assistant-approval-not-pending": "This approval was already decided. Reload the conversation.",
|
|
212
|
+
"assistant-approval-rejected": "You rejected this action, so nothing was changed.",
|
|
213
|
+
"assistant-budget-exhausted": "You have reached today's assistant limit. You can use the assistant again tomorrow.",
|
|
214
|
+
"assistant-context-unavailable": "Your session could not be identified. Sign in again and retry.",
|
|
215
|
+
"assistant-conversation-not-found": "This conversation no longer exists. Start a new conversation.",
|
|
216
|
+
"assistant-disabled": "The assistant is switched off.",
|
|
217
|
+
"assistant-message-duplicate": "This message was already sent. Wait for the answer.",
|
|
218
|
+
"assistant-message-invalid": "The message is empty or not valid. Write a question and send it again.",
|
|
219
|
+
"assistant-message-too-long": "The message is too long. Shorten it and send it again.",
|
|
220
|
+
"assistant-model-unavailable": "The language model is temporarily unavailable. Try again in a moment.",
|
|
221
|
+
"assistant-proposal-hash-mismatch": "The proposed change was modified in the meantime, so nothing was changed. Reload the conversation and decide again.",
|
|
222
|
+
"assistant-title-invalid": "The conversation title is not valid. Use a shorter title.",
|
|
223
|
+
"assistant-tool-call-limit-reached": "The assistant reached the maximum number of steps for one answer. Ask a narrower question.",
|
|
224
|
+
"assistant-tools-disabled": "No further actions were run in this answer because an action was rejected.",
|
|
225
|
+
"assistant-turn-failed": "The assistant could not finish the answer. Try again.",
|
|
226
|
+
"assistant-turn-timeout": "The answer took too long and was stopped. Try again or ask a narrower question."
|
|
227
|
+
},
|
|
228
|
+
preferences: {
|
|
229
|
+
title: "Your preferences",
|
|
230
|
+
description: "Choose how the assistant answers you. Preferences change the style only; they never change what the assistant may do or which actions need your approval.",
|
|
231
|
+
"style-label": "Style",
|
|
232
|
+
style: {
|
|
233
|
+
"default": "Standard",
|
|
234
|
+
"default-description": "Balanced answers.",
|
|
235
|
+
direct: "Straight to the point",
|
|
236
|
+
"direct-description": "Short, factual answers without small talk.",
|
|
237
|
+
personal: "Personal",
|
|
238
|
+
"personal-description": "A friendly, conversational tone.",
|
|
239
|
+
detailed: "Detailed",
|
|
240
|
+
"detailed-description": "Thorough answers with background and explanation."
|
|
241
|
+
},
|
|
242
|
+
"address-form-label": "Form of address",
|
|
243
|
+
"address-form": {
|
|
244
|
+
informal: "Informal",
|
|
245
|
+
formal: "Formal"
|
|
246
|
+
},
|
|
247
|
+
"response-length-label": "Answer length",
|
|
248
|
+
"response-length": {
|
|
249
|
+
short: "Short",
|
|
250
|
+
normal: "Normal",
|
|
251
|
+
long: "Long"
|
|
252
|
+
},
|
|
253
|
+
"custom-instructions-label": "Your own instructions",
|
|
254
|
+
"custom-instructions-hint": "Optional. For example: answer with bullet points. Your instructions never override the assistant's rules.",
|
|
255
|
+
characters: "{{count}} of {{max}} characters",
|
|
256
|
+
"too-long": "Your instructions are too long.",
|
|
257
|
+
save: "Save preferences",
|
|
258
|
+
saving: "Saving your preferences",
|
|
259
|
+
saved: "Your preferences are saved.",
|
|
260
|
+
back: "Back to the conversation",
|
|
261
|
+
loading: "Loading your preferences",
|
|
262
|
+
"load-failed": "Your preferences could not be loaded.",
|
|
263
|
+
retry: "Try again"
|
|
264
|
+
},
|
|
265
|
+
admin: {
|
|
266
|
+
title: "Assistant administration",
|
|
267
|
+
description: "Manage the application context, the agents and the MCP servers of the assistant. Changes apply from the next turn.",
|
|
268
|
+
"tabs-label": "Administration sections",
|
|
269
|
+
tabs: {
|
|
270
|
+
context: "Context",
|
|
271
|
+
agents: "Agents",
|
|
272
|
+
"mcp-servers": "MCP servers"
|
|
273
|
+
},
|
|
274
|
+
loading: "Loading",
|
|
275
|
+
"load-failed": "The data could not be loaded.",
|
|
276
|
+
retry: "Try again",
|
|
277
|
+
"forbidden-title": "No access",
|
|
278
|
+
"forbidden-description": "You need the assistant administration permission to open this page.",
|
|
279
|
+
saved: "Saved.",
|
|
280
|
+
cancel: "Cancel",
|
|
281
|
+
save: "Save",
|
|
282
|
+
saving: "Saving",
|
|
283
|
+
edit: "Edit",
|
|
284
|
+
"delete": "Delete",
|
|
285
|
+
"confirm-delete": "Confirm delete",
|
|
286
|
+
keep: "Keep",
|
|
287
|
+
enabled: "Enabled",
|
|
288
|
+
disabled: "Disabled",
|
|
289
|
+
enable: "Enable",
|
|
290
|
+
disable: "Disable",
|
|
291
|
+
characters: "{{count}} of {{max}} characters",
|
|
292
|
+
"too-long": "The text is too long.",
|
|
293
|
+
required: "This field is required.",
|
|
294
|
+
"invalid-id": "Use lowercase letters, digits and single dashes.",
|
|
295
|
+
context: {
|
|
296
|
+
title: "Application context",
|
|
297
|
+
description: "Background about the application that every agent receives before its own instructions. It cannot override the assistant's fixed rules. Do not include personal data or secrets.",
|
|
298
|
+
label: "Context text",
|
|
299
|
+
version: "Version {{version}}",
|
|
300
|
+
updated: "Updated {{date}} by {{user}}",
|
|
301
|
+
"updated-unknown": "Updated {{date}}",
|
|
302
|
+
hash: "Hash",
|
|
303
|
+
history: "Version history",
|
|
304
|
+
"history-empty": "No earlier versions.",
|
|
305
|
+
conflict: "Someone saved a newer version. Your text is kept; load the latest version, compare and save again.",
|
|
306
|
+
reload: "Load latest version"
|
|
307
|
+
},
|
|
308
|
+
agents: {
|
|
309
|
+
title: "Agents",
|
|
310
|
+
description: "Code agents come from the application; you can override, disable or reset them. Agents you create here can be deleted.",
|
|
311
|
+
"new": "New agent",
|
|
312
|
+
empty: "No agents yet.",
|
|
313
|
+
"source-code": "Code",
|
|
314
|
+
"source-admin": "Created here",
|
|
315
|
+
overridden: "Overridden",
|
|
316
|
+
reset: "Reset to code",
|
|
317
|
+
"confirm-reset": "Confirm reset",
|
|
318
|
+
version: "Version {{version}}",
|
|
319
|
+
"tool-count": "{{count}} tool selectors",
|
|
320
|
+
"server-count": "{{count}} MCP servers",
|
|
321
|
+
autonomy: {
|
|
322
|
+
observe: "Observe",
|
|
323
|
+
explain: "Explain",
|
|
324
|
+
propose: "Propose",
|
|
325
|
+
simulate: "Simulate",
|
|
326
|
+
execute: "Execute"
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
"agent-editor": {
|
|
330
|
+
"new-title": "New agent",
|
|
331
|
+
"edit-title": "Edit {{name}}",
|
|
332
|
+
id: "Id",
|
|
333
|
+
"id-hint": "Lowercase letters, digits and dashes. The id cannot change later.",
|
|
334
|
+
"display-name": "Name",
|
|
335
|
+
description: "Description",
|
|
336
|
+
instructions: "Instructions",
|
|
337
|
+
"instructions-hint": "What the agent does and how it answers.",
|
|
338
|
+
autonomy: "Autonomy",
|
|
339
|
+
"autonomy-hint": "Only Execute allows changes, and every change still needs approval.",
|
|
340
|
+
"required-policy": "Required policy",
|
|
341
|
+
"required-policy-hint": "Optional. Only users who pass this policy see the agent.",
|
|
342
|
+
enabled: "Agent is enabled",
|
|
343
|
+
"tool-selectors": "Tool selectors",
|
|
344
|
+
"tool-selectors-hint": "Choose tools from the catalog or add a pattern such as sample-api.project.*. MCP tools come from the assigned servers.",
|
|
345
|
+
"add-selector": "Add pattern",
|
|
346
|
+
"selector-placeholder": "orders.*",
|
|
347
|
+
"remove-selector": "Remove {{selector}}",
|
|
348
|
+
"no-selectors": "No tool selectors. The agent can only answer from its instructions and assigned MCP servers.",
|
|
349
|
+
catalog: "Tool catalog",
|
|
350
|
+
"catalog-filter": "Filter the tool catalog",
|
|
351
|
+
"catalog-empty": "No tools match.",
|
|
352
|
+
matches: "{{count}} catalog tools match the selectors.",
|
|
353
|
+
"mcp-servers": "MCP servers",
|
|
354
|
+
"mcp-servers-hint": "Enabled tools of these servers become available to this agent.",
|
|
355
|
+
"mcp-servers-empty": "No MCP servers are configured."
|
|
356
|
+
},
|
|
357
|
+
effect: {
|
|
358
|
+
"read-only": "Read only",
|
|
359
|
+
"idempotent-mutation": "Idempotent change",
|
|
360
|
+
mutation: "Change",
|
|
361
|
+
destructive: "Destructive"
|
|
362
|
+
},
|
|
363
|
+
source: {
|
|
364
|
+
local: "Local",
|
|
365
|
+
bridge: "API bridge",
|
|
366
|
+
mcp: "MCP"
|
|
367
|
+
},
|
|
368
|
+
mcp: {
|
|
369
|
+
title: "MCP servers",
|
|
370
|
+
description: "Connect remote MCP servers. Their tools start disabled and are treated as changes that need approval until you mark them read-only.",
|
|
371
|
+
"new": "New server",
|
|
372
|
+
empty: "No MCP servers yet.",
|
|
373
|
+
test: "Test connection",
|
|
374
|
+
testing: "Testing the connection",
|
|
375
|
+
"test-ok": "The connection works: {{count}} tools.",
|
|
376
|
+
sync: "Sync tools",
|
|
377
|
+
syncing: "Syncing tools",
|
|
378
|
+
"sync-done": "Tools synced: {{count}}.",
|
|
379
|
+
tools: "Tools",
|
|
380
|
+
"last-sync": "Last sync {{date}}",
|
|
381
|
+
"never-synced": "Never synced",
|
|
382
|
+
"sync-ok": "Sync OK",
|
|
383
|
+
"sync-failed": "Sync failed",
|
|
384
|
+
assigned: "Assigned to: {{agents}}",
|
|
385
|
+
unassigned: "Not assigned to an agent",
|
|
386
|
+
"secret-set": "Secret set",
|
|
387
|
+
"no-secret": "No secret",
|
|
388
|
+
auth: {
|
|
389
|
+
none: "No authentication",
|
|
390
|
+
bearer: "Bearer token",
|
|
391
|
+
"api-key": "API key",
|
|
392
|
+
"forward-user-token": "Forward the user's token"
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
"server-editor": {
|
|
396
|
+
"new-title": "New MCP server",
|
|
397
|
+
"edit-title": "Edit {{name}}",
|
|
398
|
+
id: "Id",
|
|
399
|
+
"id-hint": "Lowercase letters, digits and dashes, at most 40 characters. The id cannot change later.",
|
|
400
|
+
"display-name": "Name",
|
|
401
|
+
url: "URL",
|
|
402
|
+
"url-hint": "https is required; http is only allowed for localhost during development.",
|
|
403
|
+
"url-invalid": "Enter an https URL.",
|
|
404
|
+
"auth-mode": "Authentication",
|
|
405
|
+
"header-name": "Header name",
|
|
406
|
+
"header-name-hint": "Default X-Api-Key.",
|
|
407
|
+
secret: "Secret",
|
|
408
|
+
"secret-hint": "Stored encrypted and never shown again.",
|
|
409
|
+
"secret-set": "A secret is set.",
|
|
410
|
+
"secret-none": "No secret is set.",
|
|
411
|
+
"secret-replace": "Replace",
|
|
412
|
+
"secret-clear": "Clear",
|
|
413
|
+
"secret-will-clear": "The secret will be cleared when you save.",
|
|
414
|
+
"secret-undo": "Keep the current secret",
|
|
415
|
+
"secret-new": "New secret",
|
|
416
|
+
"forward-warning": "The signed-in user's token is sent to this server. The server's host must be on the allow-list of the application.",
|
|
417
|
+
"required-policy": "Required policy",
|
|
418
|
+
"required-policy-hint": "Optional. The server's tools are only available to users who pass this policy.",
|
|
419
|
+
enabled: "Server is enabled"
|
|
420
|
+
},
|
|
421
|
+
tools: {
|
|
422
|
+
title: "Tools of {{name}}",
|
|
423
|
+
description: "Tools stay disabled and are treated as changes that need approval until you enable them and choose the effect. Annotations from the server are hints only.",
|
|
424
|
+
empty: "No tools yet. Sync the server to load its tools.",
|
|
425
|
+
enabled: "Tool is enabled",
|
|
426
|
+
effect: "Effect",
|
|
427
|
+
"description-override": "Description for the assistant",
|
|
428
|
+
"description-override-hint": "Optional. Replaces the server's description.",
|
|
429
|
+
"remote-description": "Server description",
|
|
430
|
+
"hint-read-only": "Server hint: read only",
|
|
431
|
+
"hint-mutation": "Server hint: changes data",
|
|
432
|
+
status: {
|
|
433
|
+
available: "Available",
|
|
434
|
+
"schema-changed": "Input changed",
|
|
435
|
+
missing: "Missing on server"
|
|
436
|
+
},
|
|
437
|
+
"schema-changed-warning": "The tool's input changed on the server, so the tool was disabled. Review the tool before you enable it again.",
|
|
438
|
+
"missing-warning": "The server no longer lists this tool.",
|
|
439
|
+
save: "Save tool",
|
|
440
|
+
back: "Back to the servers"
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
"page-context": {
|
|
444
|
+
included: "Sent with your message: {{page}}",
|
|
445
|
+
excluded: "Not sent with this message: {{page}}",
|
|
446
|
+
exclude: "Do not send {{page}} with this message",
|
|
447
|
+
include: "Send {{page}} with this message again"
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
var nl = {
|
|
453
|
+
"nh-assistant": {
|
|
454
|
+
launcher: {
|
|
455
|
+
open: "Assistent openen",
|
|
456
|
+
close: "Assistent sluiten",
|
|
457
|
+
"pending-approval": "Er wacht een actie op je goedkeuring"
|
|
458
|
+
},
|
|
459
|
+
panel: {
|
|
460
|
+
title: "Assistent",
|
|
461
|
+
"region-label": "Assistentpaneel",
|
|
462
|
+
close: "Assistent sluiten",
|
|
463
|
+
"show-conversations": "Gesprekken tonen",
|
|
464
|
+
"hide-conversations": "Gesprekken verbergen",
|
|
465
|
+
"new-conversation": "Nieuw gesprek",
|
|
466
|
+
loading: "Assistent laden",
|
|
467
|
+
"unavailable-title": "De assistent is niet beschikbaar",
|
|
468
|
+
"unavailable-description": "De assistent staat uit of je hebt er geen toegang toe.",
|
|
469
|
+
"empty-title": "Waarmee kan ik helpen?",
|
|
470
|
+
"empty-description": "Stel een vraag of beschrijf een taak. Acties die gegevens wijzigen vragen altijd eerst om je goedkeuring.",
|
|
471
|
+
"conversation-closed": "Dit gesprek kan niet verder. Begin een nieuw gesprek.",
|
|
472
|
+
"dismiss-error": "Melding sluiten",
|
|
473
|
+
usage: "{{inputTokens}} invoertokens, {{outputTokens}} uitvoertokens, {{toolCalls}} toolaanroepen",
|
|
474
|
+
"open-preferences": "Je assistentvoorkeuren",
|
|
475
|
+
"open-admin": "Beheer van de assistent"
|
|
476
|
+
},
|
|
477
|
+
"agent-picker": {
|
|
478
|
+
label: "Agent",
|
|
479
|
+
"can-mutate": "Kan wijzigingen voorstellen",
|
|
480
|
+
"read-only": "Alleen lezen"
|
|
481
|
+
},
|
|
482
|
+
"conversation-list": {
|
|
483
|
+
title: "Gesprekken",
|
|
484
|
+
loading: "Gesprekken laden",
|
|
485
|
+
empty: "Nog geen gesprekken.",
|
|
486
|
+
untitled: "Gesprek zonder titel",
|
|
487
|
+
"delete": "Gesprek verwijderen",
|
|
488
|
+
count: "{{count}} van {{total}}",
|
|
489
|
+
status: {
|
|
490
|
+
idle: "Gereed",
|
|
491
|
+
running: "Bezig",
|
|
492
|
+
"waiting-for-approval": "Wacht op goedkeuring",
|
|
493
|
+
failed: "Mislukt",
|
|
494
|
+
archived: "Gearchiveerd"
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
thread: {
|
|
498
|
+
label: "Gesprek",
|
|
499
|
+
you: "Jij",
|
|
500
|
+
assistant: "Assistent",
|
|
501
|
+
tool: "Tool",
|
|
502
|
+
working: "De assistent is bezig",
|
|
503
|
+
loading: "Gesprek laden"
|
|
504
|
+
},
|
|
505
|
+
composer: {
|
|
506
|
+
label: "Bericht",
|
|
507
|
+
placeholder: "Vraag het de assistent",
|
|
508
|
+
hint: "Enter verstuurt, Shift+Enter voegt een regel toe",
|
|
509
|
+
send: "Bericht versturen",
|
|
510
|
+
stop: "Assistent stoppen",
|
|
511
|
+
characters: "{{count}} van {{max}} tekens",
|
|
512
|
+
"too-long": "Het bericht is te lang."
|
|
513
|
+
},
|
|
514
|
+
"tool-call": {
|
|
515
|
+
label: "Toolaanroep: {{name}}",
|
|
516
|
+
"show-details": "Details tonen",
|
|
517
|
+
"hide-details": "Details verbergen",
|
|
518
|
+
"arguments": "Invoer",
|
|
519
|
+
result: "Resultaat",
|
|
520
|
+
"result-code": "Resultaat",
|
|
521
|
+
status: {
|
|
522
|
+
running: "Bezig",
|
|
523
|
+
succeeded: "Voltooid",
|
|
524
|
+
failed: "Mislukt",
|
|
525
|
+
"awaiting-approval": "Wacht op goedkeuring",
|
|
526
|
+
rejected: "Afgewezen"
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
approval: {
|
|
530
|
+
title: "Goedkeuring nodig",
|
|
531
|
+
targets: "Betreft",
|
|
532
|
+
"arguments": "Voorgestelde wijziging",
|
|
533
|
+
"expires-in": "Verloopt over {{time}}",
|
|
534
|
+
expired: "Deze goedkeuring is verlopen.",
|
|
535
|
+
approve: "Goedkeuren",
|
|
536
|
+
reject: "Afwijzen",
|
|
537
|
+
"approve-label": "Goedkeuren: {{summary}}",
|
|
538
|
+
"reject-label": "Afwijzen: {{summary}}",
|
|
539
|
+
deciding: "Je besluit wordt verstuurd",
|
|
540
|
+
status: {
|
|
541
|
+
pending: "Open",
|
|
542
|
+
approved: "Goedgekeurd",
|
|
543
|
+
rejected: "Afgewezen",
|
|
544
|
+
expired: "Verlopen"
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
"result-codes": {
|
|
548
|
+
"api-bridge-validation": "Het verzoek was niet geldig.",
|
|
549
|
+
"api-bridge-unauthenticated": "Je bent niet ingelogd.",
|
|
550
|
+
"api-bridge-forbidden": "Je hebt geen rechten voor deze actie.",
|
|
551
|
+
"api-bridge-not-found": "Het item is niet gevonden.",
|
|
552
|
+
"api-bridge-conflict": "Het item is intussen gewijzigd.",
|
|
553
|
+
"api-bridge-upstream": "De dienst is niet beschikbaar.",
|
|
554
|
+
"api-bridge-timeout": "Het verzoek duurde te lang."
|
|
555
|
+
},
|
|
556
|
+
errors: {
|
|
557
|
+
generic: "Er ging iets mis. Probeer het opnieuw.",
|
|
558
|
+
"assistant-network": "De assistent is niet bereikbaar. Controleer je verbinding en probeer het opnieuw.",
|
|
559
|
+
"assistant-unauthenticated": "Je sessie is verlopen. Log opnieuw in om de assistent te gebruiken.",
|
|
560
|
+
"assistant-forbidden": "Je hebt geen toegang tot de assistent.",
|
|
561
|
+
"assistant-not-found": "Het item bestaat niet meer. Laad opnieuw en probeer het nog eens.",
|
|
562
|
+
"assistant-conversation-busy": "De assistent is nog bezig met dit gesprek. Probeer het zo opnieuw.",
|
|
563
|
+
"assistant-invalid-response": "De assistent gaf een onverwacht antwoord.",
|
|
564
|
+
"assistant-stream-interrupted": "De verbinding met de assistent is onderbroken.",
|
|
565
|
+
"assistant-server": "De assistent kon het verzoek niet afronden.",
|
|
566
|
+
"assistant-validation": "Sommige waarden zijn niet geldig. Controleer de velden en probeer het opnieuw.",
|
|
567
|
+
"assistant-version-conflict": "Iemand anders heeft dit intussen gewijzigd. Laad opnieuw en voer je wijziging nogmaals uit.",
|
|
568
|
+
"assistant-mcp-unreachable": "De MCP-server is niet bereikbaar.",
|
|
569
|
+
"assistant-mcp-unauthorized": "De MCP-server weigert de inloggegevens.",
|
|
570
|
+
"assistant-mcp-host-blocked": "Deze host is niet toegestaan voor MCP-servers of voor het doorsturen van het gebruikerstoken.",
|
|
571
|
+
"assistant-mcp-schema-changed": "De invoer van de tool is op de server gewijzigd. Controleer hem en zet hem opnieuw aan.",
|
|
572
|
+
"assistant-mcp-tool-disabled": "Deze tool staat uit.",
|
|
573
|
+
"assistant-instructions-too-long": "De tekst is te lang. Maak hem korter dan 20.000 tekens.",
|
|
574
|
+
"assistant-context-not-found": "De applicatiecontext is niet gevonden. Laad de pagina opnieuw.",
|
|
575
|
+
"assistant-mcp-server-not-found": "De MCP-server bestaat niet meer. Laad de lijst opnieuw.",
|
|
576
|
+
"assistant-mcp-tool-not-found": "De MCP-tool bestaat niet meer. Synchroniseer de server en probeer het opnieuw.",
|
|
577
|
+
"assistant-agent-exists": "Er bestaat al een agent met dit id. Kies een ander id.",
|
|
578
|
+
"assistant-mcp-server-exists": "Er bestaat al een MCP-server met dit id. Kies een ander id.",
|
|
579
|
+
"assistant-code-agent-not-deletable": "Agents uit de applicatiecode kunnen niet worden verwijderd. Zet de agent uit.",
|
|
580
|
+
"assistant-agent-not-code": "Alleen agents uit de applicatiecode kunnen worden teruggezet.",
|
|
581
|
+
"assistant-actor-mismatch": "Deze actie hoort bij de sessie van een andere gebruiker. Laad de pagina opnieuw en probeer het nog eens.",
|
|
582
|
+
"assistant-agent-forbidden": "Je hebt geen toegang tot deze agent. Kies een andere agent.",
|
|
583
|
+
"assistant-agent-not-found": "Deze agent is niet meer beschikbaar. Kies een andere agent.",
|
|
584
|
+
"assistant-approval-decision-invalid": "Het besluit kon niet worden verwerkt. Keur de actie opnieuw goed of wijs haar af.",
|
|
585
|
+
"assistant-approval-expired": "De goedkeuring is verlopen, er is niets gewijzigd. Vraag het de assistent opnieuw.",
|
|
586
|
+
"assistant-approval-invalid": "De goedkeuring past niet meer bij de voorgestelde wijziging, er is niets gewijzigd. Vraag het de assistent opnieuw.",
|
|
587
|
+
"assistant-approval-not-found": "Deze goedkeuring bestaat niet meer. Laad het gesprek opnieuw.",
|
|
588
|
+
"assistant-approval-not-pending": "Over deze goedkeuring is al besloten. Laad het gesprek opnieuw.",
|
|
589
|
+
"assistant-approval-rejected": "Je hebt deze actie afgewezen, er is niets gewijzigd.",
|
|
590
|
+
"assistant-budget-exhausted": "Je hebt de daglimiet van de assistent bereikt. Morgen kun je de assistent weer gebruiken.",
|
|
591
|
+
"assistant-context-unavailable": "Je sessie kon niet worden vastgesteld. Log opnieuw in en probeer het nog eens.",
|
|
592
|
+
"assistant-conversation-not-found": "Dit gesprek bestaat niet meer. Begin een nieuw gesprek.",
|
|
593
|
+
"assistant-disabled": "De assistent staat uit.",
|
|
594
|
+
"assistant-message-duplicate": "Dit bericht is al verstuurd. Wacht op het antwoord.",
|
|
595
|
+
"assistant-message-invalid": "Het bericht is leeg of ongeldig. Schrijf een vraag en verstuur hem opnieuw.",
|
|
596
|
+
"assistant-message-too-long": "Het bericht is te lang. Maak het korter en verstuur het opnieuw.",
|
|
597
|
+
"assistant-model-unavailable": "Het taalmodel is tijdelijk niet beschikbaar. Probeer het zo opnieuw.",
|
|
598
|
+
"assistant-proposal-hash-mismatch": "De voorgestelde wijziging is intussen veranderd, er is niets gewijzigd. Laad het gesprek opnieuw en besluit opnieuw.",
|
|
599
|
+
"assistant-title-invalid": "De titel van het gesprek is ongeldig. Gebruik een kortere titel.",
|
|
600
|
+
"assistant-tool-call-limit-reached": "De assistent heeft het maximale aantal stappen voor één antwoord bereikt. Stel een gerichtere vraag.",
|
|
601
|
+
"assistant-tools-disabled": "In dit antwoord zijn geen verdere acties uitgevoerd omdat een actie is afgewezen.",
|
|
602
|
+
"assistant-turn-failed": "De assistent kon het antwoord niet afmaken. Probeer het opnieuw.",
|
|
603
|
+
"assistant-turn-timeout": "Het antwoord duurde te lang en is gestopt. Probeer het opnieuw of stel een gerichtere vraag."
|
|
604
|
+
},
|
|
605
|
+
preferences: {
|
|
606
|
+
title: "Je voorkeuren",
|
|
607
|
+
description: "Kies hoe de assistent je antwoordt. Voorkeuren veranderen alleen de stijl; ze veranderen nooit wat de assistent mag of welke acties je goedkeuring nodig hebben.",
|
|
608
|
+
"style-label": "Stijl",
|
|
609
|
+
style: {
|
|
610
|
+
"default": "Standaard",
|
|
611
|
+
"default-description": "Evenwichtige antwoorden.",
|
|
612
|
+
direct: "Recht toe recht aan",
|
|
613
|
+
"direct-description": "Korte, zakelijke antwoorden zonder omhaal.",
|
|
614
|
+
personal: "Persoonlijk",
|
|
615
|
+
"personal-description": "Een vriendelijke, gemoedelijke toon.",
|
|
616
|
+
detailed: "Uitgebreid",
|
|
617
|
+
"detailed-description": "Grondige antwoorden met achtergrond en uitleg."
|
|
618
|
+
},
|
|
619
|
+
"address-form-label": "Aanspreekvorm",
|
|
620
|
+
"address-form": {
|
|
621
|
+
informal: "Je",
|
|
622
|
+
formal: "U"
|
|
623
|
+
},
|
|
624
|
+
"response-length-label": "Lengte van antwoorden",
|
|
625
|
+
"response-length": {
|
|
626
|
+
short: "Kort",
|
|
627
|
+
normal: "Normaal",
|
|
628
|
+
long: "Lang"
|
|
629
|
+
},
|
|
630
|
+
"custom-instructions-label": "Eigen instructies",
|
|
631
|
+
"custom-instructions-hint": "Optioneel. Bijvoorbeeld: antwoord met opsommingstekens. Je instructies gaan nooit boven de regels van de assistent.",
|
|
632
|
+
characters: "{{count}} van {{max}} tekens",
|
|
633
|
+
"too-long": "Je instructies zijn te lang.",
|
|
634
|
+
save: "Voorkeuren opslaan",
|
|
635
|
+
saving: "Je voorkeuren worden opgeslagen",
|
|
636
|
+
saved: "Je voorkeuren zijn opgeslagen.",
|
|
637
|
+
back: "Terug naar het gesprek",
|
|
638
|
+
loading: "Je voorkeuren laden",
|
|
639
|
+
"load-failed": "Je voorkeuren konden niet worden geladen.",
|
|
640
|
+
retry: "Opnieuw proberen"
|
|
641
|
+
},
|
|
642
|
+
admin: {
|
|
643
|
+
title: "Beheer van de assistent",
|
|
644
|
+
description: "Beheer de applicatiecontext, de agents en de MCP-servers van de assistent. Wijzigingen gelden vanaf de volgende beurt.",
|
|
645
|
+
"tabs-label": "Beheeronderdelen",
|
|
646
|
+
tabs: {
|
|
647
|
+
context: "Context",
|
|
648
|
+
agents: "Agents",
|
|
649
|
+
"mcp-servers": "MCP-servers"
|
|
650
|
+
},
|
|
651
|
+
loading: "Laden",
|
|
652
|
+
"load-failed": "De gegevens konden niet worden geladen.",
|
|
653
|
+
retry: "Opnieuw proberen",
|
|
654
|
+
"forbidden-title": "Geen toegang",
|
|
655
|
+
"forbidden-description": "Je hebt de beheerpermissie van de assistent nodig om deze pagina te openen.",
|
|
656
|
+
saved: "Opgeslagen.",
|
|
657
|
+
cancel: "Annuleren",
|
|
658
|
+
save: "Opslaan",
|
|
659
|
+
saving: "Opslaan",
|
|
660
|
+
edit: "Bewerken",
|
|
661
|
+
"delete": "Verwijderen",
|
|
662
|
+
"confirm-delete": "Verwijderen bevestigen",
|
|
663
|
+
keep: "Behouden",
|
|
664
|
+
enabled: "Aan",
|
|
665
|
+
disabled: "Uit",
|
|
666
|
+
enable: "Aanzetten",
|
|
667
|
+
disable: "Uitzetten",
|
|
668
|
+
characters: "{{count}} van {{max}} tekens",
|
|
669
|
+
"too-long": "De tekst is te lang.",
|
|
670
|
+
required: "Dit veld is verplicht.",
|
|
671
|
+
"invalid-id": "Gebruik kleine letters, cijfers en enkele streepjes.",
|
|
672
|
+
context: {
|
|
673
|
+
title: "Applicatiecontext",
|
|
674
|
+
description: "Achtergrond over de applicatie die elke agent vóór zijn eigen instructies krijgt. De context gaat nooit boven de vaste regels van de assistent. Neem geen persoonsgegevens of geheimen op.",
|
|
675
|
+
label: "Contexttekst",
|
|
676
|
+
version: "Versie {{version}}",
|
|
677
|
+
updated: "Gewijzigd {{date}} door {{user}}",
|
|
678
|
+
"updated-unknown": "Gewijzigd {{date}}",
|
|
679
|
+
hash: "Hash",
|
|
680
|
+
history: "Versiegeschiedenis",
|
|
681
|
+
"history-empty": "Geen eerdere versies.",
|
|
682
|
+
conflict: "Iemand heeft een nieuwere versie opgeslagen. Je tekst blijft staan; laad de nieuwste versie, vergelijk en sla opnieuw op.",
|
|
683
|
+
reload: "Nieuwste versie laden"
|
|
684
|
+
},
|
|
685
|
+
agents: {
|
|
686
|
+
title: "Agents",
|
|
687
|
+
description: "Code-agents komen uit de applicatie; je kunt ze overschrijven, uitzetten of terugzetten. Agents die je hier aanmaakt, kun je verwijderen.",
|
|
688
|
+
"new": "Nieuwe agent",
|
|
689
|
+
empty: "Nog geen agents.",
|
|
690
|
+
"source-code": "Code",
|
|
691
|
+
"source-admin": "Hier aangemaakt",
|
|
692
|
+
overridden: "Overschreven",
|
|
693
|
+
reset: "Terugzetten naar code",
|
|
694
|
+
"confirm-reset": "Terugzetten bevestigen",
|
|
695
|
+
version: "Versie {{version}}",
|
|
696
|
+
"tool-count": "{{count}} toolselectors",
|
|
697
|
+
"server-count": "{{count}} MCP-servers",
|
|
698
|
+
autonomy: {
|
|
699
|
+
observe: "Observeren",
|
|
700
|
+
explain: "Uitleggen",
|
|
701
|
+
propose: "Voorstellen",
|
|
702
|
+
simulate: "Simuleren",
|
|
703
|
+
execute: "Uitvoeren"
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
"agent-editor": {
|
|
707
|
+
"new-title": "Nieuwe agent",
|
|
708
|
+
"edit-title": "{{name}} bewerken",
|
|
709
|
+
id: "Id",
|
|
710
|
+
"id-hint": "Kleine letters, cijfers en streepjes. Het id kan later niet meer veranderen.",
|
|
711
|
+
"display-name": "Naam",
|
|
712
|
+
description: "Omschrijving",
|
|
713
|
+
instructions: "Instructies",
|
|
714
|
+
"instructions-hint": "Wat de agent doet en hoe hij antwoordt.",
|
|
715
|
+
autonomy: "Autonomie",
|
|
716
|
+
"autonomy-hint": "Alleen Uitvoeren staat wijzigingen toe, en elke wijziging vraagt nog steeds om goedkeuring.",
|
|
717
|
+
"required-policy": "Vereiste policy",
|
|
718
|
+
"required-policy-hint": "Optioneel. Alleen gebruikers die aan deze policy voldoen zien de agent.",
|
|
719
|
+
enabled: "Agent staat aan",
|
|
720
|
+
"tool-selectors": "Toolselectors",
|
|
721
|
+
"tool-selectors-hint": "Kies tools uit de catalogus of voeg een patroon toe, zoals sample-api.project.*. MCP-tools komen van de toegewezen servers.",
|
|
722
|
+
"add-selector": "Patroon toevoegen",
|
|
723
|
+
"selector-placeholder": "orders.*",
|
|
724
|
+
"remove-selector": "{{selector}} verwijderen",
|
|
725
|
+
"no-selectors": "Geen toolselectors. De agent kan alleen antwoorden vanuit zijn instructies en toegewezen MCP-servers.",
|
|
726
|
+
catalog: "Toolcatalogus",
|
|
727
|
+
"catalog-filter": "Toolcatalogus filteren",
|
|
728
|
+
"catalog-empty": "Geen tools gevonden.",
|
|
729
|
+
matches: "{{count}} tools uit de catalogus passen bij de selectors.",
|
|
730
|
+
"mcp-servers": "MCP-servers",
|
|
731
|
+
"mcp-servers-hint": "Ingeschakelde tools van deze servers zijn beschikbaar voor deze agent.",
|
|
732
|
+
"mcp-servers-empty": "Er zijn geen MCP-servers ingesteld."
|
|
733
|
+
},
|
|
734
|
+
effect: {
|
|
735
|
+
"read-only": "Alleen lezen",
|
|
736
|
+
"idempotent-mutation": "Idempotente wijziging",
|
|
737
|
+
mutation: "Wijziging",
|
|
738
|
+
destructive: "Destructief"
|
|
739
|
+
},
|
|
740
|
+
source: {
|
|
741
|
+
local: "Lokaal",
|
|
742
|
+
bridge: "API-bridge",
|
|
743
|
+
mcp: "MCP"
|
|
744
|
+
},
|
|
745
|
+
mcp: {
|
|
746
|
+
title: "MCP-servers",
|
|
747
|
+
description: "Koppel externe MCP-servers. Hun tools staan standaard uit en gelden als wijziging met goedkeuring tot je ze als alleen-lezen markeert.",
|
|
748
|
+
"new": "Nieuwe server",
|
|
749
|
+
empty: "Nog geen MCP-servers.",
|
|
750
|
+
test: "Verbinding testen",
|
|
751
|
+
testing: "Verbinding testen",
|
|
752
|
+
"test-ok": "De verbinding werkt: {{count}} tools.",
|
|
753
|
+
sync: "Tools synchroniseren",
|
|
754
|
+
syncing: "Tools synchroniseren",
|
|
755
|
+
"sync-done": "Tools gesynchroniseerd: {{count}}.",
|
|
756
|
+
tools: "Tools",
|
|
757
|
+
"last-sync": "Laatst gesynchroniseerd {{date}}",
|
|
758
|
+
"never-synced": "Nooit gesynchroniseerd",
|
|
759
|
+
"sync-ok": "Synchronisatie gelukt",
|
|
760
|
+
"sync-failed": "Synchronisatie mislukt",
|
|
761
|
+
assigned: "Toegewezen aan: {{agents}}",
|
|
762
|
+
unassigned: "Niet toegewezen aan een agent",
|
|
763
|
+
"secret-set": "Geheim ingesteld",
|
|
764
|
+
"no-secret": "Geen geheim",
|
|
765
|
+
auth: {
|
|
766
|
+
none: "Geen authenticatie",
|
|
767
|
+
bearer: "Bearer-token",
|
|
768
|
+
"api-key": "API-sleutel",
|
|
769
|
+
"forward-user-token": "Token van de gebruiker doorsturen"
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
"server-editor": {
|
|
773
|
+
"new-title": "Nieuwe MCP-server",
|
|
774
|
+
"edit-title": "{{name}} bewerken",
|
|
775
|
+
id: "Id",
|
|
776
|
+
"id-hint": "Kleine letters, cijfers en streepjes, maximaal 40 tekens. Het id kan later niet meer veranderen.",
|
|
777
|
+
"display-name": "Naam",
|
|
778
|
+
url: "URL",
|
|
779
|
+
"url-hint": "https is verplicht; http mag alleen voor localhost tijdens ontwikkeling.",
|
|
780
|
+
"url-invalid": "Vul een https-URL in.",
|
|
781
|
+
"auth-mode": "Authenticatie",
|
|
782
|
+
"header-name": "Headernaam",
|
|
783
|
+
"header-name-hint": "Standaard X-Api-Key.",
|
|
784
|
+
secret: "Geheim",
|
|
785
|
+
"secret-hint": "Wordt versleuteld opgeslagen en nooit meer getoond.",
|
|
786
|
+
"secret-set": "Er is een geheim ingesteld.",
|
|
787
|
+
"secret-none": "Er is geen geheim ingesteld.",
|
|
788
|
+
"secret-replace": "Vervangen",
|
|
789
|
+
"secret-clear": "Wissen",
|
|
790
|
+
"secret-will-clear": "Het geheim wordt gewist als je opslaat.",
|
|
791
|
+
"secret-undo": "Huidig geheim behouden",
|
|
792
|
+
"secret-new": "Nieuw geheim",
|
|
793
|
+
"forward-warning": "Het token van de ingelogde gebruiker wordt naar deze server gestuurd. De host van de server moet op de allow-list van de applicatie staan.",
|
|
794
|
+
"required-policy": "Vereiste policy",
|
|
795
|
+
"required-policy-hint": "Optioneel. De tools van de server zijn alleen beschikbaar voor gebruikers die aan deze policy voldoen.",
|
|
796
|
+
enabled: "Server staat aan"
|
|
797
|
+
},
|
|
798
|
+
tools: {
|
|
799
|
+
title: "Tools van {{name}}",
|
|
800
|
+
description: "Tools blijven uit en gelden als wijziging met goedkeuring tot je ze aanzet en het effect kiest. Annotaties van de server zijn alleen een hint.",
|
|
801
|
+
empty: "Nog geen tools. Synchroniseer de server om de tools te laden.",
|
|
802
|
+
enabled: "Tool staat aan",
|
|
803
|
+
effect: "Effect",
|
|
804
|
+
"description-override": "Omschrijving voor de assistent",
|
|
805
|
+
"description-override-hint": "Optioneel. Vervangt de omschrijving van de server.",
|
|
806
|
+
"remote-description": "Omschrijving van de server",
|
|
807
|
+
"hint-read-only": "Hint van de server: alleen lezen",
|
|
808
|
+
"hint-mutation": "Hint van de server: wijzigt gegevens",
|
|
809
|
+
status: {
|
|
810
|
+
available: "Beschikbaar",
|
|
811
|
+
"schema-changed": "Invoer gewijzigd",
|
|
812
|
+
missing: "Ontbreekt op de server"
|
|
813
|
+
},
|
|
814
|
+
"schema-changed-warning": "De invoer van de tool is op de server gewijzigd, daarom staat de tool uit. Controleer de tool voordat je hem opnieuw aanzet.",
|
|
815
|
+
"missing-warning": "De server biedt deze tool niet meer aan.",
|
|
816
|
+
save: "Tool opslaan",
|
|
817
|
+
back: "Terug naar de servers"
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
"page-context": {
|
|
821
|
+
included: "Wordt meegestuurd: {{page}}",
|
|
822
|
+
excluded: "Niet meegestuurd bij dit bericht: {{page}}",
|
|
823
|
+
exclude: "{{page}} niet meesturen met dit bericht",
|
|
824
|
+
include: "{{page}} toch meesturen met dit bericht"
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
/** The library's own translation bundles, keyed by language. All keys live under `nh-assistant.`. */
|
|
830
|
+
const NH_ASSISTANT_TRANSLATIONS = { en, nl };
|
|
831
|
+
const rootKey = 'nh-assistant';
|
|
832
|
+
function isTree(value) {
|
|
833
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
834
|
+
}
|
|
835
|
+
/** Deep merge where values of `override` win. */
|
|
836
|
+
function mergeTrees(base, override) {
|
|
837
|
+
const result = { ...base };
|
|
838
|
+
for (const [key, value] of Object.entries(override)) {
|
|
839
|
+
const current = result[key];
|
|
840
|
+
result[key] = isTree(current) && isTree(value) ? mergeTrees(current, value) : value;
|
|
841
|
+
}
|
|
842
|
+
return result;
|
|
843
|
+
}
|
|
844
|
+
function hasAllKeys(bundle, candidate) {
|
|
845
|
+
if (!isTree(candidate)) {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
return Object.entries(bundle).every(([key, value]) => isTree(value) ? hasAllKeys(value, candidate[key]) : key in candidate);
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Merges the bundled texts into `TranslateService` whenever the host loads or replaces a
|
|
852
|
+
* language. Host loaders never lose the library texts, the library never pre-empts the
|
|
853
|
+
* host loader, and keys the host defines under `nh-assistant.` (agent names, overrides)
|
|
854
|
+
* win over the bundle. Languages without a bundle receive the English texts.
|
|
855
|
+
*/
|
|
856
|
+
class NhAssistantTranslationMerger {
|
|
857
|
+
constructor() {
|
|
858
|
+
this.translate = inject(TranslateService, { optional: true });
|
|
859
|
+
this.destroyRef = inject(DestroyRef);
|
|
860
|
+
this.started = false;
|
|
861
|
+
}
|
|
862
|
+
start() {
|
|
863
|
+
const translate = this.translate;
|
|
864
|
+
if (this.started || !translate) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
this.started = true;
|
|
868
|
+
const subscription = merge(translate.onTranslationChange, translate.onLangChange).subscribe(event => {
|
|
869
|
+
if (event.lang && isTree(event.translations)) {
|
|
870
|
+
this.ensure(event.lang, event.translations[rootKey]);
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
this.destroyRef.onDestroy(() => subscription.unsubscribe());
|
|
874
|
+
// The current language is loaded or already requested by the host, so merging into it
|
|
875
|
+
// never pre-empts the host loader; a later load triggers a new merge.
|
|
876
|
+
const current = translate.getCurrentLang();
|
|
877
|
+
if (current) {
|
|
878
|
+
this.ensure(current, translate.instant(rootKey));
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
ensure(language, existing) {
|
|
882
|
+
const bundle = (NH_ASSISTANT_TRANSLATIONS[language] ?? NH_ASSISTANT_TRANSLATIONS.en);
|
|
883
|
+
const bundleRoot = bundle[rootKey];
|
|
884
|
+
if (hasAllKeys(bundleRoot, existing)) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const merged = mergeTrees(bundleRoot, isTree(existing) ? existing : {});
|
|
888
|
+
this.translate?.setTranslation(language, { [rootKey]: merged }, true);
|
|
889
|
+
}
|
|
890
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantTranslationMerger, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
891
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantTranslationMerger, providedIn: 'root' }); }
|
|
892
|
+
}
|
|
893
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantTranslationMerger, decorators: [{
|
|
894
|
+
type: Injectable,
|
|
895
|
+
args: [{ providedIn: 'root' }]
|
|
896
|
+
}] });
|
|
897
|
+
|
|
898
|
+
/** A failed JSON request of the assistant API. Carries a stable code, never response text. */
|
|
899
|
+
class NhAssistantApiError extends Error {
|
|
900
|
+
constructor(status, code, messageKey) {
|
|
901
|
+
super(code);
|
|
902
|
+
this.status = status;
|
|
903
|
+
this.code = code;
|
|
904
|
+
this.messageKey = messageKey;
|
|
905
|
+
this.name = 'NhAssistantApiError';
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/** Translation key that belongs to a failure code. */
|
|
909
|
+
function nhAssistantErrorMessageKey(code) {
|
|
910
|
+
return `nh-assistant.errors.${code}`;
|
|
911
|
+
}
|
|
912
|
+
/** Maps an HTTP status of the assistant API to a stable client failure code. */
|
|
913
|
+
function nhAssistantCodeForStatus(status) {
|
|
914
|
+
if (status === 0) {
|
|
915
|
+
return NhAssistantClientErrorCodes.network;
|
|
916
|
+
}
|
|
917
|
+
if (status === 400) {
|
|
918
|
+
return NhAssistantClientErrorCodes.validation;
|
|
919
|
+
}
|
|
920
|
+
if (status === 401) {
|
|
921
|
+
return NhAssistantClientErrorCodes.unauthenticated;
|
|
922
|
+
}
|
|
923
|
+
if (status === 403) {
|
|
924
|
+
return NhAssistantClientErrorCodes.forbidden;
|
|
925
|
+
}
|
|
926
|
+
if (status === 404) {
|
|
927
|
+
return NhAssistantClientErrorCodes.notFound;
|
|
928
|
+
}
|
|
929
|
+
if (status === 409) {
|
|
930
|
+
return NhAssistantClientErrorCodes.conversationBusy;
|
|
931
|
+
}
|
|
932
|
+
return NhAssistantClientErrorCodes.server;
|
|
933
|
+
}
|
|
934
|
+
const safeCode = /^[a-z0-9][a-z0-9.-]{0,79}$/;
|
|
935
|
+
/**
|
|
936
|
+
* Shared transport of the assistant services: `fetch` with the host's token, the current
|
|
937
|
+
* language and stable failure codes. Internal to the library.
|
|
938
|
+
*/
|
|
939
|
+
class NhAssistantTransport {
|
|
940
|
+
constructor() {
|
|
941
|
+
this.config = inject(NH_ASSISTANT_CONFIG);
|
|
942
|
+
this.fetchFn = inject(NH_ASSISTANT_FETCH);
|
|
943
|
+
this.translate = inject(TranslateService, { optional: true });
|
|
944
|
+
this.injector = inject(EnvironmentInjector);
|
|
945
|
+
}
|
|
946
|
+
/** A cold JSON request that aborts on unsubscribe and errors with `NhAssistantApiError`. */
|
|
947
|
+
json(method, path, body, statusCodes = {}) {
|
|
948
|
+
return new Observable(subscriber => {
|
|
949
|
+
const abort = new AbortController();
|
|
950
|
+
void this.executeJson(method, path, body, statusCodes, abort.signal, subscriber);
|
|
951
|
+
return () => abort.abort();
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
async fetch(method, path, accept, body, signal) {
|
|
955
|
+
const init = await this.createInit(method, accept, body, signal);
|
|
956
|
+
return this.fetchFn(this.url(path), init);
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* The failure code of a response: a safe `code` field of a JSON error body wins, then the
|
|
960
|
+
* endpoint's status mapping, then the generic mapping. Response text is never used.
|
|
961
|
+
*/
|
|
962
|
+
async failureCode(response, statusCodes = {}) {
|
|
963
|
+
try {
|
|
964
|
+
const text = await response.text();
|
|
965
|
+
const body = text.length > 0 ? JSON.parse(text) : null;
|
|
966
|
+
const code = body && typeof body === 'object' ? body['code'] : null;
|
|
967
|
+
if (typeof code === 'string' && safeCode.test(code)) {
|
|
968
|
+
return code;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
catch {
|
|
972
|
+
// A non-JSON error body carries no code.
|
|
973
|
+
}
|
|
974
|
+
return statusCodes[response.status] ?? nhAssistantCodeForStatus(response.status);
|
|
975
|
+
}
|
|
976
|
+
async executeJson(method, path, body, statusCodes, signal, subscriber) {
|
|
977
|
+
let response;
|
|
978
|
+
try {
|
|
979
|
+
response = await this.fetch(method, path, 'application/json', body, signal);
|
|
980
|
+
}
|
|
981
|
+
catch {
|
|
982
|
+
if (!signal.aborted) {
|
|
983
|
+
subscriber.error(createApiError(0, nhAssistantCodeForStatus(0)));
|
|
984
|
+
}
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (!response.ok) {
|
|
988
|
+
const code = await this.failureCode(response, statusCodes);
|
|
989
|
+
subscriber.error(createApiError(response.status, code));
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
try {
|
|
993
|
+
const text = response.status === 204 ? '' : await response.text();
|
|
994
|
+
subscriber.next((text.length > 0 ? JSON.parse(text) : undefined));
|
|
995
|
+
subscriber.complete();
|
|
996
|
+
}
|
|
997
|
+
catch {
|
|
998
|
+
if (!signal.aborted) {
|
|
999
|
+
subscriber.error(createApiError(response.status, NhAssistantClientErrorCodes.invalidResponse));
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
async createInit(method, accept, body, signal) {
|
|
1004
|
+
const headers = { Accept: accept };
|
|
1005
|
+
const token = await runInInjectionContext(this.injector, () => this.config.getAccessToken());
|
|
1006
|
+
if (token) {
|
|
1007
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
1008
|
+
}
|
|
1009
|
+
const language = this.translate?.getCurrentLang();
|
|
1010
|
+
if (language) {
|
|
1011
|
+
headers['Accept-Language'] = language;
|
|
1012
|
+
}
|
|
1013
|
+
if (body !== undefined) {
|
|
1014
|
+
headers['Content-Type'] = 'application/json';
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
method,
|
|
1018
|
+
headers,
|
|
1019
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
1020
|
+
signal,
|
|
1021
|
+
cache: 'no-store'
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
url(path) {
|
|
1025
|
+
const base = this.config.apiBaseUrl.replace(/\/+$/, '');
|
|
1026
|
+
return `${base}/${path}`;
|
|
1027
|
+
}
|
|
1028
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantTransport, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1029
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantTransport }); }
|
|
1030
|
+
}
|
|
1031
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantTransport, decorators: [{
|
|
1032
|
+
type: Injectable
|
|
1033
|
+
}] });
|
|
1034
|
+
function createApiError(status, code) {
|
|
1035
|
+
return new NhAssistantApiError(status, code, nhAssistantErrorMessageKey(code));
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/** In the admin endpoints a 409 means a stale `expectedVersion` or a forbidden state change. */
|
|
1039
|
+
const adminStatusCodes = {
|
|
1040
|
+
409: NhAssistantClientErrorCodes.versionConflict
|
|
1041
|
+
};
|
|
1042
|
+
/**
|
|
1043
|
+
* Client for the assistant administration endpoints (`admin/*`). The server requires the
|
|
1044
|
+
* access policy and the admin policy; show the administration only when
|
|
1045
|
+
* `NhAssistantStore.canAdminister()` is true. Failures error with `NhAssistantApiError`.
|
|
1046
|
+
* Secrets are write-only: `McpServer` only reports `hasSecret`.
|
|
1047
|
+
*/
|
|
1048
|
+
class NhAssistantAdminApiService {
|
|
1049
|
+
constructor() {
|
|
1050
|
+
this.transport = inject(NhAssistantTransport);
|
|
1051
|
+
}
|
|
1052
|
+
getContext() {
|
|
1053
|
+
return this.request('GET', 'admin/context');
|
|
1054
|
+
}
|
|
1055
|
+
/** Saves a new context version; fails with `assistant-version-conflict` when `expectedVersion` is stale. */
|
|
1056
|
+
updateContext(request) {
|
|
1057
|
+
return this.request('PUT', 'admin/context', request);
|
|
1058
|
+
}
|
|
1059
|
+
getContextVersions() {
|
|
1060
|
+
return this.request('GET', 'admin/context/versions');
|
|
1061
|
+
}
|
|
1062
|
+
/** Local, bridge and MCP tools, for choosing tool selectors. */
|
|
1063
|
+
getTools() {
|
|
1064
|
+
return this.request('GET', 'admin/tools');
|
|
1065
|
+
}
|
|
1066
|
+
getAgents() {
|
|
1067
|
+
return this.request('GET', 'admin/agents');
|
|
1068
|
+
}
|
|
1069
|
+
createAgent(input) {
|
|
1070
|
+
return this.request('POST', 'admin/agents', input);
|
|
1071
|
+
}
|
|
1072
|
+
updateAgent(agentId, request) {
|
|
1073
|
+
return this.request('PUT', `admin/agents/${encodeURIComponent(agentId)}`, request);
|
|
1074
|
+
}
|
|
1075
|
+
/** Restores a code agent to its definition in code. */
|
|
1076
|
+
resetAgent(agentId) {
|
|
1077
|
+
return this.request('POST', `admin/agents/${encodeURIComponent(agentId)}/reset`);
|
|
1078
|
+
}
|
|
1079
|
+
/** Deletes an admin agent. Code agents cannot be deleted; disable them instead. */
|
|
1080
|
+
deleteAgent(agentId) {
|
|
1081
|
+
return this.request('DELETE', `admin/agents/${encodeURIComponent(agentId)}`);
|
|
1082
|
+
}
|
|
1083
|
+
getMcpServers() {
|
|
1084
|
+
return this.request('GET', 'admin/mcp-servers');
|
|
1085
|
+
}
|
|
1086
|
+
createMcpServer(input) {
|
|
1087
|
+
return this.request('POST', 'admin/mcp-servers', input);
|
|
1088
|
+
}
|
|
1089
|
+
/** Omit `secret` to keep the stored secret; send `""` to clear it. */
|
|
1090
|
+
updateMcpServer(serverId, input) {
|
|
1091
|
+
return this.request('PUT', `admin/mcp-servers/${encodeURIComponent(serverId)}`, input);
|
|
1092
|
+
}
|
|
1093
|
+
deleteMcpServer(serverId) {
|
|
1094
|
+
return this.request('DELETE', `admin/mcp-servers/${encodeURIComponent(serverId)}`);
|
|
1095
|
+
}
|
|
1096
|
+
testMcpServer(serverId) {
|
|
1097
|
+
return this.request('POST', `admin/mcp-servers/${encodeURIComponent(serverId)}/test`);
|
|
1098
|
+
}
|
|
1099
|
+
/** Reads the remote tool list. New tools start disabled; a changed input schema disables a tool. */
|
|
1100
|
+
syncMcpServer(serverId) {
|
|
1101
|
+
return this.request('POST', `admin/mcp-servers/${encodeURIComponent(serverId)}/sync`);
|
|
1102
|
+
}
|
|
1103
|
+
getMcpTools(serverId) {
|
|
1104
|
+
return this.request('GET', `admin/mcp-servers/${encodeURIComponent(serverId)}/tools`);
|
|
1105
|
+
}
|
|
1106
|
+
updateMcpTool(serverId, remoteName, request) {
|
|
1107
|
+
const path = `admin/mcp-servers/${encodeURIComponent(serverId)}/tools/${encodeURIComponent(remoteName)}`;
|
|
1108
|
+
return this.request('PUT', path, request);
|
|
1109
|
+
}
|
|
1110
|
+
request(method, path, body) {
|
|
1111
|
+
return this.transport.json(method, path, body, adminStatusCodes);
|
|
1112
|
+
}
|
|
1113
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantAdminApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1114
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantAdminApiService }); }
|
|
1115
|
+
}
|
|
1116
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantAdminApiService, decorators: [{
|
|
1117
|
+
type: Injectable
|
|
1118
|
+
}] });
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Incremental parser for the `text/event-stream` format.
|
|
1122
|
+
*
|
|
1123
|
+
* Feed decoded text chunks in arrival order. Chunks may split lines, fields and line
|
|
1124
|
+
* terminators anywhere. Comment lines (starting with `:`, such as the `: keep-alive`
|
|
1125
|
+
* heartbeat) are ignored; `id` and `retry` fields are accepted and ignored. An event is
|
|
1126
|
+
* dispatched at the empty line that terminates it. An unterminated event at the end of
|
|
1127
|
+
* the stream is discarded, as the SSE specification requires.
|
|
1128
|
+
*/
|
|
1129
|
+
class NhAssistantSseParser {
|
|
1130
|
+
constructor() {
|
|
1131
|
+
this.buffer = '';
|
|
1132
|
+
this.eventName = '';
|
|
1133
|
+
this.dataLines = [];
|
|
1134
|
+
}
|
|
1135
|
+
push(chunk) {
|
|
1136
|
+
this.buffer += chunk;
|
|
1137
|
+
const messages = [];
|
|
1138
|
+
let lineStart = 0;
|
|
1139
|
+
for (let index = 0; index < this.buffer.length; index++) {
|
|
1140
|
+
const character = this.buffer[index];
|
|
1141
|
+
if (character !== '\n' && character !== '\r') {
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
if (character === '\r' && index === this.buffer.length - 1) {
|
|
1145
|
+
// A trailing CR may be the first half of a CRLF split across chunks.
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
const line = this.buffer.slice(lineStart, index);
|
|
1149
|
+
if (character === '\r' && this.buffer[index + 1] === '\n') {
|
|
1150
|
+
index++;
|
|
1151
|
+
}
|
|
1152
|
+
lineStart = index + 1;
|
|
1153
|
+
const message = this.processLine(line);
|
|
1154
|
+
if (message) {
|
|
1155
|
+
messages.push(message);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
this.buffer = this.buffer.slice(lineStart);
|
|
1159
|
+
return messages;
|
|
1160
|
+
}
|
|
1161
|
+
/** Signals the end of the stream and returns an event that a trailing CR completed. */
|
|
1162
|
+
end() {
|
|
1163
|
+
const messages = [];
|
|
1164
|
+
if (this.buffer.endsWith('\r')) {
|
|
1165
|
+
const message = this.processLine(this.buffer.slice(0, -1));
|
|
1166
|
+
if (message) {
|
|
1167
|
+
messages.push(message);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
this.buffer = '';
|
|
1171
|
+
this.eventName = '';
|
|
1172
|
+
this.dataLines = [];
|
|
1173
|
+
return messages;
|
|
1174
|
+
}
|
|
1175
|
+
processLine(line) {
|
|
1176
|
+
if (line.length === 0) {
|
|
1177
|
+
return this.dispatch();
|
|
1178
|
+
}
|
|
1179
|
+
if (line.startsWith(':')) {
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
const colonIndex = line.indexOf(':');
|
|
1183
|
+
const field = colonIndex < 0 ? line : line.slice(0, colonIndex);
|
|
1184
|
+
let value = colonIndex < 0 ? '' : line.slice(colonIndex + 1);
|
|
1185
|
+
if (value.startsWith(' ')) {
|
|
1186
|
+
value = value.slice(1);
|
|
1187
|
+
}
|
|
1188
|
+
if (field === 'event') {
|
|
1189
|
+
this.eventName = value;
|
|
1190
|
+
}
|
|
1191
|
+
else if (field === 'data') {
|
|
1192
|
+
this.dataLines.push(value);
|
|
1193
|
+
}
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
dispatch() {
|
|
1197
|
+
const hasData = this.dataLines.length > 0;
|
|
1198
|
+
const message = {
|
|
1199
|
+
event: this.eventName || 'message',
|
|
1200
|
+
data: this.dataLines.join('\n')
|
|
1201
|
+
};
|
|
1202
|
+
this.eventName = '';
|
|
1203
|
+
this.dataLines = [];
|
|
1204
|
+
return hasData ? message : null;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const knownEventTypes = new Set(NH_ASSISTANT_SSE_EVENT_TYPES);
|
|
1209
|
+
/**
|
|
1210
|
+
* Client for the assistant HTTP API. JSON endpoints return cold observables that abort
|
|
1211
|
+
* their request on unsubscribe. Streaming endpoints emit every contract event and complete
|
|
1212
|
+
* after `turn.completed` or `error`; transport failures become an `error` event with a
|
|
1213
|
+
* client failure code instead of an observable error.
|
|
1214
|
+
*/
|
|
1215
|
+
class NhAssistantApiService {
|
|
1216
|
+
constructor() {
|
|
1217
|
+
this.transport = inject(NhAssistantTransport);
|
|
1218
|
+
}
|
|
1219
|
+
status() {
|
|
1220
|
+
return this.transport.json('GET', 'status');
|
|
1221
|
+
}
|
|
1222
|
+
agents() {
|
|
1223
|
+
return this.transport.json('GET', 'agents');
|
|
1224
|
+
}
|
|
1225
|
+
listConversations(page = 1, itemsPerPage = 20) {
|
|
1226
|
+
const query = `page=${encodeURIComponent(page)}&itemsPerPage=${encodeURIComponent(itemsPerPage)}`;
|
|
1227
|
+
return this.transport.json('GET', `conversations?${query}`);
|
|
1228
|
+
}
|
|
1229
|
+
createConversation(request) {
|
|
1230
|
+
return this.transport.json('POST', 'conversations', request);
|
|
1231
|
+
}
|
|
1232
|
+
getConversation(conversationId) {
|
|
1233
|
+
return this.transport.json('GET', `conversations/${encodeURIComponent(conversationId)}`);
|
|
1234
|
+
}
|
|
1235
|
+
deleteConversation(conversationId) {
|
|
1236
|
+
return this.transport.json('DELETE', `conversations/${encodeURIComponent(conversationId)}`);
|
|
1237
|
+
}
|
|
1238
|
+
sendMessage(conversationId, request) {
|
|
1239
|
+
return this.requestStream(`conversations/${encodeURIComponent(conversationId)}/messages`, request);
|
|
1240
|
+
}
|
|
1241
|
+
decideApproval(conversationId, approvalId, request) {
|
|
1242
|
+
const path = `conversations/${encodeURIComponent(conversationId)}/approvals/${encodeURIComponent(approvalId)}/decide`;
|
|
1243
|
+
return this.requestStream(path, request);
|
|
1244
|
+
}
|
|
1245
|
+
cancel(conversationId) {
|
|
1246
|
+
return this.transport.json('POST', `conversations/${encodeURIComponent(conversationId)}/cancel`);
|
|
1247
|
+
}
|
|
1248
|
+
/** The caller's own assistant preferences. */
|
|
1249
|
+
getPreferences() {
|
|
1250
|
+
return this.transport.json('GET', 'preferences');
|
|
1251
|
+
}
|
|
1252
|
+
/** Replaces the caller's own assistant preferences and returns the stored values. */
|
|
1253
|
+
updatePreferences(preferences) {
|
|
1254
|
+
return this.transport.json('PUT', 'preferences', preferences);
|
|
1255
|
+
}
|
|
1256
|
+
requestStream(path, body) {
|
|
1257
|
+
return new Observable(subscriber => {
|
|
1258
|
+
const abort = new AbortController();
|
|
1259
|
+
void this.executeStream(path, body, abort.signal, subscriber);
|
|
1260
|
+
return () => abort.abort();
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
async executeStream(path, body, signal, subscriber) {
|
|
1264
|
+
let response;
|
|
1265
|
+
try {
|
|
1266
|
+
response = await this.transport.fetch('POST', path, 'text/event-stream', body, signal);
|
|
1267
|
+
}
|
|
1268
|
+
catch {
|
|
1269
|
+
this.emitClientError(subscriber, signal, NhAssistantClientErrorCodes.network);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
if (!response.ok) {
|
|
1273
|
+
this.emitClientError(subscriber, signal, await this.transport.failureCode(response));
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
1277
|
+
if (!response.body || !contentType.toLowerCase().startsWith('text/event-stream')) {
|
|
1278
|
+
this.emitClientError(subscriber, signal, NhAssistantClientErrorCodes.invalidResponse);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
await this.readStream(response.body, signal, subscriber);
|
|
1282
|
+
}
|
|
1283
|
+
async readStream(body, signal, subscriber) {
|
|
1284
|
+
const reader = body.getReader();
|
|
1285
|
+
const decoder = new TextDecoder();
|
|
1286
|
+
const parser = new NhAssistantSseParser();
|
|
1287
|
+
try {
|
|
1288
|
+
while (true) {
|
|
1289
|
+
const { done, value } = await reader.read();
|
|
1290
|
+
const text = done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
1291
|
+
const messages = done ? [...parser.push(text), ...parser.end()] : parser.push(text);
|
|
1292
|
+
for (const message of messages) {
|
|
1293
|
+
if (!knownEventTypes.has(message.event)) {
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
let data;
|
|
1297
|
+
try {
|
|
1298
|
+
data = JSON.parse(message.data);
|
|
1299
|
+
}
|
|
1300
|
+
catch {
|
|
1301
|
+
this.emitClientError(subscriber, signal, NhAssistantClientErrorCodes.invalidResponse);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const event = { type: message.event, data };
|
|
1305
|
+
subscriber.next(event);
|
|
1306
|
+
if (event.type === 'turn.completed' || event.type === 'error') {
|
|
1307
|
+
subscriber.complete();
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
if (done) {
|
|
1312
|
+
this.emitClientError(subscriber, signal, NhAssistantClientErrorCodes.streamInterrupted);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
catch {
|
|
1318
|
+
this.emitClientError(subscriber, signal, NhAssistantClientErrorCodes.streamInterrupted);
|
|
1319
|
+
}
|
|
1320
|
+
finally {
|
|
1321
|
+
// Releases the connection once a terminal event arrived or the consumer unsubscribed.
|
|
1322
|
+
reader.cancel().catch(() => undefined);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
emitClientError(subscriber, signal, code) {
|
|
1326
|
+
if (signal.aborted || subscriber.closed) {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
subscriber.next({ type: 'error', data: { code, messageKey: nhAssistantErrorMessageKey(code) } });
|
|
1330
|
+
subscriber.complete();
|
|
1331
|
+
}
|
|
1332
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1333
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantApiService }); }
|
|
1334
|
+
}
|
|
1335
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantApiService, decorators: [{
|
|
1336
|
+
type: Injectable
|
|
1337
|
+
}] });
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Applies one assistant event to a conversation and returns the new conversation.
|
|
1341
|
+
* The input is never mutated, so the result can be stored in a signal directly.
|
|
1342
|
+
*
|
|
1343
|
+
* @param clientMessageId id of the optimistic user message of the current turn; its id is
|
|
1344
|
+
* replaced by the server's `userMessageId` when `turn.started` arrives.
|
|
1345
|
+
*/
|
|
1346
|
+
function applyNhAssistantEvent(conversation, event, clientMessageId) {
|
|
1347
|
+
switch (event.type) {
|
|
1348
|
+
case 'turn.started': {
|
|
1349
|
+
let messages = conversation.messages.map(message => clientMessageId && message.id === clientMessageId ? { ...message, id: event.data.userMessageId } : message);
|
|
1350
|
+
if (!messages.some(message => message.id === event.data.assistantMessageId)) {
|
|
1351
|
+
messages = [...messages, createAssistantMessage(event.data.assistantMessageId)];
|
|
1352
|
+
}
|
|
1353
|
+
return { ...conversation, status: 'running', messages };
|
|
1354
|
+
}
|
|
1355
|
+
case 'message.delta': {
|
|
1356
|
+
const messages = ensureMessage(conversation.messages, event.data.messageId);
|
|
1357
|
+
return {
|
|
1358
|
+
...conversation,
|
|
1359
|
+
messages: messages.map(message => message.id === event.data.messageId ? appendText(message, event.data.text) : message)
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
case 'tool.started': {
|
|
1363
|
+
const data = event.data;
|
|
1364
|
+
if (findToolCall(conversation.messages, data.invocationId)) {
|
|
1365
|
+
return updateToolCall(conversation, data.invocationId, part => ({
|
|
1366
|
+
...part,
|
|
1367
|
+
toolId: data.toolId,
|
|
1368
|
+
toolVersion: data.toolVersion,
|
|
1369
|
+
displayName: data.displayName,
|
|
1370
|
+
argumentsPreview: data.argumentsPreview,
|
|
1371
|
+
status: 'running'
|
|
1372
|
+
}));
|
|
1373
|
+
}
|
|
1374
|
+
const part = {
|
|
1375
|
+
type: 'tool-call',
|
|
1376
|
+
invocationId: data.invocationId,
|
|
1377
|
+
toolId: data.toolId,
|
|
1378
|
+
toolVersion: data.toolVersion,
|
|
1379
|
+
displayName: data.displayName,
|
|
1380
|
+
status: 'running',
|
|
1381
|
+
argumentsPreview: data.argumentsPreview,
|
|
1382
|
+
resultPreview: null,
|
|
1383
|
+
resultCode: null
|
|
1384
|
+
};
|
|
1385
|
+
return appendToCurrentAssistantMessage(conversation, part);
|
|
1386
|
+
}
|
|
1387
|
+
case 'tool.completed': {
|
|
1388
|
+
const data = event.data;
|
|
1389
|
+
return updateToolCall(conversation, data.invocationId, part => ({
|
|
1390
|
+
...part,
|
|
1391
|
+
status: data.status,
|
|
1392
|
+
resultCode: data.resultCode,
|
|
1393
|
+
resultPreview: data.resultPreview
|
|
1394
|
+
}));
|
|
1395
|
+
}
|
|
1396
|
+
case 'approval.required': {
|
|
1397
|
+
const approval = { ...event.data, type: 'approval' };
|
|
1398
|
+
const withToolAwaiting = markLatestRunningToolAwaiting(conversation, approval.toolId);
|
|
1399
|
+
const exists = withToolAwaiting.messages.some(message => message.parts.some(part => part.type === 'approval' && part.approvalId === approval.approvalId));
|
|
1400
|
+
const withApproval = exists
|
|
1401
|
+
? replaceApproval(withToolAwaiting, approval)
|
|
1402
|
+
: appendToCurrentAssistantMessage(withToolAwaiting, approval);
|
|
1403
|
+
return { ...withApproval, status: 'waiting-for-approval', pendingApproval: approval };
|
|
1404
|
+
}
|
|
1405
|
+
case 'turn.completed': {
|
|
1406
|
+
const status = conversationStatusFor(event.data.status);
|
|
1407
|
+
return {
|
|
1408
|
+
...conversation,
|
|
1409
|
+
status,
|
|
1410
|
+
pendingApproval: status === 'waiting-for-approval' ? conversation.pendingApproval : null
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
case 'error':
|
|
1414
|
+
return {
|
|
1415
|
+
...conversation,
|
|
1416
|
+
status: conversation.status === 'running' ? 'failed' : conversation.status
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
/** Records the user's approval decision locally until the server state is reloaded. */
|
|
1421
|
+
function applyNhAssistantApprovalDecision(conversation, approvalId, decision) {
|
|
1422
|
+
const approval = conversation.messages
|
|
1423
|
+
.flatMap(message => message.parts)
|
|
1424
|
+
.find((part) => part.type === 'approval' && part.approvalId === approvalId)
|
|
1425
|
+
?? conversation.pendingApproval;
|
|
1426
|
+
const toolId = approval?.approvalId === approvalId ? approval.toolId : null;
|
|
1427
|
+
const messages = conversation.messages.map(message => ({
|
|
1428
|
+
...message,
|
|
1429
|
+
parts: message.parts.map(part => part.type === 'approval' && part.approvalId === approvalId
|
|
1430
|
+
? { ...part, status: decision === 'approve' ? 'approved' : 'rejected' }
|
|
1431
|
+
: part)
|
|
1432
|
+
}));
|
|
1433
|
+
let updated = {
|
|
1434
|
+
...conversation,
|
|
1435
|
+
messages,
|
|
1436
|
+
status: 'running',
|
|
1437
|
+
pendingApproval: null
|
|
1438
|
+
};
|
|
1439
|
+
if (toolId) {
|
|
1440
|
+
const awaiting = findLatestToolCall(updated.messages, part => part.toolId === toolId && part.status === 'awaiting-approval');
|
|
1441
|
+
if (awaiting) {
|
|
1442
|
+
updated = updateToolCall(updated, awaiting.invocationId, part => ({
|
|
1443
|
+
...part,
|
|
1444
|
+
status: decision === 'approve' ? 'running' : 'rejected'
|
|
1445
|
+
}));
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return updated;
|
|
1449
|
+
}
|
|
1450
|
+
function conversationStatusFor(status) {
|
|
1451
|
+
switch (status) {
|
|
1452
|
+
case 'completed':
|
|
1453
|
+
case 'cancelled':
|
|
1454
|
+
return 'idle';
|
|
1455
|
+
case 'waiting-for-approval':
|
|
1456
|
+
return 'waiting-for-approval';
|
|
1457
|
+
case 'failed':
|
|
1458
|
+
return 'failed';
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
function createAssistantMessage(id) {
|
|
1462
|
+
return { id, role: 'assistant', createdAt: new Date().toISOString(), parts: [] };
|
|
1463
|
+
}
|
|
1464
|
+
function ensureMessage(messages, messageId) {
|
|
1465
|
+
return messages.some(message => message.id === messageId)
|
|
1466
|
+
? messages
|
|
1467
|
+
: [...messages, createAssistantMessage(messageId)];
|
|
1468
|
+
}
|
|
1469
|
+
function appendText(message, text) {
|
|
1470
|
+
const last = message.parts[message.parts.length - 1];
|
|
1471
|
+
if (last?.type === 'text') {
|
|
1472
|
+
return { ...message, parts: [...message.parts.slice(0, -1), { type: 'text', text: last.text + text }] };
|
|
1473
|
+
}
|
|
1474
|
+
return { ...message, parts: [...message.parts, { type: 'text', text }] };
|
|
1475
|
+
}
|
|
1476
|
+
function appendToCurrentAssistantMessage(conversation, part) {
|
|
1477
|
+
let index = -1;
|
|
1478
|
+
for (let position = conversation.messages.length - 1; position >= 0; position--) {
|
|
1479
|
+
if (conversation.messages[position].role === 'assistant') {
|
|
1480
|
+
index = position;
|
|
1481
|
+
break;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (index < 0) {
|
|
1485
|
+
const message = createAssistantMessage(`local-${partKey(part)}`);
|
|
1486
|
+
return { ...conversation, messages: [...conversation.messages, { ...message, parts: [part] }] };
|
|
1487
|
+
}
|
|
1488
|
+
return {
|
|
1489
|
+
...conversation,
|
|
1490
|
+
messages: conversation.messages.map((message, position) => position === index ? { ...message, parts: [...message.parts, part] } : message)
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
function partKey(part) {
|
|
1494
|
+
if (part.type === 'tool-call') {
|
|
1495
|
+
return part.invocationId;
|
|
1496
|
+
}
|
|
1497
|
+
if (part.type === 'approval') {
|
|
1498
|
+
return part.approvalId;
|
|
1499
|
+
}
|
|
1500
|
+
return 'text';
|
|
1501
|
+
}
|
|
1502
|
+
function findToolCall(messages, invocationId) {
|
|
1503
|
+
return findLatestToolCall(messages, part => part.invocationId === invocationId);
|
|
1504
|
+
}
|
|
1505
|
+
function findLatestToolCall(messages, predicate) {
|
|
1506
|
+
for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex--) {
|
|
1507
|
+
const parts = messages[messageIndex].parts;
|
|
1508
|
+
for (let partIndex = parts.length - 1; partIndex >= 0; partIndex--) {
|
|
1509
|
+
const part = parts[partIndex];
|
|
1510
|
+
if (part.type === 'tool-call' && predicate(part)) {
|
|
1511
|
+
return part;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return undefined;
|
|
1516
|
+
}
|
|
1517
|
+
function updateToolCall(conversation, invocationId, update) {
|
|
1518
|
+
return {
|
|
1519
|
+
...conversation,
|
|
1520
|
+
messages: conversation.messages.map(message => message.parts.some(part => part.type === 'tool-call' && part.invocationId === invocationId)
|
|
1521
|
+
? {
|
|
1522
|
+
...message,
|
|
1523
|
+
parts: message.parts.map(part => part.type === 'tool-call' && part.invocationId === invocationId ? update(part) : part)
|
|
1524
|
+
}
|
|
1525
|
+
: message)
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
function markLatestRunningToolAwaiting(conversation, toolId) {
|
|
1529
|
+
const running = findLatestToolCall(conversation.messages, part => part.toolId === toolId && part.status === 'running');
|
|
1530
|
+
if (!running) {
|
|
1531
|
+
return conversation;
|
|
1532
|
+
}
|
|
1533
|
+
return updateToolCall(conversation, running.invocationId, part => ({ ...part, status: 'awaiting-approval' }));
|
|
1534
|
+
}
|
|
1535
|
+
function replaceApproval(conversation, approval) {
|
|
1536
|
+
return {
|
|
1537
|
+
...conversation,
|
|
1538
|
+
messages: conversation.messages.map(message => ({
|
|
1539
|
+
...message,
|
|
1540
|
+
parts: message.parts.map(part => part.type === 'approval' && part.approvalId === approval.approvalId ? approval : part)
|
|
1541
|
+
}))
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/** Limits of the page context (contract 15.2). */
|
|
1546
|
+
const NH_ASSISTANT_PAGE_CONTEXT_LIMITS = {
|
|
1547
|
+
route: 200,
|
|
1548
|
+
title: 120,
|
|
1549
|
+
entities: 5,
|
|
1550
|
+
entityId: 64,
|
|
1551
|
+
entityLabel: 120
|
|
1552
|
+
};
|
|
1553
|
+
const dashCase = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1554
|
+
/**
|
|
1555
|
+
* Validates host-provided page context and truncates it to the contract limits.
|
|
1556
|
+
* Returns null for an invalid shape (not an object or no route), so the message is sent
|
|
1557
|
+
* without page context. Route, title and labels are truncated; entities beyond five are
|
|
1558
|
+
* dropped, and entities with a type that is not dash-case, an empty id or an id longer
|
|
1559
|
+
* than 64 characters are dropped because a truncated id would point at another entity.
|
|
1560
|
+
*/
|
|
1561
|
+
function normalizeNhAssistantClientContext(value) {
|
|
1562
|
+
if (!isRecord(value)) {
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
const route = typeof value['route'] === 'string' ? value['route'].trim() : '';
|
|
1566
|
+
if (route.length === 0) {
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
const result = { route: truncate(route, NH_ASSISTANT_PAGE_CONTEXT_LIMITS.route) };
|
|
1570
|
+
const title = typeof value['title'] === 'string' ? value['title'].trim() : '';
|
|
1571
|
+
if (title.length > 0) {
|
|
1572
|
+
result.title = truncate(title, NH_ASSISTANT_PAGE_CONTEXT_LIMITS.title);
|
|
1573
|
+
}
|
|
1574
|
+
if (Array.isArray(value['entities'])) {
|
|
1575
|
+
const entities = value['entities']
|
|
1576
|
+
.map(normalizeEntity)
|
|
1577
|
+
.filter((entity) => entity !== null)
|
|
1578
|
+
.slice(0, NH_ASSISTANT_PAGE_CONTEXT_LIMITS.entities);
|
|
1579
|
+
if (entities.length > 0) {
|
|
1580
|
+
result.entities = entities;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
return result;
|
|
1584
|
+
}
|
|
1585
|
+
/** Short text for the page-context chip: the first entity's label, the title or the route. */
|
|
1586
|
+
function describeNhAssistantClientContext(context) {
|
|
1587
|
+
const entity = context.entities?.[0];
|
|
1588
|
+
if (entity) {
|
|
1589
|
+
return entity.label ?? `${entity.type} ${entity.id}`;
|
|
1590
|
+
}
|
|
1591
|
+
return context.title ?? context.route;
|
|
1592
|
+
}
|
|
1593
|
+
function normalizeEntity(value) {
|
|
1594
|
+
if (!isRecord(value)) {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
const type = typeof value['type'] === 'string' ? value['type'].trim() : '';
|
|
1598
|
+
const id = typeof value['id'] === 'string' ? value['id'].trim() : '';
|
|
1599
|
+
if (!dashCase.test(type) || id.length === 0 || id.length > NH_ASSISTANT_PAGE_CONTEXT_LIMITS.entityId) {
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
const entity = { type, id };
|
|
1603
|
+
const label = typeof value['label'] === 'string' ? value['label'].trim() : '';
|
|
1604
|
+
if (label.length > 0) {
|
|
1605
|
+
entity.label = truncate(label, NH_ASSISTANT_PAGE_CONTEXT_LIMITS.entityLabel);
|
|
1606
|
+
}
|
|
1607
|
+
return entity;
|
|
1608
|
+
}
|
|
1609
|
+
function truncate(value, max) {
|
|
1610
|
+
return value.length <= max ? value : value.slice(0, max);
|
|
1611
|
+
}
|
|
1612
|
+
function isRecord(value) {
|
|
1613
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const conversationPageSize = 50;
|
|
1617
|
+
const cancelGracePeriodMs = 5_000;
|
|
1618
|
+
/**
|
|
1619
|
+
* Signal-based state of one assistant scope: availability, agents, the conversation list
|
|
1620
|
+
* and the active conversation with its streamed turn.
|
|
1621
|
+
*/
|
|
1622
|
+
class NhAssistantStore {
|
|
1623
|
+
constructor() {
|
|
1624
|
+
this.api = inject(NhAssistantApiService);
|
|
1625
|
+
this.config = inject(NH_ASSISTANT_CONFIG);
|
|
1626
|
+
this.injector = inject(EnvironmentInjector);
|
|
1627
|
+
this.accessPolicy = inject(NH_ASSISTANT_ACCESS_POLICY);
|
|
1628
|
+
this.accessGrantedState = signal(null, ...(ngDevMode ? [{ debugName: "accessGrantedState" }] : []));
|
|
1629
|
+
this.statusState = signal(null, ...(ngDevMode ? [{ debugName: "statusState" }] : []));
|
|
1630
|
+
this.statusLoadingState = signal(false, ...(ngDevMode ? [{ debugName: "statusLoadingState" }] : []));
|
|
1631
|
+
this.selectedAgentIdState = signal(null, ...(ngDevMode ? [{ debugName: "selectedAgentIdState" }] : []));
|
|
1632
|
+
this.conversationsState = signal([], ...(ngDevMode ? [{ debugName: "conversationsState" }] : []));
|
|
1633
|
+
this.conversationsTotalState = signal(0, ...(ngDevMode ? [{ debugName: "conversationsTotalState" }] : []));
|
|
1634
|
+
this.conversationsLoadingState = signal(false, ...(ngDevMode ? [{ debugName: "conversationsLoadingState" }] : []));
|
|
1635
|
+
this.activeConversationState = signal(null, ...(ngDevMode ? [{ debugName: "activeConversationState" }] : []));
|
|
1636
|
+
this.conversationLoadingState = signal(false, ...(ngDevMode ? [{ debugName: "conversationLoadingState" }] : []));
|
|
1637
|
+
this.streamingState = signal(false, ...(ngDevMode ? [{ debugName: "streamingState" }] : []));
|
|
1638
|
+
this.decidingState = signal(false, ...(ngDevMode ? [{ debugName: "decidingState" }] : []));
|
|
1639
|
+
this.errorState = signal(null, ...(ngDevMode ? [{ debugName: "errorState" }] : []));
|
|
1640
|
+
this.lastUsageState = signal(null, ...(ngDevMode ? [{ debugName: "lastUsageState" }] : []));
|
|
1641
|
+
this.restoredDraftState = signal(null, ...(ngDevMode ? [{ debugName: "restoredDraftState" }] : []));
|
|
1642
|
+
this.pageContextState = signal(null, ...(ngDevMode ? [{ debugName: "pageContextState" }] : []));
|
|
1643
|
+
this.pageContextExcludedState = signal(false, ...(ngDevMode ? [{ debugName: "pageContextExcludedState" }] : []));
|
|
1644
|
+
/** True when the access policy allows the user and the server reports the assistant as enabled. */
|
|
1645
|
+
this.enabled = computed(() => this.accessGrantedState() === true && this.statusState()?.enabled === true, ...(ngDevMode ? [{ debugName: "enabled" }] : []));
|
|
1646
|
+
this.accessGranted = this.accessGrantedState.asReadonly();
|
|
1647
|
+
/** True when the assistant is enabled and the server reports that the caller passes the admin policy. */
|
|
1648
|
+
this.canAdminister = computed(() => this.enabled() && this.statusState()?.canAdminister === true, ...(ngDevMode ? [{ debugName: "canAdminister" }] : []));
|
|
1649
|
+
this.statusLoading = this.statusLoadingState.asReadonly();
|
|
1650
|
+
this.limits = computed(() => this.statusState()?.limits ?? null, ...(ngDevMode ? [{ debugName: "limits" }] : []));
|
|
1651
|
+
this.agents = computed(() => this.statusState()?.agents ?? [], ...(ngDevMode ? [{ debugName: "agents" }] : []));
|
|
1652
|
+
this.selectedAgentId = this.selectedAgentIdState.asReadonly();
|
|
1653
|
+
this.selectedAgent = computed(() => this.agents().find(agent => agent.id === this.selectedAgentIdState()) ?? null, ...(ngDevMode ? [{ debugName: "selectedAgent" }] : []));
|
|
1654
|
+
this.conversations = this.conversationsState.asReadonly();
|
|
1655
|
+
this.conversationsTotal = this.conversationsTotalState.asReadonly();
|
|
1656
|
+
this.conversationsLoading = this.conversationsLoadingState.asReadonly();
|
|
1657
|
+
this.activeConversation = this.activeConversationState.asReadonly();
|
|
1658
|
+
this.conversationLoading = this.conversationLoadingState.asReadonly();
|
|
1659
|
+
this.streaming = this.streamingState.asReadonly();
|
|
1660
|
+
this.pendingApproval = computed(() => this.activeConversationState()?.pendingApproval ?? null, ...(ngDevMode ? [{ debugName: "pendingApproval" }] : []));
|
|
1661
|
+
this.deciding = this.decidingState.asReadonly();
|
|
1662
|
+
this.error = this.errorState.asReadonly();
|
|
1663
|
+
this.lastUsage = this.lastUsageState.asReadonly();
|
|
1664
|
+
/** Text of a message the server did not accept, so the composer can offer it again. */
|
|
1665
|
+
this.restoredDraft = this.restoredDraftState.asReadonly();
|
|
1666
|
+
/** Page context that the next message would send, for the transparency chip. */
|
|
1667
|
+
this.pageContext = this.pageContextState.asReadonly();
|
|
1668
|
+
/** True when the user left the page context out of the next message. */
|
|
1669
|
+
this.pageContextExcluded = this.pageContextExcludedState.asReadonly();
|
|
1670
|
+
/** True while the user can send: enabled, an agent is chosen and no turn runs or waits. */
|
|
1671
|
+
this.canSend = computed(() => {
|
|
1672
|
+
const conversation = this.activeConversationState();
|
|
1673
|
+
const blocked = conversation !== null && conversation.status !== 'idle';
|
|
1674
|
+
return this.enabled() && this.selectedAgentIdState() !== null && !this.streamingState() && !blocked;
|
|
1675
|
+
}, ...(ngDevMode ? [{ debugName: "canSend" }] : []));
|
|
1676
|
+
inject(DestroyRef).onDestroy(() => {
|
|
1677
|
+
this.accessSubscription?.unsubscribe();
|
|
1678
|
+
this.streamSubscription?.unsubscribe();
|
|
1679
|
+
clearTimeout(this.cancelTimer);
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
/** Evaluates the access policy and loads the server status once. Safe to call repeatedly. */
|
|
1683
|
+
initialize() {
|
|
1684
|
+
this.initialization ??= this.startInitialization();
|
|
1685
|
+
return this.initialization;
|
|
1686
|
+
}
|
|
1687
|
+
/** Reloads the server status, for example after the host changed the signed-in user. */
|
|
1688
|
+
async reloadStatus() {
|
|
1689
|
+
if (this.accessGrantedState() !== true) {
|
|
1690
|
+
this.statusState.set(null);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
this.statusLoadingState.set(true);
|
|
1694
|
+
try {
|
|
1695
|
+
const status = await firstValueFrom(this.api.status());
|
|
1696
|
+
this.statusState.set(status);
|
|
1697
|
+
this.selectInitialAgent(status.agents);
|
|
1698
|
+
}
|
|
1699
|
+
catch {
|
|
1700
|
+
// An unavailable assistant endpoint hides the assistant instead of showing an error.
|
|
1701
|
+
this.statusState.set(null);
|
|
1702
|
+
}
|
|
1703
|
+
finally {
|
|
1704
|
+
this.statusLoadingState.set(false);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
async refreshConversations() {
|
|
1708
|
+
if (!this.enabled()) {
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
this.conversationsLoadingState.set(true);
|
|
1712
|
+
try {
|
|
1713
|
+
const page = await firstValueFrom(this.api.listConversations(1, conversationPageSize));
|
|
1714
|
+
this.conversationsState.set(page.items);
|
|
1715
|
+
this.conversationsTotalState.set(page.total);
|
|
1716
|
+
}
|
|
1717
|
+
catch (error) {
|
|
1718
|
+
this.setError(error);
|
|
1719
|
+
}
|
|
1720
|
+
finally {
|
|
1721
|
+
this.conversationsLoadingState.set(false);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
/** Chooses the agent for the next new conversation. Switching away from the active conversation's agent starts a new one. */
|
|
1725
|
+
selectAgent(agentId) {
|
|
1726
|
+
if (!this.agents().some(agent => agent.id === agentId)) {
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
this.selectedAgentIdState.set(agentId);
|
|
1730
|
+
const active = this.activeConversationState();
|
|
1731
|
+
if (active && active.agentId !== agentId && !this.streamingState()) {
|
|
1732
|
+
this.startNewConversation();
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
/** Leaves the active conversation; the next message creates a new one. */
|
|
1736
|
+
startNewConversation() {
|
|
1737
|
+
if (this.streamingState()) {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
this.activeConversationState.set(null);
|
|
1741
|
+
this.errorState.set(null);
|
|
1742
|
+
this.lastUsageState.set(null);
|
|
1743
|
+
}
|
|
1744
|
+
async openConversation(conversationId) {
|
|
1745
|
+
if (this.streamingState() || this.activeConversationState()?.id === conversationId) {
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
this.conversationLoadingState.set(true);
|
|
1749
|
+
this.errorState.set(null);
|
|
1750
|
+
try {
|
|
1751
|
+
const conversation = await firstValueFrom(this.api.getConversation(conversationId));
|
|
1752
|
+
this.activeConversationState.set(conversation);
|
|
1753
|
+
if (this.agents().some(agent => agent.id === conversation.agentId)) {
|
|
1754
|
+
this.selectedAgentIdState.set(conversation.agentId);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
catch (error) {
|
|
1758
|
+
this.setError(error);
|
|
1759
|
+
}
|
|
1760
|
+
finally {
|
|
1761
|
+
this.conversationLoadingState.set(false);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
async deleteConversation(conversationId) {
|
|
1765
|
+
if (this.streamingState() && this.activeConversationState()?.id === conversationId) {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
try {
|
|
1769
|
+
await firstValueFrom(this.api.deleteConversation(conversationId), { defaultValue: undefined });
|
|
1770
|
+
this.conversationsState.update(items => items.filter(item => item.id !== conversationId));
|
|
1771
|
+
this.conversationsTotalState.update(total => Math.max(0, total - 1));
|
|
1772
|
+
if (this.activeConversationState()?.id === conversationId) {
|
|
1773
|
+
this.activeConversationState.set(null);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
catch (error) {
|
|
1777
|
+
this.setError(error);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Sends a user message. Creates a conversation with the selected agent when none is
|
|
1782
|
+
* active, shows the message optimistically and applies the streamed events.
|
|
1783
|
+
* Resolves `false` when the message was not accepted.
|
|
1784
|
+
*/
|
|
1785
|
+
async send(text) {
|
|
1786
|
+
const trimmed = text.trim();
|
|
1787
|
+
const maxChars = this.limits()?.maxMessageChars ?? Number.MAX_SAFE_INTEGER;
|
|
1788
|
+
if (trimmed.length === 0 || trimmed.length > maxChars || !this.canSend()) {
|
|
1789
|
+
return false;
|
|
1790
|
+
}
|
|
1791
|
+
this.errorState.set(null);
|
|
1792
|
+
this.restoredDraftState.set(null);
|
|
1793
|
+
this.streamingState.set(true);
|
|
1794
|
+
let conversation = this.activeConversationState();
|
|
1795
|
+
if (!conversation) {
|
|
1796
|
+
conversation = await this.createConversation();
|
|
1797
|
+
if (!conversation) {
|
|
1798
|
+
this.streamingState.set(false);
|
|
1799
|
+
this.restoredDraftState.set(text);
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
const clientContext = await this.contextForMessage();
|
|
1804
|
+
const clientMessageId = createClientMessageId();
|
|
1805
|
+
const previousStatus = conversation.status;
|
|
1806
|
+
this.activeConversationState.set({
|
|
1807
|
+
...conversation,
|
|
1808
|
+
status: 'running',
|
|
1809
|
+
messages: [
|
|
1810
|
+
...conversation.messages,
|
|
1811
|
+
{ id: clientMessageId, role: 'user', createdAt: new Date().toISOString(), parts: [{ type: 'text', text: trimmed }] }
|
|
1812
|
+
]
|
|
1813
|
+
});
|
|
1814
|
+
const conversationId = conversation.id;
|
|
1815
|
+
return new Promise(resolve => {
|
|
1816
|
+
let started = false;
|
|
1817
|
+
let failedAfterStart = false;
|
|
1818
|
+
this.runStream(this.api.sendMessage(conversationId, {
|
|
1819
|
+
text: trimmed,
|
|
1820
|
+
clientMessageId,
|
|
1821
|
+
...(clientContext === undefined ? {} : { clientContext })
|
|
1822
|
+
}), {
|
|
1823
|
+
next: event => {
|
|
1824
|
+
if (event.type === 'error' && !started) {
|
|
1825
|
+
this.removeMessage(clientMessageId, previousStatus);
|
|
1826
|
+
this.restoredDraftState.set(text);
|
|
1827
|
+
this.errorState.set(event.data);
|
|
1828
|
+
resolve(false);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
if (event.type === 'turn.started') {
|
|
1832
|
+
started = true;
|
|
1833
|
+
resolve(true);
|
|
1834
|
+
}
|
|
1835
|
+
if (event.type === 'error') {
|
|
1836
|
+
failedAfterStart = true;
|
|
1837
|
+
}
|
|
1838
|
+
this.applyEvent(event, clientMessageId);
|
|
1839
|
+
},
|
|
1840
|
+
complete: () => {
|
|
1841
|
+
resolve(started);
|
|
1842
|
+
this.afterTurn(conversationId, failedAfterStart);
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
/** Approves or rejects the pending approval. Ignores repeated calls while a decision is in flight. */
|
|
1848
|
+
decide(decision, reason) {
|
|
1849
|
+
const conversation = this.activeConversationState();
|
|
1850
|
+
const approval = conversation?.pendingApproval ?? null;
|
|
1851
|
+
if (!conversation || !approval || this.decidingState() || this.streamingState()) {
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
this.decidingState.set(true);
|
|
1855
|
+
this.streamingState.set(true);
|
|
1856
|
+
this.errorState.set(null);
|
|
1857
|
+
let accepted = false;
|
|
1858
|
+
let failed = false;
|
|
1859
|
+
const request = { decision, expectedProposalHash: approval.proposalHash, ...(reason ? { reason } : {}) };
|
|
1860
|
+
this.runStream(this.api.decideApproval(conversation.id, approval.approvalId, request), {
|
|
1861
|
+
next: event => {
|
|
1862
|
+
if (!accepted && event.type !== 'error') {
|
|
1863
|
+
accepted = true;
|
|
1864
|
+
this.updateActive(current => applyNhAssistantApprovalDecision(current, approval.approvalId, decision));
|
|
1865
|
+
}
|
|
1866
|
+
if (event.type === 'error') {
|
|
1867
|
+
failed = true;
|
|
1868
|
+
if (!accepted) {
|
|
1869
|
+
this.errorState.set(event.data);
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
this.applyEvent(event);
|
|
1874
|
+
},
|
|
1875
|
+
complete: () => {
|
|
1876
|
+
this.decidingState.set(false);
|
|
1877
|
+
this.afterTurn(conversation.id, failed);
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
/** Asks the server to stop the running turn or to abandon a waiting approval. */
|
|
1882
|
+
async cancel() {
|
|
1883
|
+
const conversation = this.activeConversationState();
|
|
1884
|
+
if (!conversation || (conversation.status === 'idle' && !this.streamingState())) {
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
try {
|
|
1888
|
+
await firstValueFrom(this.api.cancel(conversation.id), { defaultValue: undefined });
|
|
1889
|
+
}
|
|
1890
|
+
catch (error) {
|
|
1891
|
+
this.setError(error);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
if (!this.streamingState()) {
|
|
1895
|
+
await this.reloadActiveConversation(conversation.id);
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
// The server ends the stream with turn.completed(cancelled); stop waiting if it does not.
|
|
1899
|
+
clearTimeout(this.cancelTimer);
|
|
1900
|
+
this.cancelTimer = setTimeout(() => {
|
|
1901
|
+
if (this.streamingState()) {
|
|
1902
|
+
this.streamSubscription?.unsubscribe();
|
|
1903
|
+
this.streamingState.set(false);
|
|
1904
|
+
this.decidingState.set(false);
|
|
1905
|
+
void this.reloadActiveConversation(conversation.id);
|
|
1906
|
+
}
|
|
1907
|
+
}, cancelGracePeriodMs);
|
|
1908
|
+
}
|
|
1909
|
+
clearError() {
|
|
1910
|
+
this.errorState.set(null);
|
|
1911
|
+
}
|
|
1912
|
+
/** Reads the host's page context again so the chip shows what the next message sends. */
|
|
1913
|
+
async refreshPageContext() {
|
|
1914
|
+
this.pageContextState.set((await this.readPageContext()) ?? null);
|
|
1915
|
+
}
|
|
1916
|
+
/** Leaves the page context out of the next message only, or includes it again. */
|
|
1917
|
+
setPageContextExcluded(excluded) {
|
|
1918
|
+
this.pageContextExcludedState.set(excluded);
|
|
1919
|
+
}
|
|
1920
|
+
/** Returns and clears the draft of a message that was not accepted. */
|
|
1921
|
+
consumeRestoredDraft() {
|
|
1922
|
+
const draft = this.restoredDraftState();
|
|
1923
|
+
this.restoredDraftState.set(null);
|
|
1924
|
+
return draft;
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Page context for one message: `undefined` omits the field (no getter, getter failed or
|
|
1928
|
+
* invalid shape), `null` sends an explicit "none" (no context or left out by the user).
|
|
1929
|
+
*/
|
|
1930
|
+
async contextForMessage() {
|
|
1931
|
+
if (!this.config.getPageContext) {
|
|
1932
|
+
return undefined;
|
|
1933
|
+
}
|
|
1934
|
+
if (this.pageContextExcludedState()) {
|
|
1935
|
+
this.pageContextExcludedState.set(false);
|
|
1936
|
+
return null;
|
|
1937
|
+
}
|
|
1938
|
+
const context = await this.readPageContext();
|
|
1939
|
+
this.pageContextState.set(context ?? null);
|
|
1940
|
+
return context;
|
|
1941
|
+
}
|
|
1942
|
+
async readPageContext() {
|
|
1943
|
+
const getter = this.config.getPageContext;
|
|
1944
|
+
if (!getter) {
|
|
1945
|
+
return undefined;
|
|
1946
|
+
}
|
|
1947
|
+
try {
|
|
1948
|
+
const value = await runInInjectionContext(this.injector, () => getter());
|
|
1949
|
+
if (value === null || value === undefined) {
|
|
1950
|
+
return null;
|
|
1951
|
+
}
|
|
1952
|
+
return normalizeNhAssistantClientContext(value) ?? undefined;
|
|
1953
|
+
}
|
|
1954
|
+
catch {
|
|
1955
|
+
// A failing host getter never blocks sending; the message goes without page context.
|
|
1956
|
+
return undefined;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
async startInitialization() {
|
|
1960
|
+
const decision = this.accessPolicy.canUse();
|
|
1961
|
+
if (!isObservable(decision)) {
|
|
1962
|
+
this.accessGrantedState.set(decision);
|
|
1963
|
+
await this.reloadStatus();
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
await new Promise(resolve => {
|
|
1967
|
+
this.accessSubscription = decision.subscribe({
|
|
1968
|
+
next: granted => {
|
|
1969
|
+
const changed = this.accessGrantedState() !== granted;
|
|
1970
|
+
this.accessGrantedState.set(granted);
|
|
1971
|
+
if (changed) {
|
|
1972
|
+
void this.reloadStatus().finally(() => resolve());
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
resolve();
|
|
1976
|
+
}
|
|
1977
|
+
},
|
|
1978
|
+
error: () => {
|
|
1979
|
+
this.accessGrantedState.set(false);
|
|
1980
|
+
resolve();
|
|
1981
|
+
},
|
|
1982
|
+
complete: () => resolve()
|
|
1983
|
+
});
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
async createConversation() {
|
|
1987
|
+
const agentId = this.selectedAgentIdState();
|
|
1988
|
+
if (!agentId) {
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1991
|
+
try {
|
|
1992
|
+
const conversation = await firstValueFrom(this.api.createConversation({ agentId }));
|
|
1993
|
+
this.activeConversationState.set(conversation);
|
|
1994
|
+
this.conversationsState.update(items => [toSummary(conversation), ...items.filter(item => item.id !== conversation.id)]);
|
|
1995
|
+
this.conversationsTotalState.update(total => total + 1);
|
|
1996
|
+
return conversation;
|
|
1997
|
+
}
|
|
1998
|
+
catch (error) {
|
|
1999
|
+
this.setError(error);
|
|
2000
|
+
return null;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
runStream(stream, handlers) {
|
|
2004
|
+
this.streamSubscription?.unsubscribe();
|
|
2005
|
+
this.streamingState.set(true);
|
|
2006
|
+
const finish = () => {
|
|
2007
|
+
clearTimeout(this.cancelTimer);
|
|
2008
|
+
this.streamingState.set(false);
|
|
2009
|
+
handlers.complete();
|
|
2010
|
+
};
|
|
2011
|
+
this.streamSubscription = stream.subscribe({
|
|
2012
|
+
next: event => handlers.next(event),
|
|
2013
|
+
error: () => {
|
|
2014
|
+
this.errorState.set(clientError(NhAssistantClientErrorCodes.network));
|
|
2015
|
+
finish();
|
|
2016
|
+
},
|
|
2017
|
+
complete: finish
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
applyEvent(event, clientMessageId) {
|
|
2021
|
+
this.updateActive(conversation => applyNhAssistantEvent(conversation, event, clientMessageId));
|
|
2022
|
+
if (event.type === 'error') {
|
|
2023
|
+
this.errorState.set(event.data);
|
|
2024
|
+
}
|
|
2025
|
+
if (event.type === 'turn.completed') {
|
|
2026
|
+
this.lastUsageState.set(event.data.usage);
|
|
2027
|
+
if (event.data.status === 'failed') {
|
|
2028
|
+
const code = event.data.errorCode ?? NhAssistantClientErrorCodes.server;
|
|
2029
|
+
this.errorState.set(clientError(code));
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
afterTurn(conversationId, reload) {
|
|
2034
|
+
const active = this.activeConversationState();
|
|
2035
|
+
if (active?.id === conversationId) {
|
|
2036
|
+
this.conversationsState.update(items => items.map(item => item.id === conversationId ? { ...item, status: active.status, updatedAt: new Date().toISOString() } : item));
|
|
2037
|
+
}
|
|
2038
|
+
if (reload) {
|
|
2039
|
+
void this.reloadActiveConversation(conversationId);
|
|
2040
|
+
}
|
|
2041
|
+
if (!active?.title) {
|
|
2042
|
+
void this.refreshConversations();
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
async reloadActiveConversation(conversationId) {
|
|
2046
|
+
try {
|
|
2047
|
+
const conversation = await firstValueFrom(this.api.getConversation(conversationId));
|
|
2048
|
+
if (this.activeConversationState()?.id === conversationId && !this.streamingState()) {
|
|
2049
|
+
this.activeConversationState.set(conversation);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
catch {
|
|
2053
|
+
// Keep the local state; the error of the turn is already shown.
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
updateActive(update) {
|
|
2057
|
+
const conversation = this.activeConversationState();
|
|
2058
|
+
if (conversation) {
|
|
2059
|
+
this.activeConversationState.set(update(conversation));
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
removeMessage(messageId, status) {
|
|
2063
|
+
this.updateActive(conversation => ({
|
|
2064
|
+
...conversation,
|
|
2065
|
+
status,
|
|
2066
|
+
messages: conversation.messages.filter(message => message.id !== messageId)
|
|
2067
|
+
}));
|
|
2068
|
+
}
|
|
2069
|
+
selectInitialAgent(agents) {
|
|
2070
|
+
const current = this.selectedAgentIdState();
|
|
2071
|
+
if (current && agents.some(agent => agent.id === current)) {
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
const preferred = agents.find(agent => agent.id === this.config.defaultAgentId) ?? agents[0] ?? null;
|
|
2075
|
+
this.selectedAgentIdState.set(preferred?.id ?? null);
|
|
2076
|
+
}
|
|
2077
|
+
setError(error) {
|
|
2078
|
+
if (error instanceof NhAssistantApiError) {
|
|
2079
|
+
this.errorState.set({ code: error.code, messageKey: error.messageKey });
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
this.errorState.set(clientError(NhAssistantClientErrorCodes.server));
|
|
2083
|
+
}
|
|
2084
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2085
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantStore }); }
|
|
2086
|
+
}
|
|
2087
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantStore, decorators: [{
|
|
2088
|
+
type: Injectable
|
|
2089
|
+
}], ctorParameters: () => [] });
|
|
2090
|
+
function clientError(code) {
|
|
2091
|
+
return { code, messageKey: nhAssistantErrorMessageKey(code) };
|
|
2092
|
+
}
|
|
2093
|
+
function toSummary(conversation) {
|
|
2094
|
+
return {
|
|
2095
|
+
id: conversation.id,
|
|
2096
|
+
agentId: conversation.agentId,
|
|
2097
|
+
title: conversation.title,
|
|
2098
|
+
status: conversation.status,
|
|
2099
|
+
createdAt: conversation.createdAt,
|
|
2100
|
+
updatedAt: conversation.updatedAt
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
function createClientMessageId() {
|
|
2104
|
+
const cryptoApi = globalThis.crypto;
|
|
2105
|
+
if (cryptoApi?.randomUUID) {
|
|
2106
|
+
return cryptoApi.randomUUID();
|
|
2107
|
+
}
|
|
2108
|
+
const bytes = new Uint8Array(16);
|
|
2109
|
+
if (cryptoApi?.getRandomValues) {
|
|
2110
|
+
cryptoApi.getRandomValues(bytes);
|
|
2111
|
+
}
|
|
2112
|
+
else {
|
|
2113
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
2114
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
2118
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
2119
|
+
const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
2120
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
/** Drawer width on wider screens; below the mobile breakpoint the panel fills the screen. */
|
|
2124
|
+
const NH_ASSISTANT_PANEL_WIDTH = '420px';
|
|
2125
|
+
const NH_ASSISTANT_PANEL_MOBILE_QUERY = '(max-width: 599.98px)';
|
|
2126
|
+
/**
|
|
2127
|
+
* Opens and closes the assistant panel as a CDK overlay drawer on the right edge.
|
|
2128
|
+
* `nh-assistant-panel` registers its content once; any component can then open the panel,
|
|
2129
|
+
* optionally on a specific conversation. Escape closes the panel and focus returns to the
|
|
2130
|
+
* element that opened it.
|
|
2131
|
+
*/
|
|
2132
|
+
class NhAssistantPanelService {
|
|
2133
|
+
constructor() {
|
|
2134
|
+
this.overlay = inject(Overlay);
|
|
2135
|
+
this.store = inject(NhAssistantStore);
|
|
2136
|
+
this.injector = inject(Injector);
|
|
2137
|
+
this.document = inject(DOCUMENT);
|
|
2138
|
+
this.breakpoints = inject(BreakpointObserver);
|
|
2139
|
+
this.openState = signal(false, ...(ngDevMode ? [{ debugName: "openState" }] : []));
|
|
2140
|
+
this.subscriptions = new Subscription();
|
|
2141
|
+
this.returnFocusTo = null;
|
|
2142
|
+
this.isOpen = this.openState.asReadonly();
|
|
2143
|
+
inject(DestroyRef).onDestroy(() => this.dispose());
|
|
2144
|
+
}
|
|
2145
|
+
open(conversationId) {
|
|
2146
|
+
void this.store.initialize();
|
|
2147
|
+
if (conversationId) {
|
|
2148
|
+
void this.store.openConversation(conversationId);
|
|
2149
|
+
}
|
|
2150
|
+
if (this.openState()) {
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
const active = this.document.activeElement;
|
|
2154
|
+
this.returnFocusTo = active instanceof HTMLElement ? active : null;
|
|
2155
|
+
this.openState.set(true);
|
|
2156
|
+
this.attach();
|
|
2157
|
+
}
|
|
2158
|
+
close() {
|
|
2159
|
+
if (!this.openState()) {
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
this.openState.set(false);
|
|
2163
|
+
this.overlayRef?.detach();
|
|
2164
|
+
const target = this.returnFocusTo;
|
|
2165
|
+
this.returnFocusTo = null;
|
|
2166
|
+
if (target?.isConnected) {
|
|
2167
|
+
target.focus();
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
toggle() {
|
|
2171
|
+
if (this.openState()) {
|
|
2172
|
+
this.close();
|
|
2173
|
+
}
|
|
2174
|
+
else {
|
|
2175
|
+
this.open();
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
/** Called by `nh-assistant-panel`; hosts should place that component once instead. */
|
|
2179
|
+
registerPanel(portal) {
|
|
2180
|
+
this.portal = portal;
|
|
2181
|
+
if (this.openState()) {
|
|
2182
|
+
this.attach();
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
unregisterPanel(portal) {
|
|
2186
|
+
if (this.portal !== portal) {
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
this.overlayRef?.detach();
|
|
2190
|
+
this.portal = undefined;
|
|
2191
|
+
}
|
|
2192
|
+
attach() {
|
|
2193
|
+
if (!this.portal || this.overlayRef?.hasAttached()) {
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
const overlayRef = this.ensureOverlay();
|
|
2197
|
+
overlayRef.attach(this.portal);
|
|
2198
|
+
this.updateSize(this.breakpoints.isMatched(NH_ASSISTANT_PANEL_MOBILE_QUERY));
|
|
2199
|
+
afterNextRender(() => {
|
|
2200
|
+
const target = overlayRef.overlayElement.querySelector('[data-nh-assistant-autofocus]');
|
|
2201
|
+
target?.focus();
|
|
2202
|
+
}, { injector: this.injector });
|
|
2203
|
+
}
|
|
2204
|
+
ensureOverlay() {
|
|
2205
|
+
if (this.overlayRef) {
|
|
2206
|
+
return this.overlayRef;
|
|
2207
|
+
}
|
|
2208
|
+
const overlayRef = this.overlay.create({
|
|
2209
|
+
positionStrategy: this.overlay.position().global().top('0').right('0'),
|
|
2210
|
+
scrollStrategy: this.overlay.scrollStrategies.noop(),
|
|
2211
|
+
hasBackdrop: false,
|
|
2212
|
+
panelClass: 'nh-assistant-overlay-pane',
|
|
2213
|
+
height: '100%',
|
|
2214
|
+
width: NH_ASSISTANT_PANEL_WIDTH,
|
|
2215
|
+
maxWidth: '100vw'
|
|
2216
|
+
});
|
|
2217
|
+
this.subscriptions.add(overlayRef.keydownEvents().subscribe(event => {
|
|
2218
|
+
if (event.key === 'Escape') {
|
|
2219
|
+
event.preventDefault();
|
|
2220
|
+
this.close();
|
|
2221
|
+
}
|
|
2222
|
+
}));
|
|
2223
|
+
this.subscriptions.add(this.breakpoints.observe(NH_ASSISTANT_PANEL_MOBILE_QUERY).subscribe(state => {
|
|
2224
|
+
this.updateSize(state.matches);
|
|
2225
|
+
}));
|
|
2226
|
+
this.overlayRef = overlayRef;
|
|
2227
|
+
return overlayRef;
|
|
2228
|
+
}
|
|
2229
|
+
updateSize(mobile) {
|
|
2230
|
+
this.overlayRef?.updateSize({ width: mobile ? '100vw' : NH_ASSISTANT_PANEL_WIDTH, height: '100%' });
|
|
2231
|
+
}
|
|
2232
|
+
dispose() {
|
|
2233
|
+
this.subscriptions.unsubscribe();
|
|
2234
|
+
this.overlayRef?.dispose();
|
|
2235
|
+
this.overlayRef = undefined;
|
|
2236
|
+
}
|
|
2237
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantPanelService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2238
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantPanelService }); }
|
|
2239
|
+
}
|
|
2240
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantPanelService, decorators: [{
|
|
2241
|
+
type: Injectable
|
|
2242
|
+
}], ctorParameters: () => [] });
|
|
2243
|
+
|
|
2244
|
+
/**
|
|
2245
|
+
* Registers the assistant client, store and panel for one application or route scope.
|
|
2246
|
+
* Call it once in the root providers; a route may call it again to host an isolated
|
|
2247
|
+
* assistant with its own configuration.
|
|
2248
|
+
*/
|
|
2249
|
+
function provideNhAssistant(config) {
|
|
2250
|
+
return makeEnvironmentProviders([
|
|
2251
|
+
{ provide: NH_ASSISTANT_CONFIG, useValue: config },
|
|
2252
|
+
{ provide: NH_ASSISTANT_ACCESS_POLICY, useClass: config.accessPolicy ?? NhAssistantAllowAllAccessPolicy },
|
|
2253
|
+
NhAssistantTransport,
|
|
2254
|
+
NhAssistantApiService,
|
|
2255
|
+
NhAssistantAdminApiService,
|
|
2256
|
+
NhAssistantStore,
|
|
2257
|
+
NhAssistantPanelService,
|
|
2258
|
+
...(config.translations === 'host'
|
|
2259
|
+
? []
|
|
2260
|
+
: [provideEnvironmentInitializer(() => inject(NhAssistantTranslationMerger).start())])
|
|
2261
|
+
]);
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
/**
|
|
2265
|
+
* Optional icon classes of the host's icon library, for example `{ close: 'ph ph-x' }`.
|
|
2266
|
+
* Icons without a class use the library's small built-in outline glyphs.
|
|
2267
|
+
*/
|
|
2268
|
+
const NH_ASSISTANT_ICONS = new InjectionToken('NH_ASSISTANT_ICONS');
|
|
2269
|
+
const builtInPaths = {
|
|
2270
|
+
assistant: 'M12 3l1.8 4.7 4.7 1.8-4.7 1.8L12 16l-1.8-4.7-4.7-1.8 4.7-1.8zM18.5 15l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8z',
|
|
2271
|
+
close: 'M6 6l12 12M18 6L6 18',
|
|
2272
|
+
send: 'M4 12l16-8-6 16-2.5-6.5zM11.5 13.5L20 4',
|
|
2273
|
+
stop: 'M7 7h10v10H7z',
|
|
2274
|
+
plus: 'M12 5v14M5 12h14',
|
|
2275
|
+
list: 'M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01',
|
|
2276
|
+
trash: 'M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3',
|
|
2277
|
+
tool: 'M14.7 6.3a4 4 0 0 0-5.4 5.4L4 17l3 3 5.3-5.3a4 4 0 0 0 5.4-5.4l-2.5 2.5-2.1-.4-.4-2.1z',
|
|
2278
|
+
check: 'M5 12.5l4.5 4.5L19 7.5',
|
|
2279
|
+
x: 'M7 7l10 10M17 7L7 17',
|
|
2280
|
+
warning: 'M12 4L2.5 20h19zM12 10v4M12 17h.01',
|
|
2281
|
+
'chevron-down': 'M6 9l6 6 6-6',
|
|
2282
|
+
clock: 'M12 3a9 9 0 1 0 0 18 9 9 0 1 0 0-18zM12 7v5l3 2',
|
|
2283
|
+
settings: 'M4 7h9M17 7h3M4 17h3M11 17h9M15 5v4M9 15v4',
|
|
2284
|
+
admin: 'M12 3l7 3v5c0 4.5-3 8.2-7 10-4-1.8-7-5.5-7-10V6zM9.5 12l2 2 3.5-3.5',
|
|
2285
|
+
edit: 'M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4',
|
|
2286
|
+
refresh: 'M20 11a8 8 0 0 0-14.3-4.9L4 8M4 4v4h4M4 13a8 8 0 0 0 14.3 4.9L20 16M20 20v-4h-4',
|
|
2287
|
+
link: 'M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1',
|
|
2288
|
+
page: 'M6 3h8l4 4v14H6zM14 3v4h4M9 12h6M9 16h6'
|
|
2289
|
+
};
|
|
2290
|
+
/** Decorative icon. The surrounding control provides the accessible name. */
|
|
2291
|
+
class NhAssistantIconComponent {
|
|
2292
|
+
constructor() {
|
|
2293
|
+
this.icons = inject(NH_ASSISTANT_ICONS, { optional: true });
|
|
2294
|
+
this.name = input.required(...(ngDevMode ? [{ debugName: "name" }] : []));
|
|
2295
|
+
this.iconClass = computed(() => this.icons?.[this.name()] ?? null, ...(ngDevMode ? [{ debugName: "iconClass" }] : []));
|
|
2296
|
+
this.path = computed(() => builtInPaths[this.name()], ...(ngDevMode ? [{ debugName: "path" }] : []));
|
|
2297
|
+
}
|
|
2298
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2299
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhAssistantIconComponent, isStandalone: true, selector: "nh-assistant-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
|
|
2300
|
+
@if (iconClass(); as iconClass) {
|
|
2301
|
+
<i [class]="iconClass" aria-hidden="true"></i>
|
|
2302
|
+
} @else {
|
|
2303
|
+
<svg viewBox="0 0 24 24" width="1em" height="1em" aria-hidden="true" focusable="false">
|
|
2304
|
+
<path [attr.d]="path()" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
2305
|
+
</svg>
|
|
2306
|
+
}
|
|
2307
|
+
`, isInline: true, styles: [":host{display:inline-flex;align-items:center;justify-content:center;line-height:1;font-size:1.15em}svg{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2308
|
+
}
|
|
2309
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantIconComponent, decorators: [{
|
|
2310
|
+
type: Component,
|
|
2311
|
+
args: [{ selector: 'nh-assistant-icon', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
2312
|
+
@if (iconClass(); as iconClass) {
|
|
2313
|
+
<i [class]="iconClass" aria-hidden="true"></i>
|
|
2314
|
+
} @else {
|
|
2315
|
+
<svg viewBox="0 0 24 24" width="1em" height="1em" aria-hidden="true" focusable="false">
|
|
2316
|
+
<path [attr.d]="path()" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
2317
|
+
</svg>
|
|
2318
|
+
}
|
|
2319
|
+
`, styles: [":host{display:inline-flex;align-items:center;justify-content:center;line-height:1;font-size:1.15em}svg{display:block}\n"] }]
|
|
2320
|
+
}], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }] } });
|
|
2321
|
+
|
|
2322
|
+
/**
|
|
2323
|
+
* Header button that opens the assistant panel. It renders nothing while the assistant is
|
|
2324
|
+
* disabled on the server or the access policy denies the user. A badge marks a conversation
|
|
2325
|
+
* that waits for approval. Projected content replaces the default icon.
|
|
2326
|
+
*/
|
|
2327
|
+
class NhAssistantLauncherComponent {
|
|
2328
|
+
constructor() {
|
|
2329
|
+
this.store = inject(NhAssistantStore);
|
|
2330
|
+
this.panel = inject(NhAssistantPanelService);
|
|
2331
|
+
this.waitingForApproval = computed(() => this.store.pendingApproval() !== null ||
|
|
2332
|
+
this.store.conversations().some(conversation => conversation.status === 'waiting-for-approval'), ...(ngDevMode ? [{ debugName: "waitingForApproval" }] : []));
|
|
2333
|
+
this.label = computed(() => this.panel.isOpen() ? 'nh-assistant.launcher.close' : 'nh-assistant.launcher.open', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
2334
|
+
// Browser only: the status request and access policy need the signed-in user.
|
|
2335
|
+
afterNextRender(() => {
|
|
2336
|
+
void this.store.initialize();
|
|
2337
|
+
});
|
|
2338
|
+
effect(() => {
|
|
2339
|
+
if (this.store.enabled()) {
|
|
2340
|
+
untracked(() => void this.store.refreshConversations());
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantLauncherComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2345
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhAssistantLauncherComponent, isStandalone: true, selector: "nh-assistant-launcher", ngImport: i0, template: `
|
|
2346
|
+
@if (store.enabled()) {
|
|
2347
|
+
<button
|
|
2348
|
+
class="launcher"
|
|
2349
|
+
type="button"
|
|
2350
|
+
[class.open]="panel.isOpen()"
|
|
2351
|
+
[attr.aria-expanded]="panel.isOpen()"
|
|
2352
|
+
[attr.aria-label]="label() | translate"
|
|
2353
|
+
[attr.title]="label() | translate"
|
|
2354
|
+
(click)="panel.toggle()">
|
|
2355
|
+
<ng-content><nh-assistant-icon name="assistant" /></ng-content>
|
|
2356
|
+
@if (waitingForApproval()) {
|
|
2357
|
+
<span class="badge" aria-hidden="true"></span>
|
|
2358
|
+
<span class="visually-hidden">{{ 'nh-assistant.launcher.pending-approval' | translate }}</span>
|
|
2359
|
+
}
|
|
2360
|
+
</button>
|
|
2361
|
+
}
|
|
2362
|
+
`, isInline: true, styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:inline-flex}.launcher{position:relative;display:inline-flex;align-items:center;justify-content:center;width:var(--nh-assistant-launcher-size, 40px);height:var(--nh-assistant-launcher-size, 40px);border:1px solid var(--nh-assistant-launcher-border, var(--nha-border));border-radius:var(--nha-radius-control);padding:0;color:var(--nh-assistant-launcher-color, var(--nha-text));background:var(--nh-assistant-launcher-background, transparent);font-size:18px;cursor:pointer}.launcher:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.launcher:hover,.launcher.open{border-color:var(--nha-highlight);color:var(--nha-highlight)}.badge{position:absolute;top:6px;right:6px;width:9px;height:9px;border:2px solid var(--nh-assistant-launcher-badge-ring, var(--nha-background));border-radius:50%;background:var(--nha-highlight)}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}\n"], dependencies: [{ kind: "component", type: NhAssistantIconComponent, selector: "nh-assistant-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2363
|
+
}
|
|
2364
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantLauncherComponent, decorators: [{
|
|
2365
|
+
type: Component,
|
|
2366
|
+
args: [{ selector: 'nh-assistant-launcher', standalone: true, imports: [TranslatePipe, NhAssistantIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
2367
|
+
@if (store.enabled()) {
|
|
2368
|
+
<button
|
|
2369
|
+
class="launcher"
|
|
2370
|
+
type="button"
|
|
2371
|
+
[class.open]="panel.isOpen()"
|
|
2372
|
+
[attr.aria-expanded]="panel.isOpen()"
|
|
2373
|
+
[attr.aria-label]="label() | translate"
|
|
2374
|
+
[attr.title]="label() | translate"
|
|
2375
|
+
(click)="panel.toggle()">
|
|
2376
|
+
<ng-content><nh-assistant-icon name="assistant" /></ng-content>
|
|
2377
|
+
@if (waitingForApproval()) {
|
|
2378
|
+
<span class="badge" aria-hidden="true"></span>
|
|
2379
|
+
<span class="visually-hidden">{{ 'nh-assistant.launcher.pending-approval' | translate }}</span>
|
|
2380
|
+
}
|
|
2381
|
+
</button>
|
|
2382
|
+
}
|
|
2383
|
+
`, styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:inline-flex}.launcher{position:relative;display:inline-flex;align-items:center;justify-content:center;width:var(--nh-assistant-launcher-size, 40px);height:var(--nh-assistant-launcher-size, 40px);border:1px solid var(--nh-assistant-launcher-border, var(--nha-border));border-radius:var(--nha-radius-control);padding:0;color:var(--nh-assistant-launcher-color, var(--nha-text));background:var(--nh-assistant-launcher-background, transparent);font-size:18px;cursor:pointer}.launcher:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.launcher:hover,.launcher.open{border-color:var(--nha-highlight);color:var(--nha-highlight)}.badge{position:absolute;top:6px;right:6px;width:9px;height:9px;border:2px solid var(--nh-assistant-launcher-badge-ring, var(--nha-background));border-radius:50%;background:var(--nha-highlight)}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}\n"] }]
|
|
2384
|
+
}], ctorParameters: () => [] });
|
|
2385
|
+
|
|
2386
|
+
/**
|
|
2387
|
+
* Translates the first key that has a translation and otherwise returns the fallback text.
|
|
2388
|
+
* Used for server-provided keys (agent names, error message keys, result codes) that the
|
|
2389
|
+
* host may or may not translate.
|
|
2390
|
+
*/
|
|
2391
|
+
class NhAssistantTranslatePipe {
|
|
2392
|
+
constructor() {
|
|
2393
|
+
this.translate = inject(TranslateService);
|
|
2394
|
+
this.lastKeys = '';
|
|
2395
|
+
this.lastParams = '';
|
|
2396
|
+
this.lastValue = '';
|
|
2397
|
+
this.dirty = true;
|
|
2398
|
+
const changeDetector = inject(ChangeDetectorRef);
|
|
2399
|
+
const subscription = merge(this.translate.onTranslationChange, this.translate.onLangChange).subscribe(() => {
|
|
2400
|
+
this.dirty = true;
|
|
2401
|
+
changeDetector.markForCheck();
|
|
2402
|
+
});
|
|
2403
|
+
inject(DestroyRef).onDestroy(() => subscription.unsubscribe());
|
|
2404
|
+
}
|
|
2405
|
+
transform(keys, fallback = '', params) {
|
|
2406
|
+
const candidates = (Array.isArray(keys) ? keys : [keys]).filter((key) => !!key);
|
|
2407
|
+
const keySignature = candidates.join('|');
|
|
2408
|
+
const paramSignature = params ? JSON.stringify(params) : '';
|
|
2409
|
+
if (!this.dirty && keySignature === this.lastKeys && paramSignature === this.lastParams) {
|
|
2410
|
+
return this.lastValue;
|
|
2411
|
+
}
|
|
2412
|
+
this.lastKeys = keySignature;
|
|
2413
|
+
this.lastParams = paramSignature;
|
|
2414
|
+
this.dirty = false;
|
|
2415
|
+
this.lastValue = this.resolve(candidates, fallback, params);
|
|
2416
|
+
return this.lastValue;
|
|
2417
|
+
}
|
|
2418
|
+
resolve(candidates, fallback, params) {
|
|
2419
|
+
for (const key of candidates) {
|
|
2420
|
+
const value = this.translate.instant(key, params);
|
|
2421
|
+
if (typeof value === 'string' && value.length > 0 && value !== key) {
|
|
2422
|
+
return value;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
return fallback;
|
|
2426
|
+
}
|
|
2427
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantTranslatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
2428
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantTranslatePipe, isStandalone: true, name: "nhAssistantTranslate", pure: false }); }
|
|
2429
|
+
}
|
|
2430
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantTranslatePipe, decorators: [{
|
|
2431
|
+
type: Pipe,
|
|
2432
|
+
args: [{ name: 'nhAssistantTranslate', standalone: true, pure: false }]
|
|
2433
|
+
}], ctorParameters: () => [] });
|
|
2434
|
+
|
|
2435
|
+
let nextId$4 = 0;
|
|
2436
|
+
/**
|
|
2437
|
+
* Chooses the agent for new conversations. Agent names come from the host's translation
|
|
2438
|
+
* keys; agents created by an administrator carry literal text, which is shown as is.
|
|
2439
|
+
*/
|
|
2440
|
+
class NhAssistantAgentPickerComponent {
|
|
2441
|
+
constructor() {
|
|
2442
|
+
this.agents = input.required(...(ngDevMode ? [{ debugName: "agents" }] : []));
|
|
2443
|
+
this.selectedAgentId = input(null, ...(ngDevMode ? [{ debugName: "selectedAgentId" }] : []));
|
|
2444
|
+
this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
2445
|
+
this.agentChange = output();
|
|
2446
|
+
this.selectId = `nh-assistant-agent-picker-${nextId$4++}`;
|
|
2447
|
+
this.selected = computed(() => this.agents().find(agent => agent.id === this.selectedAgentId()) ?? null, ...(ngDevMode ? [{ debugName: "selected" }] : []));
|
|
2448
|
+
}
|
|
2449
|
+
onChange(event) {
|
|
2450
|
+
this.agentChange.emit(event.target.value);
|
|
2451
|
+
}
|
|
2452
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantAgentPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2453
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhAssistantAgentPickerComponent, isStandalone: true, selector: "nh-assistant-agent-picker", inputs: { agents: { classPropertyName: "agents", publicName: "agents", isSignal: true, isRequired: true, transformFunction: null }, selectedAgentId: { classPropertyName: "selectedAgentId", publicName: "selectedAgentId", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { agentChange: "agentChange" }, ngImport: i0, template: `
|
|
2454
|
+
<label class="visually-hidden" [for]="selectId">{{ 'nh-assistant.agent-picker.label' | translate }}</label>
|
|
2455
|
+
<select
|
|
2456
|
+
[id]="selectId"
|
|
2457
|
+
[disabled]="disabled() || agents().length < 2"
|
|
2458
|
+
[attr.title]="selected() ? ([selected()!.descriptionKey] | nhAssistantTranslate: selected()!.descriptionKey) : null"
|
|
2459
|
+
(change)="onChange($event)">
|
|
2460
|
+
@for (agent of agents(); track agent.id) {
|
|
2461
|
+
<option [value]="agent.id" [selected]="agent.id === selectedAgentId()">
|
|
2462
|
+
{{ [agent.displayNameKey] | nhAssistantTranslate: agent.displayNameKey }}
|
|
2463
|
+
</option>
|
|
2464
|
+
}
|
|
2465
|
+
</select>
|
|
2466
|
+
@if (selected(); as agent) {
|
|
2467
|
+
<span class="capability" [attr.data-can-mutate]="agent.canMutate">
|
|
2468
|
+
{{ (agent.canMutate ? 'nh-assistant.agent-picker.can-mutate' : 'nh-assistant.agent-picker.read-only') | translate }}
|
|
2469
|
+
</span>
|
|
2470
|
+
}
|
|
2471
|
+
`, isInline: true, styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:flex;flex-direction:column;gap:2px;min-width:0}select{align-self:flex-start;max-width:100%;min-height:32px;border:1px solid var(--nha-border);border-radius:var(--nha-radius-control);padding:4px 28px 4px 10px;color:var(--nha-text);background:var(--nha-background);font:inherit;font-size:13px;font-weight:650}select:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}select:disabled{opacity:1;cursor:default}.capability{padding-left:2px;color:var(--nha-text-muted);font-size:11px}.capability[data-can-mutate=true]{color:var(--nha-warning)}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "pipe", type: NhAssistantTranslatePipe, name: "nhAssistantTranslate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2472
|
+
}
|
|
2473
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantAgentPickerComponent, decorators: [{
|
|
2474
|
+
type: Component,
|
|
2475
|
+
args: [{ selector: 'nh-assistant-agent-picker', standalone: true, imports: [TranslatePipe, NhAssistantTranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
2476
|
+
<label class="visually-hidden" [for]="selectId">{{ 'nh-assistant.agent-picker.label' | translate }}</label>
|
|
2477
|
+
<select
|
|
2478
|
+
[id]="selectId"
|
|
2479
|
+
[disabled]="disabled() || agents().length < 2"
|
|
2480
|
+
[attr.title]="selected() ? ([selected()!.descriptionKey] | nhAssistantTranslate: selected()!.descriptionKey) : null"
|
|
2481
|
+
(change)="onChange($event)">
|
|
2482
|
+
@for (agent of agents(); track agent.id) {
|
|
2483
|
+
<option [value]="agent.id" [selected]="agent.id === selectedAgentId()">
|
|
2484
|
+
{{ [agent.displayNameKey] | nhAssistantTranslate: agent.displayNameKey }}
|
|
2485
|
+
</option>
|
|
2486
|
+
}
|
|
2487
|
+
</select>
|
|
2488
|
+
@if (selected(); as agent) {
|
|
2489
|
+
<span class="capability" [attr.data-can-mutate]="agent.canMutate">
|
|
2490
|
+
{{ (agent.canMutate ? 'nh-assistant.agent-picker.can-mutate' : 'nh-assistant.agent-picker.read-only') | translate }}
|
|
2491
|
+
</span>
|
|
2492
|
+
}
|
|
2493
|
+
`, styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:flex;flex-direction:column;gap:2px;min-width:0}select{align-self:flex-start;max-width:100%;min-height:32px;border:1px solid var(--nha-border);border-radius:var(--nha-radius-control);padding:4px 28px 4px 10px;color:var(--nha-text);background:var(--nha-background);font:inherit;font-size:13px;font-weight:650}select:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}select:disabled{opacity:1;cursor:default}.capability{padding-left:2px;color:var(--nha-text-muted);font-size:11px}.capability[data-can-mutate=true]{color:var(--nha-warning)}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}\n"] }]
|
|
2494
|
+
}], propDecorators: { agents: [{ type: i0.Input, args: [{ isSignal: true, alias: "agents", required: true }] }], selectedAgentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedAgentId", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], agentChange: [{ type: i0.Output, args: ["agentChange"] }] } });
|
|
2495
|
+
|
|
2496
|
+
let nextId$3 = 0;
|
|
2497
|
+
/**
|
|
2498
|
+
* Message input. Enter sends, Shift+Enter inserts a new line. While the assistant runs
|
|
2499
|
+
* the send button becomes a stop button.
|
|
2500
|
+
*/
|
|
2501
|
+
class NhAssistantComposerComponent {
|
|
2502
|
+
constructor() {
|
|
2503
|
+
/** Disables typing and sending, for example while a turn runs or waits for approval. */
|
|
2504
|
+
this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
2505
|
+
/** Shows the stop button instead of the send button. */
|
|
2506
|
+
this.busy = input(false, ...(ngDevMode ? [{ debugName: "busy" }] : []));
|
|
2507
|
+
this.maxLength = input(null, ...(ngDevMode ? [{ debugName: "maxLength" }] : []));
|
|
2508
|
+
/** Text to put back into the input, for example a message the server did not accept. */
|
|
2509
|
+
this.restore = input(null, ...(ngDevMode ? [{ debugName: "restore" }] : []));
|
|
2510
|
+
this.send = output();
|
|
2511
|
+
this.cancel = output();
|
|
2512
|
+
this.restored = output();
|
|
2513
|
+
this.inputId = `nh-assistant-composer-${nextId$3++}`;
|
|
2514
|
+
this.hintId = `${this.inputId}-hint`;
|
|
2515
|
+
this.text = signal('', ...(ngDevMode ? [{ debugName: "text" }] : []));
|
|
2516
|
+
this.length = computed(() => this.text().trim().length, ...(ngDevMode ? [{ debugName: "length" }] : []));
|
|
2517
|
+
this.tooLong = computed(() => {
|
|
2518
|
+
const max = this.maxLength();
|
|
2519
|
+
return max !== null && this.length() > max;
|
|
2520
|
+
}, ...(ngDevMode ? [{ debugName: "tooLong" }] : []));
|
|
2521
|
+
this.showCounter = computed(() => {
|
|
2522
|
+
const max = this.maxLength();
|
|
2523
|
+
return max !== null && this.length() >= max * 0.8;
|
|
2524
|
+
}, ...(ngDevMode ? [{ debugName: "showCounter" }] : []));
|
|
2525
|
+
this.canSend = computed(() => !this.disabled() && this.length() > 0 && !this.tooLong(), ...(ngDevMode ? [{ debugName: "canSend" }] : []));
|
|
2526
|
+
effect(() => {
|
|
2527
|
+
const draft = this.restore();
|
|
2528
|
+
if (draft !== null) {
|
|
2529
|
+
untracked(() => {
|
|
2530
|
+
this.text.set(draft);
|
|
2531
|
+
this.restored.emit();
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
onInput(event) {
|
|
2537
|
+
this.text.set(event.target.value);
|
|
2538
|
+
}
|
|
2539
|
+
onKeydown(event) {
|
|
2540
|
+
if (event.key !== 'Enter' || event.shiftKey || event.isComposing) {
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
event.preventDefault();
|
|
2544
|
+
this.submit();
|
|
2545
|
+
}
|
|
2546
|
+
submit() {
|
|
2547
|
+
if (!this.canSend()) {
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
const text = this.text().trim();
|
|
2551
|
+
this.text.set('');
|
|
2552
|
+
this.send.emit(text);
|
|
2553
|
+
}
|
|
2554
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2555
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhAssistantComposerComponent, isStandalone: true, selector: "nh-assistant-composer", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, restore: { classPropertyName: "restore", publicName: "restore", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { send: "send", cancel: "cancel", restored: "restored" }, ngImport: i0, template: "<form class=\"composer\" (submit)=\"$event.preventDefault(); submit()\">\n <label class=\"visually-hidden\" [for]=\"inputId\">{{ 'nh-assistant.composer.label' | translate }}</label>\n <textarea\n data-nh-assistant-autofocus\n rows=\"2\"\n [id]=\"inputId\"\n [value]=\"text()\"\n [disabled]=\"disabled()\"\n [attr.placeholder]=\"'nh-assistant.composer.placeholder' | translate\"\n [attr.aria-describedby]=\"hintId\"\n [attr.aria-invalid]=\"tooLong() ? true : null\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"></textarea>\n\n <div class=\"actions\">\n @if (busy()) {\n <button\n class=\"stop\"\n type=\"button\"\n [attr.aria-label]=\"'nh-assistant.composer.stop' | translate\"\n [attr.title]=\"'nh-assistant.composer.stop' | translate\"\n (click)=\"cancel.emit()\">\n <nh-assistant-icon name=\"stop\" />\n </button>\n } @else {\n <button\n class=\"send\"\n type=\"submit\"\n [disabled]=\"!canSend()\"\n [attr.aria-label]=\"'nh-assistant.composer.send' | translate\"\n [attr.title]=\"'nh-assistant.composer.send' | translate\">\n <nh-assistant-icon name=\"send\" />\n </button>\n }\n </div>\n</form>\n\n<p class=\"hint\" [id]=\"hintId\">\n @if (tooLong()) {\n <span class=\"error\">{{ 'nh-assistant.composer.too-long' | translate }}</span>\n } @else {\n <span>{{ 'nh-assistant.composer.hint' | translate }}</span>\n }\n @if (showCounter()) {\n <span class=\"counter\" [class.error]=\"tooLong()\">\n {{ 'nh-assistant.composer.characters' | translate: { count: length(), max: maxLength() } }}\n </span>\n }\n</p>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:block}.composer{display:flex;align-items:flex-end;gap:8px;border:1px solid var(--nha-border);border-radius:var(--nha-radius-control);padding:6px 6px 6px 12px;background:var(--nha-background)}.composer:focus-within{border-color:var(--nha-focus);box-shadow:0 0 0 2px color-mix(in srgb,var(--nha-focus) 30%,transparent)}textarea{flex:1;min-width:0;min-height:44px;max-height:180px;border:0;padding:6px 0;color:var(--nha-text);background:transparent;font:inherit;font-size:14px;line-height:1.45;resize:vertical;outline:none}textarea::placeholder{color:var(--nha-text-muted);opacity:1}textarea:disabled{cursor:not-allowed}.actions{display:flex}.send,.stop{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1px solid transparent;border-radius:var(--nha-radius-control);color:var(--nha-text-muted);background:transparent;cursor:pointer;width:40px;height:40px}.send:focus-visible,.stop:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.send:hover:not(:disabled),.stop:hover:not(:disabled){color:var(--nha-text);background:var(--nha-surface-strong)}.send:disabled,.stop:disabled{cursor:not-allowed;opacity:.5}.send:not(:disabled){color:var(--nha-accent-contrast);background:var(--nha-accent)}.send:not(:disabled):hover{color:var(--nha-accent-contrast);background:var(--nha-accent);filter:brightness(1.12)}.stop{color:var(--nha-danger);border-color:var(--nha-danger)}.hint{display:flex;justify-content:space-between;gap:8px;margin:6px 2px 0;color:var(--nha-text-muted);font-size:11px}.counter{flex:none;font-variant-numeric:tabular-nums}.error{color:var(--nha-danger)}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}\n"], dependencies: [{ kind: "component", type: NhAssistantIconComponent, selector: "nh-assistant-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2556
|
+
}
|
|
2557
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantComposerComponent, decorators: [{
|
|
2558
|
+
type: Component,
|
|
2559
|
+
args: [{ selector: 'nh-assistant-composer', standalone: true, imports: [TranslatePipe, NhAssistantIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<form class=\"composer\" (submit)=\"$event.preventDefault(); submit()\">\n <label class=\"visually-hidden\" [for]=\"inputId\">{{ 'nh-assistant.composer.label' | translate }}</label>\n <textarea\n data-nh-assistant-autofocus\n rows=\"2\"\n [id]=\"inputId\"\n [value]=\"text()\"\n [disabled]=\"disabled()\"\n [attr.placeholder]=\"'nh-assistant.composer.placeholder' | translate\"\n [attr.aria-describedby]=\"hintId\"\n [attr.aria-invalid]=\"tooLong() ? true : null\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"></textarea>\n\n <div class=\"actions\">\n @if (busy()) {\n <button\n class=\"stop\"\n type=\"button\"\n [attr.aria-label]=\"'nh-assistant.composer.stop' | translate\"\n [attr.title]=\"'nh-assistant.composer.stop' | translate\"\n (click)=\"cancel.emit()\">\n <nh-assistant-icon name=\"stop\" />\n </button>\n } @else {\n <button\n class=\"send\"\n type=\"submit\"\n [disabled]=\"!canSend()\"\n [attr.aria-label]=\"'nh-assistant.composer.send' | translate\"\n [attr.title]=\"'nh-assistant.composer.send' | translate\">\n <nh-assistant-icon name=\"send\" />\n </button>\n }\n </div>\n</form>\n\n<p class=\"hint\" [id]=\"hintId\">\n @if (tooLong()) {\n <span class=\"error\">{{ 'nh-assistant.composer.too-long' | translate }}</span>\n } @else {\n <span>{{ 'nh-assistant.composer.hint' | translate }}</span>\n }\n @if (showCounter()) {\n <span class=\"counter\" [class.error]=\"tooLong()\">\n {{ 'nh-assistant.composer.characters' | translate: { count: length(), max: maxLength() } }}\n </span>\n }\n</p>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:block}.composer{display:flex;align-items:flex-end;gap:8px;border:1px solid var(--nha-border);border-radius:var(--nha-radius-control);padding:6px 6px 6px 12px;background:var(--nha-background)}.composer:focus-within{border-color:var(--nha-focus);box-shadow:0 0 0 2px color-mix(in srgb,var(--nha-focus) 30%,transparent)}textarea{flex:1;min-width:0;min-height:44px;max-height:180px;border:0;padding:6px 0;color:var(--nha-text);background:transparent;font:inherit;font-size:14px;line-height:1.45;resize:vertical;outline:none}textarea::placeholder{color:var(--nha-text-muted);opacity:1}textarea:disabled{cursor:not-allowed}.actions{display:flex}.send,.stop{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1px solid transparent;border-radius:var(--nha-radius-control);color:var(--nha-text-muted);background:transparent;cursor:pointer;width:40px;height:40px}.send:focus-visible,.stop:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.send:hover:not(:disabled),.stop:hover:not(:disabled){color:var(--nha-text);background:var(--nha-surface-strong)}.send:disabled,.stop:disabled{cursor:not-allowed;opacity:.5}.send:not(:disabled){color:var(--nha-accent-contrast);background:var(--nha-accent)}.send:not(:disabled):hover{color:var(--nha-accent-contrast);background:var(--nha-accent);filter:brightness(1.12)}.stop{color:var(--nha-danger);border-color:var(--nha-danger)}.hint{display:flex;justify-content:space-between;gap:8px;margin:6px 2px 0;color:var(--nha-text-muted);font-size:11px}.counter{flex:none;font-variant-numeric:tabular-nums}.error{color:var(--nha-danger)}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}\n"] }]
|
|
2560
|
+
}], ctorParameters: () => [], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], restore: [{ type: i0.Input, args: [{ isSignal: true, alias: "restore", required: false }] }], send: [{ type: i0.Output, args: ["send"] }], cancel: [{ type: i0.Output, args: ["cancel"] }], restored: [{ type: i0.Output, args: ["restored"] }] } });
|
|
2561
|
+
|
|
2562
|
+
/** The user's conversations with loading and empty states; selecting one opens it. */
|
|
2563
|
+
class NhAssistantConversationListComponent {
|
|
2564
|
+
constructor() {
|
|
2565
|
+
this.conversations = input.required(...(ngDevMode ? [{ debugName: "conversations" }] : []));
|
|
2566
|
+
this.total = input(0, ...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
2567
|
+
this.activeConversationId = input(null, ...(ngDevMode ? [{ debugName: "activeConversationId" }] : []));
|
|
2568
|
+
this.loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
2569
|
+
this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
2570
|
+
this.conversationSelect = output();
|
|
2571
|
+
this.conversationDelete = output();
|
|
2572
|
+
this.conversationCreate = output();
|
|
2573
|
+
}
|
|
2574
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantConversationListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2575
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhAssistantConversationListComponent, isStandalone: true, selector: "nh-assistant-conversation-list", inputs: { conversations: { classPropertyName: "conversations", publicName: "conversations", isSignal: true, isRequired: true, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: false, transformFunction: null }, activeConversationId: { classPropertyName: "activeConversationId", publicName: "activeConversationId", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { conversationSelect: "conversationSelect", conversationDelete: "conversationDelete", conversationCreate: "conversationCreate" }, ngImport: i0, template: "<nav class=\"list\" [attr.aria-label]=\"'nh-assistant.conversation-list.title' | translate\" [attr.aria-busy]=\"loading()\">\n <header class=\"header\">\n <h3>{{ 'nh-assistant.conversation-list.title' | translate }}</h3>\n @if (conversations().length > 0 && total() > conversations().length) {\n <span class=\"count\">{{ 'nh-assistant.conversation-list.count' | translate: { count: conversations().length, total: total() } }}</span>\n }\n <button\n class=\"icon-button\"\n type=\"button\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"'nh-assistant.panel.new-conversation' | translate\"\n [attr.title]=\"'nh-assistant.panel.new-conversation' | translate\"\n (click)=\"conversationCreate.emit()\">\n <nh-assistant-icon name=\"plus\" />\n </button>\n </header>\n\n @if (loading() && conversations().length === 0) {\n <p class=\"state\" role=\"status\">{{ 'nh-assistant.conversation-list.loading' | translate }}</p>\n } @else if (conversations().length === 0) {\n <p class=\"state\">{{ 'nh-assistant.conversation-list.empty' | translate }}</p>\n } @else {\n <ul>\n @for (conversation of conversations(); track conversation.id) {\n <li [class.active]=\"conversation.id === activeConversationId()\">\n <button\n class=\"item\"\n type=\"button\"\n [disabled]=\"disabled()\"\n [attr.aria-current]=\"conversation.id === activeConversationId() ? 'true' : null\"\n (click)=\"conversationSelect.emit(conversation.id)\">\n <span class=\"title\">{{ conversation.title || ('nh-assistant.conversation-list.untitled' | translate) }}</span>\n <span class=\"meta\">\n <span class=\"status\" [attr.data-status]=\"conversation.status\">{{ 'nh-assistant.conversation-list.status.' + conversation.status | translate }}</span>\n <time [attr.datetime]=\"conversation.updatedAt\">{{ conversation.updatedAt | date: 'short' }}</time>\n </span>\n </button>\n <button\n class=\"icon-button delete\"\n type=\"button\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"('nh-assistant.conversation-list.delete' | translate) + ': ' + (conversation.title || ('nh-assistant.conversation-list.untitled' | translate))\"\n [attr.title]=\"'nh-assistant.conversation-list.delete' | translate\"\n (click)=\"conversationDelete.emit(conversation.id)\">\n <nh-assistant-icon name=\"trash\" />\n </button>\n </li>\n }\n </ul>\n }\n</nav>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:flex;flex-direction:column;min-height:0;color:var(--nha-text)}.list{display:flex;flex:1;flex-direction:column;min-height:0}.header{display:flex;align-items:center;gap:8px;padding:12px 12px 8px 16px}h3{flex:1;margin:0;font-size:13px;font-weight:700}.count{color:var(--nha-text-muted);font-size:11px}.icon-button{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1px solid transparent;border-radius:var(--nha-radius-control);color:var(--nha-text-muted);background:transparent;cursor:pointer}.icon-button:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.icon-button:hover:not(:disabled){color:var(--nha-text);background:var(--nha-surface-strong)}.icon-button:disabled{cursor:not-allowed;opacity:.5}.state{margin:0;padding:12px 16px;color:var(--nha-text-muted);font-size:13px}ul{flex:1;min-height:0;margin:0;overflow-y:auto;padding:0 8px 12px;list-style:none}li{display:flex;align-items:center;gap:4px;border-radius:var(--nha-radius-control)}li.active{background:var(--nha-surface-strong)}.item{display:flex;flex:1;flex-direction:column;gap:2px;min-width:0;border:0;border-radius:var(--nha-radius-control);padding:8px;color:inherit;background:transparent;font:inherit;text-align:left;cursor:pointer}.item:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.item:hover:not(:disabled){background:var(--nha-surface-strong)}.item:disabled{cursor:not-allowed;opacity:.6}.title{overflow:hidden;font-size:13px;font-weight:650;text-overflow:ellipsis;white-space:nowrap}.meta{display:flex;gap:8px;color:var(--nha-text-muted);font-size:11px}.status[data-status=waiting-for-approval]{color:var(--nha-warning);font-weight:700}.status[data-status=failed]{color:var(--nha-danger)}.delete{flex:none}\n"], dependencies: [{ kind: "component", type: NhAssistantIconComponent, selector: "nh-assistant-icon", inputs: ["name"] }, { kind: "pipe", type: DatePipe, name: "date" }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2576
|
+
}
|
|
2577
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantConversationListComponent, decorators: [{
|
|
2578
|
+
type: Component,
|
|
2579
|
+
args: [{ selector: 'nh-assistant-conversation-list', standalone: true, imports: [DatePipe, TranslatePipe, NhAssistantIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<nav class=\"list\" [attr.aria-label]=\"'nh-assistant.conversation-list.title' | translate\" [attr.aria-busy]=\"loading()\">\n <header class=\"header\">\n <h3>{{ 'nh-assistant.conversation-list.title' | translate }}</h3>\n @if (conversations().length > 0 && total() > conversations().length) {\n <span class=\"count\">{{ 'nh-assistant.conversation-list.count' | translate: { count: conversations().length, total: total() } }}</span>\n }\n <button\n class=\"icon-button\"\n type=\"button\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"'nh-assistant.panel.new-conversation' | translate\"\n [attr.title]=\"'nh-assistant.panel.new-conversation' | translate\"\n (click)=\"conversationCreate.emit()\">\n <nh-assistant-icon name=\"plus\" />\n </button>\n </header>\n\n @if (loading() && conversations().length === 0) {\n <p class=\"state\" role=\"status\">{{ 'nh-assistant.conversation-list.loading' | translate }}</p>\n } @else if (conversations().length === 0) {\n <p class=\"state\">{{ 'nh-assistant.conversation-list.empty' | translate }}</p>\n } @else {\n <ul>\n @for (conversation of conversations(); track conversation.id) {\n <li [class.active]=\"conversation.id === activeConversationId()\">\n <button\n class=\"item\"\n type=\"button\"\n [disabled]=\"disabled()\"\n [attr.aria-current]=\"conversation.id === activeConversationId() ? 'true' : null\"\n (click)=\"conversationSelect.emit(conversation.id)\">\n <span class=\"title\">{{ conversation.title || ('nh-assistant.conversation-list.untitled' | translate) }}</span>\n <span class=\"meta\">\n <span class=\"status\" [attr.data-status]=\"conversation.status\">{{ 'nh-assistant.conversation-list.status.' + conversation.status | translate }}</span>\n <time [attr.datetime]=\"conversation.updatedAt\">{{ conversation.updatedAt | date: 'short' }}</time>\n </span>\n </button>\n <button\n class=\"icon-button delete\"\n type=\"button\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"('nh-assistant.conversation-list.delete' | translate) + ': ' + (conversation.title || ('nh-assistant.conversation-list.untitled' | translate))\"\n [attr.title]=\"'nh-assistant.conversation-list.delete' | translate\"\n (click)=\"conversationDelete.emit(conversation.id)\">\n <nh-assistant-icon name=\"trash\" />\n </button>\n </li>\n }\n </ul>\n }\n</nav>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:flex;flex-direction:column;min-height:0;color:var(--nha-text)}.list{display:flex;flex:1;flex-direction:column;min-height:0}.header{display:flex;align-items:center;gap:8px;padding:12px 12px 8px 16px}h3{flex:1;margin:0;font-size:13px;font-weight:700}.count{color:var(--nha-text-muted);font-size:11px}.icon-button{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1px solid transparent;border-radius:var(--nha-radius-control);color:var(--nha-text-muted);background:transparent;cursor:pointer}.icon-button:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.icon-button:hover:not(:disabled){color:var(--nha-text);background:var(--nha-surface-strong)}.icon-button:disabled{cursor:not-allowed;opacity:.5}.state{margin:0;padding:12px 16px;color:var(--nha-text-muted);font-size:13px}ul{flex:1;min-height:0;margin:0;overflow-y:auto;padding:0 8px 12px;list-style:none}li{display:flex;align-items:center;gap:4px;border-radius:var(--nha-radius-control)}li.active{background:var(--nha-surface-strong)}.item{display:flex;flex:1;flex-direction:column;gap:2px;min-width:0;border:0;border-radius:var(--nha-radius-control);padding:8px;color:inherit;background:transparent;font:inherit;text-align:left;cursor:pointer}.item:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.item:hover:not(:disabled){background:var(--nha-surface-strong)}.item:disabled{cursor:not-allowed;opacity:.6}.title{overflow:hidden;font-size:13px;font-weight:650;text-overflow:ellipsis;white-space:nowrap}.meta{display:flex;gap:8px;color:var(--nha-text-muted);font-size:11px}.status[data-status=waiting-for-approval]{color:var(--nha-warning);font-weight:700}.status[data-status=failed]{color:var(--nha-danger)}.delete{flex:none}\n"] }]
|
|
2580
|
+
}], propDecorators: { conversations: [{ type: i0.Input, args: [{ isSignal: true, alias: "conversations", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: false }] }], activeConversationId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeConversationId", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], conversationSelect: [{ type: i0.Output, args: ["conversationSelect"] }], conversationDelete: [{ type: i0.Output, args: ["conversationDelete"] }], conversationCreate: [{ type: i0.Output, args: ["conversationCreate"] }] } });
|
|
2581
|
+
|
|
2582
|
+
/** Maximum length of the user's own instructions. */
|
|
2583
|
+
const NH_ASSISTANT_MAX_CUSTOM_INSTRUCTIONS = 1_000;
|
|
2584
|
+
let nextId$2 = 0;
|
|
2585
|
+
/**
|
|
2586
|
+
* The user's own assistant preferences: style, form of address, answer length and
|
|
2587
|
+
* optional instructions. Preferences steer style only; the server keeps them below the
|
|
2588
|
+
* application context and agent instructions.
|
|
2589
|
+
*/
|
|
2590
|
+
class NhAssistantPreferencesComponent {
|
|
2591
|
+
constructor() {
|
|
2592
|
+
this.api = inject(NhAssistantApiService);
|
|
2593
|
+
/** Emitted when the user leaves the preferences. */
|
|
2594
|
+
this.closed = output();
|
|
2595
|
+
/** Emitted with the stored preferences after a successful save. */
|
|
2596
|
+
this.saved = output();
|
|
2597
|
+
this.id = `nh-assistant-preferences-${nextId$2++}`;
|
|
2598
|
+
this.styles = ['default', 'direct', 'personal', 'detailed'];
|
|
2599
|
+
this.addressForms = ['informal', 'formal'];
|
|
2600
|
+
this.responseLengths = ['short', 'normal', 'long'];
|
|
2601
|
+
this.maxInstructions = NH_ASSISTANT_MAX_CUSTOM_INSTRUCTIONS;
|
|
2602
|
+
this.loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
2603
|
+
this.loadFailed = signal(false, ...(ngDevMode ? [{ debugName: "loadFailed" }] : []));
|
|
2604
|
+
this.saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : []));
|
|
2605
|
+
this.savedMessage = signal(false, ...(ngDevMode ? [{ debugName: "savedMessage" }] : []));
|
|
2606
|
+
this.error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
2607
|
+
this.style = signal('default', ...(ngDevMode ? [{ debugName: "style" }] : []));
|
|
2608
|
+
this.addressForm = signal('informal', ...(ngDevMode ? [{ debugName: "addressForm" }] : []));
|
|
2609
|
+
this.responseLength = signal('normal', ...(ngDevMode ? [{ debugName: "responseLength" }] : []));
|
|
2610
|
+
this.customInstructions = signal('', ...(ngDevMode ? [{ debugName: "customInstructions" }] : []));
|
|
2611
|
+
this.stored = signal(null, ...(ngDevMode ? [{ debugName: "stored" }] : []));
|
|
2612
|
+
this.instructionsLength = computed(() => this.customInstructions().length, ...(ngDevMode ? [{ debugName: "instructionsLength" }] : []));
|
|
2613
|
+
this.tooLong = computed(() => this.instructionsLength() > this.maxInstructions, ...(ngDevMode ? [{ debugName: "tooLong" }] : []));
|
|
2614
|
+
this.dirty = computed(() => {
|
|
2615
|
+
const stored = this.stored();
|
|
2616
|
+
return !stored || stored.style !== this.style() || stored.addressForm !== this.addressForm() ||
|
|
2617
|
+
stored.responseLength !== this.responseLength() || (stored.customInstructions ?? '') !== this.customInstructions();
|
|
2618
|
+
}, ...(ngDevMode ? [{ debugName: "dirty" }] : []));
|
|
2619
|
+
this.canSave = computed(() => !this.loading() && !this.saving() && !this.tooLong() && this.dirty(), ...(ngDevMode ? [{ debugName: "canSave" }] : []));
|
|
2620
|
+
this.errorKeys = computed(() => {
|
|
2621
|
+
const error = this.error();
|
|
2622
|
+
return error ? [error.messageKey, nhAssistantErrorMessageKey(error.code), 'nh-assistant.errors.generic'] : [];
|
|
2623
|
+
}, ...(ngDevMode ? [{ debugName: "errorKeys" }] : []));
|
|
2624
|
+
inject(DestroyRef).onDestroy(() => this.subscription?.unsubscribe());
|
|
2625
|
+
this.load();
|
|
2626
|
+
}
|
|
2627
|
+
load() {
|
|
2628
|
+
this.loading.set(true);
|
|
2629
|
+
this.loadFailed.set(false);
|
|
2630
|
+
this.subscription?.unsubscribe();
|
|
2631
|
+
this.subscription = this.api.getPreferences().subscribe({
|
|
2632
|
+
next: preferences => {
|
|
2633
|
+
this.apply(preferences);
|
|
2634
|
+
this.loading.set(false);
|
|
2635
|
+
},
|
|
2636
|
+
error: () => {
|
|
2637
|
+
this.loading.set(false);
|
|
2638
|
+
this.loadFailed.set(true);
|
|
2639
|
+
}
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
onInstructionsInput(event) {
|
|
2643
|
+
this.customInstructions.set(event.target.value);
|
|
2644
|
+
this.savedMessage.set(false);
|
|
2645
|
+
}
|
|
2646
|
+
choose(target, value) {
|
|
2647
|
+
target.set(value);
|
|
2648
|
+
this.savedMessage.set(false);
|
|
2649
|
+
}
|
|
2650
|
+
save() {
|
|
2651
|
+
if (!this.canSave()) {
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
const instructions = this.customInstructions().trim();
|
|
2655
|
+
const preferences = {
|
|
2656
|
+
style: this.style(),
|
|
2657
|
+
addressForm: this.addressForm(),
|
|
2658
|
+
responseLength: this.responseLength(),
|
|
2659
|
+
customInstructions: instructions.length > 0 ? instructions : null
|
|
2660
|
+
};
|
|
2661
|
+
this.saving.set(true);
|
|
2662
|
+
this.error.set(null);
|
|
2663
|
+
this.savedMessage.set(false);
|
|
2664
|
+
this.subscription?.unsubscribe();
|
|
2665
|
+
this.subscription = this.api.updatePreferences(preferences).subscribe({
|
|
2666
|
+
next: stored => {
|
|
2667
|
+
this.apply(stored);
|
|
2668
|
+
this.saving.set(false);
|
|
2669
|
+
this.savedMessage.set(true);
|
|
2670
|
+
this.saved.emit(stored);
|
|
2671
|
+
},
|
|
2672
|
+
error: (error) => {
|
|
2673
|
+
this.saving.set(false);
|
|
2674
|
+
this.error.set(error instanceof NhAssistantApiError
|
|
2675
|
+
? { code: error.code, messageKey: error.messageKey }
|
|
2676
|
+
: { code: 'assistant-server', messageKey: nhAssistantErrorMessageKey('assistant-server') });
|
|
2677
|
+
}
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
apply(preferences) {
|
|
2681
|
+
this.stored.set(preferences);
|
|
2682
|
+
this.style.set(preferences.style);
|
|
2683
|
+
this.addressForm.set(preferences.addressForm);
|
|
2684
|
+
this.responseLength.set(preferences.responseLength);
|
|
2685
|
+
this.customInstructions.set(preferences.customInstructions ?? '');
|
|
2686
|
+
}
|
|
2687
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantPreferencesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2688
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhAssistantPreferencesComponent, isStandalone: true, selector: "nh-assistant-preferences", outputs: { closed: "closed", saved: "saved" }, ngImport: i0, template: "<section class=\"preferences\" [attr.aria-labelledby]=\"id + '-title'\" [attr.aria-busy]=\"loading() || saving()\">\n <header class=\"header\">\n <h3 [id]=\"id + '-title'\">{{ 'nh-assistant.preferences.title' | translate }}</h3>\n <button\n class=\"icon-button\"\n type=\"button\"\n [attr.aria-label]=\"'nh-assistant.preferences.back' | translate\"\n [attr.title]=\"'nh-assistant.preferences.back' | translate\"\n (click)=\"closed.emit()\">\n <nh-assistant-icon name=\"close\" />\n </button>\n </header>\n\n @if (loading()) {\n <p class=\"state\" role=\"status\">{{ 'nh-assistant.preferences.loading' | translate }}</p>\n } @else if (loadFailed()) {\n <div class=\"state\" role=\"alert\">\n <p>{{ 'nh-assistant.preferences.load-failed' | translate }}</p>\n <button class=\"secondary\" type=\"button\" (click)=\"load()\">{{ 'nh-assistant.preferences.retry' | translate }}</button>\n </div>\n } @else {\n <form class=\"form\" (submit)=\"$event.preventDefault(); save()\">\n <p class=\"description\">{{ 'nh-assistant.preferences.description' | translate }}</p>\n\n <fieldset>\n <legend>{{ 'nh-assistant.preferences.style-label' | translate }}</legend>\n <div class=\"options styles\">\n @for (option of styles; track option) {\n <label class=\"option\" [class.selected]=\"style() === option\">\n <input type=\"radio\" [name]=\"id + '-style'\" [value]=\"option\" [checked]=\"style() === option\" (change)=\"choose(style, option)\" />\n <span>\n <strong>{{ 'nh-assistant.preferences.style.' + option | translate }}</strong>\n <small>{{ 'nh-assistant.preferences.style.' + option + '-description' | translate }}</small>\n </span>\n </label>\n }\n </div>\n </fieldset>\n\n <fieldset>\n <legend>{{ 'nh-assistant.preferences.address-form-label' | translate }}</legend>\n <div class=\"options segmented\">\n @for (option of addressForms; track option) {\n <label class=\"option\" [class.selected]=\"addressForm() === option\">\n <input type=\"radio\" [name]=\"id + '-address'\" [value]=\"option\" [checked]=\"addressForm() === option\" (change)=\"choose(addressForm, option)\" />\n <span><strong>{{ 'nh-assistant.preferences.address-form.' + option | translate }}</strong></span>\n </label>\n }\n </div>\n </fieldset>\n\n <fieldset>\n <legend>{{ 'nh-assistant.preferences.response-length-label' | translate }}</legend>\n <div class=\"options segmented\">\n @for (option of responseLengths; track option) {\n <label class=\"option\" [class.selected]=\"responseLength() === option\">\n <input type=\"radio\" [name]=\"id + '-length'\" [value]=\"option\" [checked]=\"responseLength() === option\" (change)=\"choose(responseLength, option)\" />\n <span><strong>{{ 'nh-assistant.preferences.response-length.' + option | translate }}</strong></span>\n </label>\n }\n </div>\n </fieldset>\n\n <div class=\"field\">\n <label [for]=\"id + '-instructions'\">{{ 'nh-assistant.preferences.custom-instructions-label' | translate }}</label>\n <textarea\n rows=\"4\"\n [id]=\"id + '-instructions'\"\n [value]=\"customInstructions()\"\n [attr.aria-describedby]=\"id + '-instructions-hint'\"\n [attr.aria-invalid]=\"tooLong() ? true : null\"\n (input)=\"onInstructionsInput($event)\"></textarea>\n <p class=\"hint\" [id]=\"id + '-instructions-hint'\">\n @if (tooLong()) {\n <span class=\"error-text\">{{ 'nh-assistant.preferences.too-long' | translate }}</span>\n } @else {\n <span>{{ 'nh-assistant.preferences.custom-instructions-hint' | translate }}</span>\n }\n <span class=\"counter\" [class.error-text]=\"tooLong()\">\n {{ 'nh-assistant.preferences.characters' | translate: { count: instructionsLength(), max: maxInstructions } }}\n </span>\n </p>\n </div>\n\n @if (error(); as error) {\n <p class=\"message error\" role=\"alert\">\n <nh-assistant-icon name=\"warning\" />\n {{ errorKeys() | nhAssistantTranslate: error.code }}\n </p>\n }\n @if (savedMessage()) {\n <p class=\"message success\" role=\"status\">\n <nh-assistant-icon name=\"check\" />\n {{ 'nh-assistant.preferences.saved' | translate }}\n </p>\n }\n\n <div class=\"actions\">\n <button class=\"secondary\" type=\"button\" (click)=\"closed.emit()\">{{ 'nh-assistant.preferences.back' | translate }}</button>\n <button class=\"primary\" type=\"submit\" [disabled]=\"!canSave()\">\n {{ (saving() ? 'nh-assistant.preferences.saving' : 'nh-assistant.preferences.save') | translate }}\n </button>\n </div>\n </form>\n }\n</section>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:flex;flex-direction:column;min-height:0;color:var(--nha-text)}:host fieldset{min-width:0;margin:0;border:0;padding:0}:host legend,:host .label,:host .field>label{display:block;margin-bottom:6px;padding:0;color:var(--nha-text);font-size:12px;font-weight:700}:host input[type=text],:host input[type=url],:host input[type=password],:host input[type=search],:host select,:host textarea{width:100%;min-width:0;min-height:38px;border:1px solid var(--nha-border);border-radius:var(--nha-radius-control);padding:8px 10px;color:var(--nha-text);background:var(--nha-background);font:inherit;font-size:13px}:host input[type=text]:focus-visible,:host input[type=url]:focus-visible,:host input[type=password]:focus-visible,:host input[type=search]:focus-visible,:host select:focus-visible,:host textarea:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}:host input[type=text]:disabled,:host input[type=url]:disabled,:host input[type=password]:disabled,:host input[type=search]:disabled,:host select:disabled,:host textarea:disabled{cursor:not-allowed;opacity:.6}:host input[type=text][aria-invalid=true],:host input[type=url][aria-invalid=true],:host input[type=password][aria-invalid=true],:host input[type=search][aria-invalid=true],:host select[aria-invalid=true],:host textarea[aria-invalid=true]{border-color:var(--nha-danger)}:host textarea{line-height:1.45;resize:vertical}:host .mono{font-family:var(--nha-mono);font-size:12px}:host .field{display:flex;flex-direction:column;min-width:0}:host .hint{display:flex;justify-content:space-between;gap:8px;margin:4px 2px 0;color:var(--nha-text-muted);font-size:11px}:host .counter{flex:none;font-variant-numeric:tabular-nums}:host .error-text{color:var(--nha-danger)}:host .message{display:flex;align-items:center;gap:8px;margin:0;border:1px solid transparent;border-radius:var(--nha-radius-control);padding:8px 12px;font-size:13px}:host .message.error{border-color:var(--nha-danger);color:var(--nha-text);background:var(--nha-danger-soft)}:host .message.warning{border-color:var(--nha-warning);color:var(--nha-text);background:var(--nha-warning-soft)}:host .message.success{border-color:var(--nha-success);color:var(--nha-text);background:var(--nha-success-soft)}:host .message nh-assistant-icon{flex:none}:host .primary,:host .secondary,:host .danger{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:36px;border:1px solid var(--nha-border);border-radius:var(--nha-radius-control);padding:6px 14px;font:inherit;font-size:13px;font-weight:700;white-space:nowrap;cursor:pointer}:host .primary:focus-visible,:host .secondary:focus-visible,:host .danger:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}:host .primary:disabled,:host .secondary:disabled,:host .danger:disabled{cursor:not-allowed;opacity:.55}:host .primary{border-color:var(--nha-accent);color:var(--nha-accent-contrast);background:var(--nha-accent)}:host .primary:hover:not(:disabled){filter:brightness(1.12)}:host .secondary{color:var(--nha-text);background:transparent}:host .secondary:hover:not(:disabled){background:var(--nha-surface-strong)}:host .danger{border-color:var(--nha-danger);color:var(--nha-danger);background:transparent}:host .danger:hover:not(:disabled){background:var(--nha-danger-soft)}:host .icon-button{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1px solid transparent;border-radius:var(--nha-radius-control);color:var(--nha-text-muted);background:transparent;cursor:pointer}:host .icon-button:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}:host .icon-button:hover:not(:disabled){color:var(--nha-text);background:var(--nha-surface-strong)}:host .icon-button:disabled{cursor:not-allowed;opacity:.5}:host .actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}:host .badge{display:inline-flex;align-items:center;gap:4px;border-radius:999px;padding:2px 8px;color:var(--nha-text-muted);background:var(--nha-surface-strong);font-size:11px;font-weight:700;white-space:nowrap}:host .badge.success{color:var(--nha-success);background:var(--nha-success-soft)}:host .badge.warning{color:var(--nha-warning);background:var(--nha-warning-soft)}:host .badge.danger{color:var(--nha-danger);background:var(--nha-danger-soft)}:host .visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}.preferences{display:flex;flex:1;flex-direction:column;min-height:0}.header{display:flex;align-items:center;gap:8px;padding:12px 12px 8px 16px}h3{flex:1;margin:0;font-size:15px;font-weight:700}.state{display:flex;flex-direction:column;align-items:flex-start;gap:10px;margin:0;padding:12px 16px;color:var(--nha-text-muted);font-size:13px}.state p{margin:0}.form{display:flex;flex:1;flex-direction:column;gap:18px;min-height:0;overflow-y:auto;padding:0 16px 16px}.description{margin:0;color:var(--nha-text-muted);font-size:13px}.options{display:grid;gap:8px}.styles{grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}.segmented{grid-template-columns:repeat(auto-fit,minmax(90px,1fr))}.option{position:relative;display:flex;align-items:flex-start;gap:8px;border:1px solid var(--nha-border);border-radius:var(--nha-radius-control);padding:10px;background:var(--nha-background);cursor:pointer}.option:hover{background:var(--nha-surface)}.option.selected{border-color:var(--nha-highlight);background:var(--nha-surface);box-shadow:inset 0 0 0 1px var(--nha-highlight)}.option:focus-within{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.option input{flex:none;margin:2px 0 0;accent-color:var(--nha-highlight)}.option span{display:flex;flex-direction:column;gap:2px;min-width:0}.option strong{font-size:13px}.option small{color:var(--nha-text-muted);font-size:12px}\n"], dependencies: [{ kind: "component", type: NhAssistantIconComponent, selector: "nh-assistant-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "pipe", type: NhAssistantTranslatePipe, name: "nhAssistantTranslate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2689
|
+
}
|
|
2690
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantPreferencesComponent, decorators: [{
|
|
2691
|
+
type: Component,
|
|
2692
|
+
args: [{ selector: 'nh-assistant-preferences', standalone: true, imports: [TranslatePipe, NhAssistantTranslatePipe, NhAssistantIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<section class=\"preferences\" [attr.aria-labelledby]=\"id + '-title'\" [attr.aria-busy]=\"loading() || saving()\">\n <header class=\"header\">\n <h3 [id]=\"id + '-title'\">{{ 'nh-assistant.preferences.title' | translate }}</h3>\n <button\n class=\"icon-button\"\n type=\"button\"\n [attr.aria-label]=\"'nh-assistant.preferences.back' | translate\"\n [attr.title]=\"'nh-assistant.preferences.back' | translate\"\n (click)=\"closed.emit()\">\n <nh-assistant-icon name=\"close\" />\n </button>\n </header>\n\n @if (loading()) {\n <p class=\"state\" role=\"status\">{{ 'nh-assistant.preferences.loading' | translate }}</p>\n } @else if (loadFailed()) {\n <div class=\"state\" role=\"alert\">\n <p>{{ 'nh-assistant.preferences.load-failed' | translate }}</p>\n <button class=\"secondary\" type=\"button\" (click)=\"load()\">{{ 'nh-assistant.preferences.retry' | translate }}</button>\n </div>\n } @else {\n <form class=\"form\" (submit)=\"$event.preventDefault(); save()\">\n <p class=\"description\">{{ 'nh-assistant.preferences.description' | translate }}</p>\n\n <fieldset>\n <legend>{{ 'nh-assistant.preferences.style-label' | translate }}</legend>\n <div class=\"options styles\">\n @for (option of styles; track option) {\n <label class=\"option\" [class.selected]=\"style() === option\">\n <input type=\"radio\" [name]=\"id + '-style'\" [value]=\"option\" [checked]=\"style() === option\" (change)=\"choose(style, option)\" />\n <span>\n <strong>{{ 'nh-assistant.preferences.style.' + option | translate }}</strong>\n <small>{{ 'nh-assistant.preferences.style.' + option + '-description' | translate }}</small>\n </span>\n </label>\n }\n </div>\n </fieldset>\n\n <fieldset>\n <legend>{{ 'nh-assistant.preferences.address-form-label' | translate }}</legend>\n <div class=\"options segmented\">\n @for (option of addressForms; track option) {\n <label class=\"option\" [class.selected]=\"addressForm() === option\">\n <input type=\"radio\" [name]=\"id + '-address'\" [value]=\"option\" [checked]=\"addressForm() === option\" (change)=\"choose(addressForm, option)\" />\n <span><strong>{{ 'nh-assistant.preferences.address-form.' + option | translate }}</strong></span>\n </label>\n }\n </div>\n </fieldset>\n\n <fieldset>\n <legend>{{ 'nh-assistant.preferences.response-length-label' | translate }}</legend>\n <div class=\"options segmented\">\n @for (option of responseLengths; track option) {\n <label class=\"option\" [class.selected]=\"responseLength() === option\">\n <input type=\"radio\" [name]=\"id + '-length'\" [value]=\"option\" [checked]=\"responseLength() === option\" (change)=\"choose(responseLength, option)\" />\n <span><strong>{{ 'nh-assistant.preferences.response-length.' + option | translate }}</strong></span>\n </label>\n }\n </div>\n </fieldset>\n\n <div class=\"field\">\n <label [for]=\"id + '-instructions'\">{{ 'nh-assistant.preferences.custom-instructions-label' | translate }}</label>\n <textarea\n rows=\"4\"\n [id]=\"id + '-instructions'\"\n [value]=\"customInstructions()\"\n [attr.aria-describedby]=\"id + '-instructions-hint'\"\n [attr.aria-invalid]=\"tooLong() ? true : null\"\n (input)=\"onInstructionsInput($event)\"></textarea>\n <p class=\"hint\" [id]=\"id + '-instructions-hint'\">\n @if (tooLong()) {\n <span class=\"error-text\">{{ 'nh-assistant.preferences.too-long' | translate }}</span>\n } @else {\n <span>{{ 'nh-assistant.preferences.custom-instructions-hint' | translate }}</span>\n }\n <span class=\"counter\" [class.error-text]=\"tooLong()\">\n {{ 'nh-assistant.preferences.characters' | translate: { count: instructionsLength(), max: maxInstructions } }}\n </span>\n </p>\n </div>\n\n @if (error(); as error) {\n <p class=\"message error\" role=\"alert\">\n <nh-assistant-icon name=\"warning\" />\n {{ errorKeys() | nhAssistantTranslate: error.code }}\n </p>\n }\n @if (savedMessage()) {\n <p class=\"message success\" role=\"status\">\n <nh-assistant-icon name=\"check\" />\n {{ 'nh-assistant.preferences.saved' | translate }}\n </p>\n }\n\n <div class=\"actions\">\n <button class=\"secondary\" type=\"button\" (click)=\"closed.emit()\">{{ 'nh-assistant.preferences.back' | translate }}</button>\n <button class=\"primary\" type=\"submit\" [disabled]=\"!canSave()\">\n {{ (saving() ? 'nh-assistant.preferences.saving' : 'nh-assistant.preferences.save') | translate }}\n </button>\n </div>\n </form>\n }\n</section>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:flex;flex-direction:column;min-height:0;color:var(--nha-text)}:host fieldset{min-width:0;margin:0;border:0;padding:0}:host legend,:host .label,:host .field>label{display:block;margin-bottom:6px;padding:0;color:var(--nha-text);font-size:12px;font-weight:700}:host input[type=text],:host input[type=url],:host input[type=password],:host input[type=search],:host select,:host textarea{width:100%;min-width:0;min-height:38px;border:1px solid var(--nha-border);border-radius:var(--nha-radius-control);padding:8px 10px;color:var(--nha-text);background:var(--nha-background);font:inherit;font-size:13px}:host input[type=text]:focus-visible,:host input[type=url]:focus-visible,:host input[type=password]:focus-visible,:host input[type=search]:focus-visible,:host select:focus-visible,:host textarea:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}:host input[type=text]:disabled,:host input[type=url]:disabled,:host input[type=password]:disabled,:host input[type=search]:disabled,:host select:disabled,:host textarea:disabled{cursor:not-allowed;opacity:.6}:host input[type=text][aria-invalid=true],:host input[type=url][aria-invalid=true],:host input[type=password][aria-invalid=true],:host input[type=search][aria-invalid=true],:host select[aria-invalid=true],:host textarea[aria-invalid=true]{border-color:var(--nha-danger)}:host textarea{line-height:1.45;resize:vertical}:host .mono{font-family:var(--nha-mono);font-size:12px}:host .field{display:flex;flex-direction:column;min-width:0}:host .hint{display:flex;justify-content:space-between;gap:8px;margin:4px 2px 0;color:var(--nha-text-muted);font-size:11px}:host .counter{flex:none;font-variant-numeric:tabular-nums}:host .error-text{color:var(--nha-danger)}:host .message{display:flex;align-items:center;gap:8px;margin:0;border:1px solid transparent;border-radius:var(--nha-radius-control);padding:8px 12px;font-size:13px}:host .message.error{border-color:var(--nha-danger);color:var(--nha-text);background:var(--nha-danger-soft)}:host .message.warning{border-color:var(--nha-warning);color:var(--nha-text);background:var(--nha-warning-soft)}:host .message.success{border-color:var(--nha-success);color:var(--nha-text);background:var(--nha-success-soft)}:host .message nh-assistant-icon{flex:none}:host .primary,:host .secondary,:host .danger{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:36px;border:1px solid var(--nha-border);border-radius:var(--nha-radius-control);padding:6px 14px;font:inherit;font-size:13px;font-weight:700;white-space:nowrap;cursor:pointer}:host .primary:focus-visible,:host .secondary:focus-visible,:host .danger:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}:host .primary:disabled,:host .secondary:disabled,:host .danger:disabled{cursor:not-allowed;opacity:.55}:host .primary{border-color:var(--nha-accent);color:var(--nha-accent-contrast);background:var(--nha-accent)}:host .primary:hover:not(:disabled){filter:brightness(1.12)}:host .secondary{color:var(--nha-text);background:transparent}:host .secondary:hover:not(:disabled){background:var(--nha-surface-strong)}:host .danger{border-color:var(--nha-danger);color:var(--nha-danger);background:transparent}:host .danger:hover:not(:disabled){background:var(--nha-danger-soft)}:host .icon-button{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1px solid transparent;border-radius:var(--nha-radius-control);color:var(--nha-text-muted);background:transparent;cursor:pointer}:host .icon-button:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}:host .icon-button:hover:not(:disabled){color:var(--nha-text);background:var(--nha-surface-strong)}:host .icon-button:disabled{cursor:not-allowed;opacity:.5}:host .actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}:host .badge{display:inline-flex;align-items:center;gap:4px;border-radius:999px;padding:2px 8px;color:var(--nha-text-muted);background:var(--nha-surface-strong);font-size:11px;font-weight:700;white-space:nowrap}:host .badge.success{color:var(--nha-success);background:var(--nha-success-soft)}:host .badge.warning{color:var(--nha-warning);background:var(--nha-warning-soft)}:host .badge.danger{color:var(--nha-danger);background:var(--nha-danger-soft)}:host .visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}.preferences{display:flex;flex:1;flex-direction:column;min-height:0}.header{display:flex;align-items:center;gap:8px;padding:12px 12px 8px 16px}h3{flex:1;margin:0;font-size:15px;font-weight:700}.state{display:flex;flex-direction:column;align-items:flex-start;gap:10px;margin:0;padding:12px 16px;color:var(--nha-text-muted);font-size:13px}.state p{margin:0}.form{display:flex;flex:1;flex-direction:column;gap:18px;min-height:0;overflow-y:auto;padding:0 16px 16px}.description{margin:0;color:var(--nha-text-muted);font-size:13px}.options{display:grid;gap:8px}.styles{grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}.segmented{grid-template-columns:repeat(auto-fit,minmax(90px,1fr))}.option{position:relative;display:flex;align-items:flex-start;gap:8px;border:1px solid var(--nha-border);border-radius:var(--nha-radius-control);padding:10px;background:var(--nha-background);cursor:pointer}.option:hover{background:var(--nha-surface)}.option.selected{border-color:var(--nha-highlight);background:var(--nha-surface);box-shadow:inset 0 0 0 1px var(--nha-highlight)}.option:focus-within{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.option input{flex:none;margin:2px 0 0;accent-color:var(--nha-highlight)}.option span{display:flex;flex-direction:column;gap:2px;min-width:0}.option strong{font-size:13px}.option small{color:var(--nha-text-muted);font-size:12px}\n"] }]
|
|
2693
|
+
}], ctorParameters: () => [], propDecorators: { closed: [{ type: i0.Output, args: ["closed"] }], saved: [{ type: i0.Output, args: ["saved"] }] } });
|
|
2694
|
+
|
|
2695
|
+
const allowedTags = [
|
|
2696
|
+
'p', 'br', 'strong', 'em', 'del', 'code', 'pre', 'blockquote', 'ul', 'ol', 'li', 'a',
|
|
2697
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'table', 'thead', 'tbody', 'tr', 'th', 'td'
|
|
2698
|
+
];
|
|
2699
|
+
const allowedAttributes = ['href', 'title', 'start', 'align'];
|
|
2700
|
+
const allowedUris = /^(?:https?:|mailto:|\/(?!\/)|#)/i;
|
|
2701
|
+
function escapeHtml(text) {
|
|
2702
|
+
return text
|
|
2703
|
+
.replace(/&/g, '&')
|
|
2704
|
+
.replace(/</g, '<')
|
|
2705
|
+
.replace(/>/g, '>')
|
|
2706
|
+
.replace(/"/g, '"')
|
|
2707
|
+
.replace(/'/g, ''');
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Renders model text as Markdown into a strict HTML subset. Inline HTML in the model text
|
|
2711
|
+
* is shown as literal text, images render as their alt text, and the result is sanitized
|
|
2712
|
+
* with DOMPurify. Links open in a new tab with `rel="noopener noreferrer"`.
|
|
2713
|
+
*/
|
|
2714
|
+
class NhAssistantMarkdownRenderer {
|
|
2715
|
+
constructor() {
|
|
2716
|
+
this.document = inject(DOCUMENT);
|
|
2717
|
+
this.marked = new Marked({
|
|
2718
|
+
gfm: true,
|
|
2719
|
+
breaks: true,
|
|
2720
|
+
async: false,
|
|
2721
|
+
renderer: {
|
|
2722
|
+
html(token) {
|
|
2723
|
+
return escapeHtml(token.text);
|
|
2724
|
+
},
|
|
2725
|
+
image(token) {
|
|
2726
|
+
return escapeHtml(token.text);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
});
|
|
2730
|
+
this.purifier = null;
|
|
2731
|
+
}
|
|
2732
|
+
render(text) {
|
|
2733
|
+
const purifier = this.getPurifier();
|
|
2734
|
+
if (!purifier) {
|
|
2735
|
+
return `<p>${escapeHtml(text)}</p>`;
|
|
2736
|
+
}
|
|
2737
|
+
const html = this.marked.parse(text, { async: false });
|
|
2738
|
+
return purifier.sanitize(html, {
|
|
2739
|
+
ALLOWED_TAGS: allowedTags,
|
|
2740
|
+
ALLOWED_ATTR: allowedAttributes,
|
|
2741
|
+
ALLOWED_URI_REGEXP: allowedUris,
|
|
2742
|
+
ALLOW_DATA_ATTR: false,
|
|
2743
|
+
ALLOW_ARIA_ATTR: false
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
getPurifier() {
|
|
2747
|
+
if (this.purifier) {
|
|
2748
|
+
return this.purifier;
|
|
2749
|
+
}
|
|
2750
|
+
const view = this.document.defaultView;
|
|
2751
|
+
if (!view) {
|
|
2752
|
+
return null;
|
|
2753
|
+
}
|
|
2754
|
+
const purifier = createDOMPurify(view);
|
|
2755
|
+
if (!purifier.isSupported) {
|
|
2756
|
+
return null;
|
|
2757
|
+
}
|
|
2758
|
+
purifier.addHook('afterSanitizeAttributes', node => {
|
|
2759
|
+
if (node.tagName === 'A') {
|
|
2760
|
+
node.setAttribute('target', '_blank');
|
|
2761
|
+
node.setAttribute('rel', 'noopener noreferrer');
|
|
2762
|
+
}
|
|
2763
|
+
});
|
|
2764
|
+
this.purifier = purifier;
|
|
2765
|
+
return purifier;
|
|
2766
|
+
}
|
|
2767
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantMarkdownRenderer, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2768
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantMarkdownRenderer, providedIn: 'root' }); }
|
|
2769
|
+
}
|
|
2770
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantMarkdownRenderer, decorators: [{
|
|
2771
|
+
type: Injectable,
|
|
2772
|
+
args: [{ providedIn: 'root' }]
|
|
2773
|
+
}] });
|
|
2774
|
+
|
|
2775
|
+
/** Renders model text into sanitized HTML. Pure, so unchanged text is not rendered again. */
|
|
2776
|
+
class NhAssistantMarkdownPipe {
|
|
2777
|
+
constructor() {
|
|
2778
|
+
this.renderer = inject(NhAssistantMarkdownRenderer);
|
|
2779
|
+
}
|
|
2780
|
+
transform(text) {
|
|
2781
|
+
return this.renderer.render(text);
|
|
2782
|
+
}
|
|
2783
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantMarkdownPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
2784
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantMarkdownPipe, isStandalone: true, name: "nhAssistantMarkdown" }); }
|
|
2785
|
+
}
|
|
2786
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantMarkdownPipe, decorators: [{
|
|
2787
|
+
type: Pipe,
|
|
2788
|
+
args: [{ name: 'nhAssistantMarkdown', standalone: true }]
|
|
2789
|
+
}] });
|
|
2790
|
+
|
|
2791
|
+
/** Formats remaining milliseconds as `m:ss`, or `h:mm:ss` above one hour. */
|
|
2792
|
+
function formatNhAssistantCountdown(milliseconds) {
|
|
2793
|
+
const totalSeconds = Math.max(0, Math.ceil(milliseconds / 1000));
|
|
2794
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
2795
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
2796
|
+
const seconds = totalSeconds % 60;
|
|
2797
|
+
const paddedSeconds = seconds.toString().padStart(2, '0');
|
|
2798
|
+
return hours > 0
|
|
2799
|
+
? `${hours}:${minutes.toString().padStart(2, '0')}:${paddedSeconds}`
|
|
2800
|
+
: `${minutes}:${paddedSeconds}`;
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* Asks the user to approve or reject a proposed mutation. Shows the summary, targets,
|
|
2804
|
+
* proposed arguments and a countdown to expiry. A decision is emitted at most once until
|
|
2805
|
+
* the host reports that it is no longer deciding.
|
|
2806
|
+
*/
|
|
2807
|
+
class NhAssistantApprovalCardComponent {
|
|
2808
|
+
constructor() {
|
|
2809
|
+
this.approval = input.required(...(ngDevMode ? [{ debugName: "approval" }] : []));
|
|
2810
|
+
/** True while the host submits a decision; disables both buttons. */
|
|
2811
|
+
this.deciding = input(false, ...(ngDevMode ? [{ debugName: "deciding" }] : []));
|
|
2812
|
+
this.decide = output();
|
|
2813
|
+
this.now = signal(Date.now(), ...(ngDevMode ? [{ debugName: "now" }] : []));
|
|
2814
|
+
this.submitted = signal(false, ...(ngDevMode ? [{ debugName: "submitted" }] : []));
|
|
2815
|
+
this.remainingMs = computed(() => Date.parse(this.approval().expiresAt) - this.now(), ...(ngDevMode ? [{ debugName: "remainingMs" }] : []));
|
|
2816
|
+
this.expired = computed(() => this.approval().status === 'expired' ||
|
|
2817
|
+
(this.approval().status === 'pending' && this.remainingMs() <= 0), ...(ngDevMode ? [{ debugName: "expired" }] : []));
|
|
2818
|
+
this.effectiveStatus = computed(() => this.expired() ? 'expired' : this.approval().status, ...(ngDevMode ? [{ debugName: "effectiveStatus" }] : []));
|
|
2819
|
+
this.actionable = computed(() => this.effectiveStatus() === 'pending', ...(ngDevMode ? [{ debugName: "actionable" }] : []));
|
|
2820
|
+
this.disabled = computed(() => !this.actionable() || this.deciding() || this.submitted(), ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
2821
|
+
this.countdown = computed(() => formatNhAssistantCountdown(this.remainingMs()), ...(ngDevMode ? [{ debugName: "countdown" }] : []));
|
|
2822
|
+
// Tick only while the approval is pending and not yet expired.
|
|
2823
|
+
effect(onCleanup => {
|
|
2824
|
+
if (this.approval().status !== 'pending') {
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
const expiresAt = Date.parse(this.approval().expiresAt);
|
|
2828
|
+
const timer = setInterval(() => {
|
|
2829
|
+
this.now.set(Date.now());
|
|
2830
|
+
if (Date.now() >= expiresAt) {
|
|
2831
|
+
clearInterval(timer);
|
|
2832
|
+
}
|
|
2833
|
+
}, 1000);
|
|
2834
|
+
onCleanup(() => clearInterval(timer));
|
|
2835
|
+
});
|
|
2836
|
+
// Allow a new attempt once the host finished deciding and the approval is still pending.
|
|
2837
|
+
effect(() => {
|
|
2838
|
+
if (!this.deciding() && this.approval().status === 'pending') {
|
|
2839
|
+
this.submitted.set(false);
|
|
2840
|
+
}
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
choose(decision) {
|
|
2844
|
+
if (this.disabled()) {
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
this.submitted.set(true);
|
|
2848
|
+
this.decide.emit(decision);
|
|
2849
|
+
}
|
|
2850
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantApprovalCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2851
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhAssistantApprovalCardComponent, isStandalone: true, selector: "nh-assistant-approval-card", inputs: { approval: { classPropertyName: "approval", publicName: "approval", isSignal: true, isRequired: true, transformFunction: null }, deciding: { classPropertyName: "deciding", publicName: "deciding", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { decide: "decide" }, host: { properties: { "attr.data-status": "effectiveStatus()" } }, ngImport: i0, template: "<section class=\"card\" [attr.aria-label]=\"'nh-assistant.approval.title' | translate\">\n <header class=\"header\">\n <nh-assistant-icon [name]=\"actionable() ? 'warning' : effectiveStatus() === 'approved' ? 'check' : 'x'\" />\n <h3>{{ 'nh-assistant.approval.title' | translate }}</h3>\n <span class=\"status\">{{ 'nh-assistant.approval.status.' + effectiveStatus() | translate }}</span>\n </header>\n\n <p class=\"summary\">{{ approval().summary }}</p>\n\n @if (approval().targets.length > 0) {\n <p class=\"label\">{{ 'nh-assistant.approval.targets' | translate }}</p>\n <ul class=\"targets\">\n @for (target of approval().targets; track $index) {\n <li>{{ target }}</li>\n }\n </ul>\n }\n\n @if (approval().argumentsPreview) {\n <p class=\"label\">{{ 'nh-assistant.approval.arguments' | translate }}</p>\n <pre>{{ approval().argumentsPreview }}</pre>\n }\n\n @if (actionable()) {\n <p class=\"expiry\" aria-live=\"off\">\n <nh-assistant-icon name=\"clock\" />\n {{ 'nh-assistant.approval.expires-in' | translate: { time: countdown() } }}\n </p>\n <div class=\"actions\">\n <button\n class=\"reject\"\n type=\"button\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"'nh-assistant.approval.reject-label' | translate: { summary: approval().summary }\"\n (click)=\"choose('reject')\">\n {{ 'nh-assistant.approval.reject' | translate }}\n </button>\n <button\n class=\"approve\"\n type=\"button\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"'nh-assistant.approval.approve-label' | translate: { summary: approval().summary }\"\n (click)=\"choose('approve')\">\n {{ 'nh-assistant.approval.approve' | translate }}\n </button>\n </div>\n @if (deciding()) {\n <p class=\"deciding\" role=\"status\">{{ 'nh-assistant.approval.deciding' | translate }}</p>\n }\n } @else if (expired()) {\n <p class=\"expiry\">{{ 'nh-assistant.approval.expired' | translate }}</p>\n }\n</section>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:block;--nha-status-color: var(--nha-warning);--nha-status-background: var(--nha-warning-soft)}:host([data-status=approved]){--nha-status-color: var(--nha-success);--nha-status-background: var(--nha-success-soft)}:host([data-status=rejected]),:host([data-status=expired]){--nha-status-color: var(--nha-text-muted);--nha-status-background: var(--nha-surface-strong)}.card{border:1px solid var(--nha-status-color);border-radius:var(--nha-radius-control);padding:12px;color:var(--nha-text);background:var(--nha-background)}.header{display:flex;align-items:center;gap:8px;color:var(--nha-status-color)}h3{flex:1;margin:0;color:var(--nha-text);font-size:14px;font-weight:700}.status{border-radius:999px;padding:2px 8px;color:var(--nha-status-color);background:var(--nha-status-background);font-size:11px;font-weight:700}.summary{margin:8px 0 0;font-size:14px;overflow-wrap:anywhere}.label{margin:10px 0 4px;color:var(--nha-text-muted);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.targets{margin:0;padding-left:18px;font-size:13px;overflow-wrap:anywhere}pre{max-height:160px;margin:0;overflow:auto;border-radius:8px;padding:8px;background:var(--nha-surface);font-family:var(--nha-mono);font-size:12px;white-space:pre-wrap;word-break:break-word}.expiry,.deciding{display:flex;align-items:center;gap:6px;margin:10px 0 0;color:var(--nha-text-muted);font-size:12px;font-variant-numeric:tabular-nums}.actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:12px}button{min-height:36px;border:1px solid var(--nha-border);border-radius:var(--nha-radius-control);padding:6px 14px;font:inherit;font-size:13px;font-weight:700;cursor:pointer}button:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}button:disabled{cursor:not-allowed;opacity:.55}.reject{color:var(--nha-text);background:transparent}.reject:hover:not(:disabled){background:var(--nha-surface-strong)}.approve{border-color:var(--nha-accent);color:var(--nha-accent-contrast);background:var(--nha-accent)}.approve:hover:not(:disabled){filter:brightness(1.12)}\n"], dependencies: [{ kind: "component", type: NhAssistantIconComponent, selector: "nh-assistant-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2852
|
+
}
|
|
2853
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantApprovalCardComponent, decorators: [{
|
|
2854
|
+
type: Component,
|
|
2855
|
+
args: [{ selector: 'nh-assistant-approval-card', standalone: true, imports: [TranslatePipe, NhAssistantIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
2856
|
+
'[attr.data-status]': 'effectiveStatus()'
|
|
2857
|
+
}, template: "<section class=\"card\" [attr.aria-label]=\"'nh-assistant.approval.title' | translate\">\n <header class=\"header\">\n <nh-assistant-icon [name]=\"actionable() ? 'warning' : effectiveStatus() === 'approved' ? 'check' : 'x'\" />\n <h3>{{ 'nh-assistant.approval.title' | translate }}</h3>\n <span class=\"status\">{{ 'nh-assistant.approval.status.' + effectiveStatus() | translate }}</span>\n </header>\n\n <p class=\"summary\">{{ approval().summary }}</p>\n\n @if (approval().targets.length > 0) {\n <p class=\"label\">{{ 'nh-assistant.approval.targets' | translate }}</p>\n <ul class=\"targets\">\n @for (target of approval().targets; track $index) {\n <li>{{ target }}</li>\n }\n </ul>\n }\n\n @if (approval().argumentsPreview) {\n <p class=\"label\">{{ 'nh-assistant.approval.arguments' | translate }}</p>\n <pre>{{ approval().argumentsPreview }}</pre>\n }\n\n @if (actionable()) {\n <p class=\"expiry\" aria-live=\"off\">\n <nh-assistant-icon name=\"clock\" />\n {{ 'nh-assistant.approval.expires-in' | translate: { time: countdown() } }}\n </p>\n <div class=\"actions\">\n <button\n class=\"reject\"\n type=\"button\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"'nh-assistant.approval.reject-label' | translate: { summary: approval().summary }\"\n (click)=\"choose('reject')\">\n {{ 'nh-assistant.approval.reject' | translate }}\n </button>\n <button\n class=\"approve\"\n type=\"button\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"'nh-assistant.approval.approve-label' | translate: { summary: approval().summary }\"\n (click)=\"choose('approve')\">\n {{ 'nh-assistant.approval.approve' | translate }}\n </button>\n </div>\n @if (deciding()) {\n <p class=\"deciding\" role=\"status\">{{ 'nh-assistant.approval.deciding' | translate }}</p>\n }\n } @else if (expired()) {\n <p class=\"expiry\">{{ 'nh-assistant.approval.expired' | translate }}</p>\n }\n</section>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:block;--nha-status-color: var(--nha-warning);--nha-status-background: var(--nha-warning-soft)}:host([data-status=approved]){--nha-status-color: var(--nha-success);--nha-status-background: var(--nha-success-soft)}:host([data-status=rejected]),:host([data-status=expired]){--nha-status-color: var(--nha-text-muted);--nha-status-background: var(--nha-surface-strong)}.card{border:1px solid var(--nha-status-color);border-radius:var(--nha-radius-control);padding:12px;color:var(--nha-text);background:var(--nha-background)}.header{display:flex;align-items:center;gap:8px;color:var(--nha-status-color)}h3{flex:1;margin:0;color:var(--nha-text);font-size:14px;font-weight:700}.status{border-radius:999px;padding:2px 8px;color:var(--nha-status-color);background:var(--nha-status-background);font-size:11px;font-weight:700}.summary{margin:8px 0 0;font-size:14px;overflow-wrap:anywhere}.label{margin:10px 0 4px;color:var(--nha-text-muted);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.targets{margin:0;padding-left:18px;font-size:13px;overflow-wrap:anywhere}pre{max-height:160px;margin:0;overflow:auto;border-radius:8px;padding:8px;background:var(--nha-surface);font-family:var(--nha-mono);font-size:12px;white-space:pre-wrap;word-break:break-word}.expiry,.deciding{display:flex;align-items:center;gap:6px;margin:10px 0 0;color:var(--nha-text-muted);font-size:12px;font-variant-numeric:tabular-nums}.actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:12px}button{min-height:36px;border:1px solid var(--nha-border);border-radius:var(--nha-radius-control);padding:6px 14px;font:inherit;font-size:13px;font-weight:700;cursor:pointer}button:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}button:disabled{cursor:not-allowed;opacity:.55}.reject{color:var(--nha-text);background:transparent}.reject:hover:not(:disabled){background:var(--nha-surface-strong)}.approve{border-color:var(--nha-accent);color:var(--nha-accent-contrast);background:var(--nha-accent)}.approve:hover:not(:disabled){filter:brightness(1.12)}\n"] }]
|
|
2858
|
+
}], ctorParameters: () => [], propDecorators: { approval: [{ type: i0.Input, args: [{ isSignal: true, alias: "approval", required: true }] }], deciding: [{ type: i0.Input, args: [{ isSignal: true, alias: "deciding", required: false }] }], decide: [{ type: i0.Output, args: ["decide"] }] } });
|
|
2859
|
+
|
|
2860
|
+
let nextId$1 = 0;
|
|
2861
|
+
const statusIcons = {
|
|
2862
|
+
running: 'tool',
|
|
2863
|
+
succeeded: 'check',
|
|
2864
|
+
failed: 'warning',
|
|
2865
|
+
'awaiting-approval': 'clock',
|
|
2866
|
+
rejected: 'x'
|
|
2867
|
+
};
|
|
2868
|
+
/** Shows one tool invocation with its status and expandable input and result previews. */
|
|
2869
|
+
class NhAssistantToolCallCardComponent {
|
|
2870
|
+
constructor() {
|
|
2871
|
+
this.part = input.required(...(ngDevMode ? [{ debugName: "part" }] : []));
|
|
2872
|
+
this.detailsId = `nh-assistant-tool-call-${nextId$1++}`;
|
|
2873
|
+
this.expanded = signal(false, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
|
|
2874
|
+
this.statusIcon = computed(() => statusIcons[this.part().status], ...(ngDevMode ? [{ debugName: "statusIcon" }] : []));
|
|
2875
|
+
this.hasDetails = computed(() => !!this.part().argumentsPreview || !!this.part().resultPreview, ...(ngDevMode ? [{ debugName: "hasDetails" }] : []));
|
|
2876
|
+
}
|
|
2877
|
+
toggle() {
|
|
2878
|
+
this.expanded.update(expanded => !expanded);
|
|
2879
|
+
}
|
|
2880
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantToolCallCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2881
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhAssistantToolCallCardComponent, isStandalone: true, selector: "nh-assistant-tool-call-card", inputs: { part: { classPropertyName: "part", publicName: "part", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.data-status": "part().status" } }, ngImport: i0, template: "<article class=\"card\" [attr.aria-label]=\"'nh-assistant.tool-call.label' | translate: { name: part().displayName }\">\n <header class=\"header\">\n <span class=\"status-icon\" [class.spinning]=\"part().status === 'running'\">\n <nh-assistant-icon [name]=\"statusIcon()\" />\n </span>\n <span class=\"title\">{{ part().displayName }}</span>\n <span class=\"status\">{{ 'nh-assistant.tool-call.status.' + part().status | translate }}</span>\n @if (hasDetails()) {\n <button\n class=\"toggle\"\n type=\"button\"\n [attr.aria-expanded]=\"expanded()\"\n [attr.aria-controls]=\"detailsId\"\n [attr.aria-label]=\"(expanded() ? 'nh-assistant.tool-call.hide-details' : 'nh-assistant.tool-call.show-details') | translate\"\n [attr.title]=\"(expanded() ? 'nh-assistant.tool-call.hide-details' : 'nh-assistant.tool-call.show-details') | translate\"\n (click)=\"toggle()\">\n <nh-assistant-icon name=\"chevron-down\" [class.rotated]=\"expanded()\" />\n </button>\n }\n </header>\n\n @if (part().resultCode; as resultCode) {\n <p class=\"result-code\">\n {{ ['nh-assistant.result-codes.' + resultCode, 'nh-assistant.errors.' + resultCode] | nhAssistantTranslate: resultCode }}\n </p>\n }\n\n @if (hasDetails()) {\n <div class=\"details\" [id]=\"detailsId\" [hidden]=\"!expanded()\">\n @if (part().argumentsPreview; as argumentsPreview) {\n <p class=\"label\">{{ 'nh-assistant.tool-call.arguments' | translate }}</p>\n <pre>{{ argumentsPreview }}</pre>\n }\n @if (part().resultPreview; as resultPreview) {\n <p class=\"label\">{{ 'nh-assistant.tool-call.result' | translate }}</p>\n <pre>{{ resultPreview }}</pre>\n }\n </div>\n }\n</article>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:block;font:inherit;--nha-status-color: var(--nha-text-muted);--nha-status-background: var(--nha-surface-strong)}:host([data-status=succeeded]){--nha-status-color: var(--nha-success);--nha-status-background: var(--nha-success-soft)}:host([data-status=failed]){--nha-status-color: var(--nha-danger);--nha-status-background: var(--nha-danger-soft)}:host([data-status=awaiting-approval]){--nha-status-color: var(--nha-warning);--nha-status-background: var(--nha-warning-soft)}:host([data-status=rejected]){--nha-status-color: var(--nha-text-muted)}.card{border:1px solid var(--nha-border);border-left:3px solid var(--nha-status-color);border-radius:var(--nha-radius-control);padding:8px 10px;color:var(--nha-text);background:var(--nha-background)}.header{display:flex;align-items:center;gap:8px;min-width:0}.status-icon{display:inline-flex;color:var(--nha-status-color)}.title{flex:1;min-width:0;overflow:hidden;font-size:13px;font-weight:650;text-overflow:ellipsis;white-space:nowrap}.status{flex:none;border-radius:999px;padding:2px 8px;color:var(--nha-status-color);background:var(--nha-status-background);font-size:11px;font-weight:700}.toggle{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1px solid transparent;border-radius:var(--nha-radius-control);color:var(--nha-text-muted);background:transparent;cursor:pointer;width:28px;height:28px}.toggle:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.toggle:hover:not(:disabled){color:var(--nha-text);background:var(--nha-surface-strong)}.toggle:disabled{cursor:not-allowed;opacity:.5}.rotated{transform:rotate(180deg)}.result-code{margin:6px 0 0;color:var(--nha-text-muted);font-size:12px}.details{margin-top:8px}.label{margin:8px 0 4px;color:var(--nha-text-muted);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}pre{max-height:180px;margin:0;overflow:auto;border-radius:8px;padding:8px;background:var(--nha-surface);font-family:var(--nha-mono);font-size:12px;white-space:pre-wrap;word-break:break-word}@media(prefers-reduced-motion:no-preference){.spinning{animation:nha-pulse 1.2s ease-in-out infinite}nh-assistant-icon{transition:transform .16s ease}}@keyframes nha-pulse{50%{opacity:.35}}\n"], dependencies: [{ kind: "component", type: NhAssistantIconComponent, selector: "nh-assistant-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "pipe", type: NhAssistantTranslatePipe, name: "nhAssistantTranslate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2882
|
+
}
|
|
2883
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantToolCallCardComponent, decorators: [{
|
|
2884
|
+
type: Component,
|
|
2885
|
+
args: [{ selector: 'nh-assistant-tool-call-card', standalone: true, imports: [TranslatePipe, NhAssistantTranslatePipe, NhAssistantIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
2886
|
+
'[attr.data-status]': 'part().status'
|
|
2887
|
+
}, template: "<article class=\"card\" [attr.aria-label]=\"'nh-assistant.tool-call.label' | translate: { name: part().displayName }\">\n <header class=\"header\">\n <span class=\"status-icon\" [class.spinning]=\"part().status === 'running'\">\n <nh-assistant-icon [name]=\"statusIcon()\" />\n </span>\n <span class=\"title\">{{ part().displayName }}</span>\n <span class=\"status\">{{ 'nh-assistant.tool-call.status.' + part().status | translate }}</span>\n @if (hasDetails()) {\n <button\n class=\"toggle\"\n type=\"button\"\n [attr.aria-expanded]=\"expanded()\"\n [attr.aria-controls]=\"detailsId\"\n [attr.aria-label]=\"(expanded() ? 'nh-assistant.tool-call.hide-details' : 'nh-assistant.tool-call.show-details') | translate\"\n [attr.title]=\"(expanded() ? 'nh-assistant.tool-call.hide-details' : 'nh-assistant.tool-call.show-details') | translate\"\n (click)=\"toggle()\">\n <nh-assistant-icon name=\"chevron-down\" [class.rotated]=\"expanded()\" />\n </button>\n }\n </header>\n\n @if (part().resultCode; as resultCode) {\n <p class=\"result-code\">\n {{ ['nh-assistant.result-codes.' + resultCode, 'nh-assistant.errors.' + resultCode] | nhAssistantTranslate: resultCode }}\n </p>\n }\n\n @if (hasDetails()) {\n <div class=\"details\" [id]=\"detailsId\" [hidden]=\"!expanded()\">\n @if (part().argumentsPreview; as argumentsPreview) {\n <p class=\"label\">{{ 'nh-assistant.tool-call.arguments' | translate }}</p>\n <pre>{{ argumentsPreview }}</pre>\n }\n @if (part().resultPreview; as resultPreview) {\n <p class=\"label\">{{ 'nh-assistant.tool-call.result' | translate }}</p>\n <pre>{{ resultPreview }}</pre>\n }\n </div>\n }\n</article>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:block;font:inherit;--nha-status-color: var(--nha-text-muted);--nha-status-background: var(--nha-surface-strong)}:host([data-status=succeeded]){--nha-status-color: var(--nha-success);--nha-status-background: var(--nha-success-soft)}:host([data-status=failed]){--nha-status-color: var(--nha-danger);--nha-status-background: var(--nha-danger-soft)}:host([data-status=awaiting-approval]){--nha-status-color: var(--nha-warning);--nha-status-background: var(--nha-warning-soft)}:host([data-status=rejected]){--nha-status-color: var(--nha-text-muted)}.card{border:1px solid var(--nha-border);border-left:3px solid var(--nha-status-color);border-radius:var(--nha-radius-control);padding:8px 10px;color:var(--nha-text);background:var(--nha-background)}.header{display:flex;align-items:center;gap:8px;min-width:0}.status-icon{display:inline-flex;color:var(--nha-status-color)}.title{flex:1;min-width:0;overflow:hidden;font-size:13px;font-weight:650;text-overflow:ellipsis;white-space:nowrap}.status{flex:none;border-radius:999px;padding:2px 8px;color:var(--nha-status-color);background:var(--nha-status-background);font-size:11px;font-weight:700}.toggle{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1px solid transparent;border-radius:var(--nha-radius-control);color:var(--nha-text-muted);background:transparent;cursor:pointer;width:28px;height:28px}.toggle:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.toggle:hover:not(:disabled){color:var(--nha-text);background:var(--nha-surface-strong)}.toggle:disabled{cursor:not-allowed;opacity:.5}.rotated{transform:rotate(180deg)}.result-code{margin:6px 0 0;color:var(--nha-text-muted);font-size:12px}.details{margin-top:8px}.label{margin:8px 0 4px;color:var(--nha-text-muted);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}pre{max-height:180px;margin:0;overflow:auto;border-radius:8px;padding:8px;background:var(--nha-surface);font-family:var(--nha-mono);font-size:12px;white-space:pre-wrap;word-break:break-word}@media(prefers-reduced-motion:no-preference){.spinning{animation:nha-pulse 1.2s ease-in-out infinite}nh-assistant-icon{transition:transform .16s ease}}@keyframes nha-pulse{50%{opacity:.35}}\n"] }]
|
|
2888
|
+
}], propDecorators: { part: [{ type: i0.Input, args: [{ isSignal: true, alias: "part", required: true }] }] } });
|
|
2889
|
+
|
|
2890
|
+
/** Above this number of messages the thread renders through CDK virtual scrolling. */
|
|
2891
|
+
const NH_ASSISTANT_VIRTUAL_SCROLL_THRESHOLD = 200;
|
|
2892
|
+
/** Distance from the bottom, in pixels, within which new content keeps the thread pinned to the end. */
|
|
2893
|
+
const stickToBottomDistance = 120;
|
|
2894
|
+
/**
|
|
2895
|
+
* The messages of a conversation as a live log. Assistant text renders as sanitized
|
|
2896
|
+
* Markdown, tool calls and approvals as cards. The thread follows new content while the
|
|
2897
|
+
* user is at the bottom.
|
|
2898
|
+
*/
|
|
2899
|
+
class NhAssistantThreadComponent {
|
|
2900
|
+
constructor() {
|
|
2901
|
+
this.injector = inject(Injector);
|
|
2902
|
+
this.messages = input.required(...(ngDevMode ? [{ debugName: "messages" }] : []));
|
|
2903
|
+
this.streaming = input(false, ...(ngDevMode ? [{ debugName: "streaming" }] : []));
|
|
2904
|
+
this.deciding = input(false, ...(ngDevMode ? [{ debugName: "deciding" }] : []));
|
|
2905
|
+
this.markdown = input(true, ...(ngDevMode ? [{ debugName: "markdown" }] : []));
|
|
2906
|
+
this.approvalDecision = output();
|
|
2907
|
+
this.virtualThreshold = NH_ASSISTANT_VIRTUAL_SCROLL_THRESHOLD;
|
|
2908
|
+
this.virtual = computed(() => this.messages().length > NH_ASSISTANT_VIRTUAL_SCROLL_THRESHOLD, ...(ngDevMode ? [{ debugName: "virtual" }] : []));
|
|
2909
|
+
this.working = computed(() => {
|
|
2910
|
+
if (!this.streaming()) {
|
|
2911
|
+
return false;
|
|
2912
|
+
}
|
|
2913
|
+
const last = this.messages()[this.messages().length - 1];
|
|
2914
|
+
return !last || last.role === 'user' || last.parts.length === 0;
|
|
2915
|
+
}, ...(ngDevMode ? [{ debugName: "working" }] : []));
|
|
2916
|
+
this.scroller = viewChild('scroller', ...(ngDevMode ? [{ debugName: "scroller" }] : []));
|
|
2917
|
+
this.viewport = viewChild(CdkVirtualScrollViewport, ...(ngDevMode ? [{ debugName: "viewport" }] : []));
|
|
2918
|
+
effect(() => {
|
|
2919
|
+
this.messages();
|
|
2920
|
+
this.working();
|
|
2921
|
+
const element = this.scrollElement();
|
|
2922
|
+
const pinned = !element || element.scrollHeight - element.scrollTop - element.clientHeight <= stickToBottomDistance;
|
|
2923
|
+
if (pinned) {
|
|
2924
|
+
afterNextRender(() => this.scrollToEnd(), { injector: this.injector });
|
|
2925
|
+
}
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
trackMessage(_index, message) {
|
|
2929
|
+
return message.id;
|
|
2930
|
+
}
|
|
2931
|
+
scrollElement() {
|
|
2932
|
+
return this.viewport()?.elementRef.nativeElement ?? this.scroller()?.nativeElement ?? null;
|
|
2933
|
+
}
|
|
2934
|
+
scrollToEnd() {
|
|
2935
|
+
const viewport = this.viewport();
|
|
2936
|
+
if (viewport) {
|
|
2937
|
+
viewport.scrollToIndex(this.messages().length - 1);
|
|
2938
|
+
viewport.scrollTo({ bottom: 0 });
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
const element = this.scroller()?.nativeElement;
|
|
2942
|
+
if (element) {
|
|
2943
|
+
element.scrollTop = element.scrollHeight;
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantThreadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2947
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhAssistantThreadComponent, isStandalone: true, selector: "nh-assistant-thread", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: true, transformFunction: null }, streaming: { classPropertyName: "streaming", publicName: "streaming", isSignal: true, isRequired: false, transformFunction: null }, deciding: { classPropertyName: "deciding", publicName: "deciding", isSignal: true, isRequired: false, transformFunction: null }, markdown: { classPropertyName: "markdown", publicName: "markdown", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { approvalDecision: "approvalDecision" }, viewQueries: [{ propertyName: "scroller", first: true, predicate: ["scroller"], descendants: true, isSignal: true }, { propertyName: "viewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"log\" role=\"log\" aria-live=\"polite\" [attr.aria-label]=\"'nh-assistant.thread.label' | translate\" [attr.aria-busy]=\"streaming()\">\n @if (virtual()) {\n <cdk-virtual-scroll-viewport class=\"scroller\" itemSize=\"120\" minBufferPx=\"1200\" maxBufferPx=\"2400\">\n <div class=\"row\" *cdkVirtualFor=\"let message of messages(); trackBy: trackMessage\">\n <ng-container [ngTemplateOutlet]=\"messageTemplate\" [ngTemplateOutletContext]=\"{ $implicit: message }\" />\n </div>\n @if (working()) {\n <ng-container [ngTemplateOutlet]=\"workingTemplate\" />\n }\n </cdk-virtual-scroll-viewport>\n } @else {\n <div class=\"scroller\" #scroller tabindex=\"0\">\n @for (message of messages(); track message.id) {\n <div class=\"row\">\n <ng-container [ngTemplateOutlet]=\"messageTemplate\" [ngTemplateOutletContext]=\"{ $implicit: message }\" />\n </div>\n }\n @if (working()) {\n <ng-container [ngTemplateOutlet]=\"workingTemplate\" />\n }\n </div>\n }\n</div>\n\n<ng-template #messageTemplate let-message>\n <article class=\"message\" [attr.data-role]=\"message.role\">\n <p class=\"author\">{{ 'nh-assistant.thread.' + (message.role === 'user' ? 'you' : message.role) | translate }}</p>\n @for (part of message.parts; track $index) {\n @switch (part.type) {\n @case ('text') {\n @if (message.role === 'assistant' && markdown()) {\n <div class=\"bubble markdown\" [innerHTML]=\"part.text | nhAssistantMarkdown\"></div>\n } @else {\n <div class=\"bubble plain\">{{ part.text }}</div>\n }\n }\n @case ('tool-call') {\n <nh-assistant-tool-call-card class=\"part\" [part]=\"part\" />\n }\n @case ('approval') {\n <nh-assistant-approval-card class=\"part\" [approval]=\"part\" [deciding]=\"deciding()\" (decide)=\"approvalDecision.emit($event)\" />\n }\n }\n }\n </article>\n</ng-template>\n\n<ng-template #workingTemplate>\n <p class=\"working\" role=\"status\">\n <span class=\"dots\" aria-hidden=\"true\"><span></span><span></span><span></span></span>\n {{ 'nh-assistant.thread.working' | translate }}\n </p>\n</ng-template>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:flex;flex-direction:column;min-height:0;color:var(--nha-text)}.log{display:flex;flex:1;flex-direction:column;min-height:0}.scroller{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:16px;outline:none}.scroller:focus-visible{box-shadow:inset 0 0 0 2px var(--nha-focus)}cdk-virtual-scroll-viewport.scroller{height:100%}.row+.row{margin-top:14px}.message{display:flex;flex-direction:column;align-items:flex-start;gap:6px;min-width:0}.message[data-role=user]{align-items:flex-end}.author{margin:0;color:var(--nha-text-muted);font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.bubble{max-width:100%;border-radius:var(--nha-radius-control);padding:8px 12px;font-size:14px;line-height:1.5;overflow-wrap:anywhere}.plain{white-space:pre-wrap}.message[data-role=user] .bubble{max-width:88%;color:var(--nha-user-text);background:var(--nha-user-bubble)}.message[data-role=assistant] .bubble,.message[data-role=tool] .bubble{border:1px solid var(--nha-border);background:var(--nha-surface)}.part{width:100%}:host ::ng-deep .markdown :first-child{margin-top:0}:host ::ng-deep .markdown :last-child{margin-bottom:0}:host ::ng-deep .markdown p,:host ::ng-deep .markdown ul,:host ::ng-deep .markdown ol,:host ::ng-deep .markdown pre,:host ::ng-deep .markdown blockquote,:host ::ng-deep .markdown table{margin:0 0 8px}:host ::ng-deep .markdown ul,:host ::ng-deep .markdown ol{padding-left:20px}:host ::ng-deep .markdown h1,:host ::ng-deep .markdown h2,:host ::ng-deep .markdown h3,:host ::ng-deep .markdown h4,:host ::ng-deep .markdown h5,:host ::ng-deep .markdown h6{margin:10px 0 6px;font-size:15px;line-height:1.3}:host ::ng-deep .markdown a{color:var(--nha-highlight);text-decoration:underline}:host ::ng-deep .markdown a:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}:host ::ng-deep .markdown code{border-radius:6px;padding:1px 4px;background:var(--nha-surface-strong);font-family:var(--nha-mono);font-size:12.5px}:host ::ng-deep .markdown pre{overflow-x:auto;border-radius:8px;padding:8px;background:var(--nha-surface-strong)}:host ::ng-deep .markdown pre code{padding:0;background:none}:host ::ng-deep .markdown blockquote{border-left:3px solid var(--nha-border);padding-left:10px;color:var(--nha-text-muted)}:host ::ng-deep .markdown table{display:block;max-width:100%;overflow-x:auto;border-collapse:collapse;font-size:13px}:host ::ng-deep .markdown th,:host ::ng-deep .markdown td{border:1px solid var(--nha-border);padding:4px 8px;text-align:left}.working{display:flex;align-items:center;gap:8px;margin:14px 0 0;color:var(--nha-text-muted);font-size:13px}.dots{display:inline-flex;gap:3px}.dots span{width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.6}@media(prefers-reduced-motion:no-preference){.dots span{animation:nha-bounce 1s ease-in-out infinite}.dots span:nth-child(2){animation-delay:.15s}.dots span:nth-child(3){animation-delay:.3s}}@keyframes nha-bounce{0%,80%,to{transform:translateY(0);opacity:.35}40%{transform:translateY(-3px);opacity:1}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ScrollingModule }, { kind: "directive", type: i1.CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i1.CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i1.CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "component", type: NhAssistantToolCallCardComponent, selector: "nh-assistant-tool-call-card", inputs: ["part"] }, { kind: "component", type: NhAssistantApprovalCardComponent, selector: "nh-assistant-approval-card", inputs: ["approval", "deciding"], outputs: ["decide"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "pipe", type: NhAssistantMarkdownPipe, name: "nhAssistantMarkdown" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2948
|
+
}
|
|
2949
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantThreadComponent, decorators: [{
|
|
2950
|
+
type: Component,
|
|
2951
|
+
args: [{ selector: 'nh-assistant-thread', standalone: true, imports: [
|
|
2952
|
+
NgTemplateOutlet,
|
|
2953
|
+
ScrollingModule,
|
|
2954
|
+
TranslatePipe,
|
|
2955
|
+
NhAssistantMarkdownPipe,
|
|
2956
|
+
NhAssistantToolCallCardComponent,
|
|
2957
|
+
NhAssistantApprovalCardComponent
|
|
2958
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"log\" role=\"log\" aria-live=\"polite\" [attr.aria-label]=\"'nh-assistant.thread.label' | translate\" [attr.aria-busy]=\"streaming()\">\n @if (virtual()) {\n <cdk-virtual-scroll-viewport class=\"scroller\" itemSize=\"120\" minBufferPx=\"1200\" maxBufferPx=\"2400\">\n <div class=\"row\" *cdkVirtualFor=\"let message of messages(); trackBy: trackMessage\">\n <ng-container [ngTemplateOutlet]=\"messageTemplate\" [ngTemplateOutletContext]=\"{ $implicit: message }\" />\n </div>\n @if (working()) {\n <ng-container [ngTemplateOutlet]=\"workingTemplate\" />\n }\n </cdk-virtual-scroll-viewport>\n } @else {\n <div class=\"scroller\" #scroller tabindex=\"0\">\n @for (message of messages(); track message.id) {\n <div class=\"row\">\n <ng-container [ngTemplateOutlet]=\"messageTemplate\" [ngTemplateOutletContext]=\"{ $implicit: message }\" />\n </div>\n }\n @if (working()) {\n <ng-container [ngTemplateOutlet]=\"workingTemplate\" />\n }\n </div>\n }\n</div>\n\n<ng-template #messageTemplate let-message>\n <article class=\"message\" [attr.data-role]=\"message.role\">\n <p class=\"author\">{{ 'nh-assistant.thread.' + (message.role === 'user' ? 'you' : message.role) | translate }}</p>\n @for (part of message.parts; track $index) {\n @switch (part.type) {\n @case ('text') {\n @if (message.role === 'assistant' && markdown()) {\n <div class=\"bubble markdown\" [innerHTML]=\"part.text | nhAssistantMarkdown\"></div>\n } @else {\n <div class=\"bubble plain\">{{ part.text }}</div>\n }\n }\n @case ('tool-call') {\n <nh-assistant-tool-call-card class=\"part\" [part]=\"part\" />\n }\n @case ('approval') {\n <nh-assistant-approval-card class=\"part\" [approval]=\"part\" [deciding]=\"deciding()\" (decide)=\"approvalDecision.emit($event)\" />\n }\n }\n }\n </article>\n</ng-template>\n\n<ng-template #workingTemplate>\n <p class=\"working\" role=\"status\">\n <span class=\"dots\" aria-hidden=\"true\"><span></span><span></span><span></span></span>\n {{ 'nh-assistant.thread.working' | translate }}\n </p>\n</ng-template>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:flex;flex-direction:column;min-height:0;color:var(--nha-text)}.log{display:flex;flex:1;flex-direction:column;min-height:0}.scroller{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:16px;outline:none}.scroller:focus-visible{box-shadow:inset 0 0 0 2px var(--nha-focus)}cdk-virtual-scroll-viewport.scroller{height:100%}.row+.row{margin-top:14px}.message{display:flex;flex-direction:column;align-items:flex-start;gap:6px;min-width:0}.message[data-role=user]{align-items:flex-end}.author{margin:0;color:var(--nha-text-muted);font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.bubble{max-width:100%;border-radius:var(--nha-radius-control);padding:8px 12px;font-size:14px;line-height:1.5;overflow-wrap:anywhere}.plain{white-space:pre-wrap}.message[data-role=user] .bubble{max-width:88%;color:var(--nha-user-text);background:var(--nha-user-bubble)}.message[data-role=assistant] .bubble,.message[data-role=tool] .bubble{border:1px solid var(--nha-border);background:var(--nha-surface)}.part{width:100%}:host ::ng-deep .markdown :first-child{margin-top:0}:host ::ng-deep .markdown :last-child{margin-bottom:0}:host ::ng-deep .markdown p,:host ::ng-deep .markdown ul,:host ::ng-deep .markdown ol,:host ::ng-deep .markdown pre,:host ::ng-deep .markdown blockquote,:host ::ng-deep .markdown table{margin:0 0 8px}:host ::ng-deep .markdown ul,:host ::ng-deep .markdown ol{padding-left:20px}:host ::ng-deep .markdown h1,:host ::ng-deep .markdown h2,:host ::ng-deep .markdown h3,:host ::ng-deep .markdown h4,:host ::ng-deep .markdown h5,:host ::ng-deep .markdown h6{margin:10px 0 6px;font-size:15px;line-height:1.3}:host ::ng-deep .markdown a{color:var(--nha-highlight);text-decoration:underline}:host ::ng-deep .markdown a:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}:host ::ng-deep .markdown code{border-radius:6px;padding:1px 4px;background:var(--nha-surface-strong);font-family:var(--nha-mono);font-size:12.5px}:host ::ng-deep .markdown pre{overflow-x:auto;border-radius:8px;padding:8px;background:var(--nha-surface-strong)}:host ::ng-deep .markdown pre code{padding:0;background:none}:host ::ng-deep .markdown blockquote{border-left:3px solid var(--nha-border);padding-left:10px;color:var(--nha-text-muted)}:host ::ng-deep .markdown table{display:block;max-width:100%;overflow-x:auto;border-collapse:collapse;font-size:13px}:host ::ng-deep .markdown th,:host ::ng-deep .markdown td{border:1px solid var(--nha-border);padding:4px 8px;text-align:left}.working{display:flex;align-items:center;gap:8px;margin:14px 0 0;color:var(--nha-text-muted);font-size:13px}.dots{display:inline-flex;gap:3px}.dots span{width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.6}@media(prefers-reduced-motion:no-preference){.dots span{animation:nha-bounce 1s ease-in-out infinite}.dots span:nth-child(2){animation-delay:.15s}.dots span:nth-child(3){animation-delay:.3s}}@keyframes nha-bounce{0%,80%,to{transform:translateY(0);opacity:.35}40%{transform:translateY(-3px);opacity:1}}\n"] }]
|
|
2959
|
+
}], ctorParameters: () => [], propDecorators: { messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: true }] }], streaming: [{ type: i0.Input, args: [{ isSignal: true, alias: "streaming", required: false }] }], deciding: [{ type: i0.Input, args: [{ isSignal: true, alias: "deciding", required: false }] }], markdown: [{ type: i0.Input, args: [{ isSignal: true, alias: "markdown", required: false }] }], approvalDecision: [{ type: i0.Output, args: ["approvalDecision"] }], scroller: [{ type: i0.ViewChild, args: ['scroller', { isSignal: true }] }], viewport: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CdkVirtualScrollViewport), { isSignal: true }] }] } });
|
|
2960
|
+
|
|
2961
|
+
let nextId = 0;
|
|
2962
|
+
/**
|
|
2963
|
+
* The assistant drawer. Place it once in the application layout; it renders nothing in
|
|
2964
|
+
* place and appears as an overlay when `NhAssistantPanelService.open()` is called.
|
|
2965
|
+
*/
|
|
2966
|
+
class NhAssistantPanelComponent {
|
|
2967
|
+
constructor() {
|
|
2968
|
+
this.store = inject(NhAssistantStore);
|
|
2969
|
+
this.panel = inject(NhAssistantPanelService);
|
|
2970
|
+
this.config = inject(NH_ASSISTANT_CONFIG);
|
|
2971
|
+
this.viewContainerRef = inject(ViewContainerRef);
|
|
2972
|
+
this.router = inject(Router, { optional: true });
|
|
2973
|
+
this.drawer = viewChild.required('drawer');
|
|
2974
|
+
this.titleId = `nh-assistant-panel-title-${nextId++}`;
|
|
2975
|
+
this.markdown = this.config.markdown?.enabled ?? true;
|
|
2976
|
+
this.showConversations = signal(false, ...(ngDevMode ? [{ debugName: "showConversations" }] : []));
|
|
2977
|
+
this.showPreferences = signal(false, ...(ngDevMode ? [{ debugName: "showPreferences" }] : []));
|
|
2978
|
+
this.adminRoute = this.config.adminRoute ?? null;
|
|
2979
|
+
this.conversation = this.store.activeConversation;
|
|
2980
|
+
this.conversationId = computed(() => this.conversation()?.id ?? null, ...(ngDevMode ? [{ debugName: "conversationId" }] : []));
|
|
2981
|
+
this.messages = computed(() => this.conversation()?.messages ?? [], ...(ngDevMode ? [{ debugName: "messages" }] : []));
|
|
2982
|
+
this.closed = computed(() => {
|
|
2983
|
+
const status = this.conversation()?.status;
|
|
2984
|
+
return status === 'failed' || status === 'archived';
|
|
2985
|
+
}, ...(ngDevMode ? [{ debugName: "closed" }] : []));
|
|
2986
|
+
this.busy = computed(() => this.store.streaming() || this.conversation()?.status === 'running', ...(ngDevMode ? [{ debugName: "busy" }] : []));
|
|
2987
|
+
this.composerDisabled = computed(() => !this.store.canSend(), ...(ngDevMode ? [{ debugName: "composerDisabled" }] : []));
|
|
2988
|
+
this.errorKeys = computed(() => {
|
|
2989
|
+
const error = this.store.error();
|
|
2990
|
+
return error ? [error.messageKey, `nh-assistant.errors.${error.code}`, 'nh-assistant.errors.generic'] : [];
|
|
2991
|
+
}, ...(ngDevMode ? [{ debugName: "errorKeys" }] : []));
|
|
2992
|
+
// Opening, creating or leaving a conversation returns from the list to the thread.
|
|
2993
|
+
effect(() => {
|
|
2994
|
+
if (this.conversationId() !== undefined) {
|
|
2995
|
+
untracked(() => {
|
|
2996
|
+
this.showConversations.set(false);
|
|
2997
|
+
this.showPreferences.set(false);
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
});
|
|
3001
|
+
effect(() => {
|
|
3002
|
+
if (this.panel.isOpen()) {
|
|
3003
|
+
untracked(() => {
|
|
3004
|
+
void this.store.initialize().then(() => this.store.refreshConversations());
|
|
3005
|
+
this.refreshPageContext();
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
});
|
|
3009
|
+
// The drawer stays open while the user navigates; keep the page-context chip current.
|
|
3010
|
+
const navigation = this.router?.events.subscribe(event => {
|
|
3011
|
+
if (event instanceof NavigationEnd && this.panel.isOpen()) {
|
|
3012
|
+
this.refreshPageContext();
|
|
3013
|
+
}
|
|
3014
|
+
});
|
|
3015
|
+
inject(DestroyRef).onDestroy(() => navigation?.unsubscribe());
|
|
3016
|
+
}
|
|
3017
|
+
ngAfterViewInit() {
|
|
3018
|
+
this.portal = new TemplatePortal(this.drawer(), this.viewContainerRef);
|
|
3019
|
+
this.panel.registerPanel(this.portal);
|
|
3020
|
+
}
|
|
3021
|
+
ngOnDestroy() {
|
|
3022
|
+
if (this.portal) {
|
|
3023
|
+
this.panel.unregisterPanel(this.portal);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
refreshPageContext() {
|
|
3027
|
+
void this.store.refreshPageContext();
|
|
3028
|
+
}
|
|
3029
|
+
describeContext(context) {
|
|
3030
|
+
return describeNhAssistantClientContext(context);
|
|
3031
|
+
}
|
|
3032
|
+
toggleConversations() {
|
|
3033
|
+
this.showPreferences.set(false);
|
|
3034
|
+
this.showConversations.update(show => !show);
|
|
3035
|
+
}
|
|
3036
|
+
togglePreferences() {
|
|
3037
|
+
this.showConversations.set(false);
|
|
3038
|
+
this.showPreferences.update(show => !show);
|
|
3039
|
+
}
|
|
3040
|
+
newConversation() {
|
|
3041
|
+
this.store.startNewConversation();
|
|
3042
|
+
this.showConversations.set(false);
|
|
3043
|
+
}
|
|
3044
|
+
openConversation(conversationId) {
|
|
3045
|
+
void this.store.openConversation(conversationId);
|
|
3046
|
+
this.showConversations.set(false);
|
|
3047
|
+
}
|
|
3048
|
+
deleteConversation(conversationId) {
|
|
3049
|
+
void this.store.deleteConversation(conversationId);
|
|
3050
|
+
}
|
|
3051
|
+
send(text) {
|
|
3052
|
+
this.showConversations.set(false);
|
|
3053
|
+
this.showPreferences.set(false);
|
|
3054
|
+
void this.store.send(text);
|
|
3055
|
+
}
|
|
3056
|
+
decide(decision) {
|
|
3057
|
+
this.store.decide(decision);
|
|
3058
|
+
}
|
|
3059
|
+
cancel() {
|
|
3060
|
+
void this.store.cancel();
|
|
3061
|
+
}
|
|
3062
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3063
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhAssistantPanelComponent, isStandalone: true, selector: "nh-assistant-panel", viewQueries: [{ propertyName: "drawer", first: true, predicate: ["drawer"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-template #drawer>\n <section class=\"drawer\" role=\"dialog\" aria-modal=\"false\" [attr.aria-labelledby]=\"titleId\">\n <header class=\"header\">\n <div class=\"heading\">\n <h2 [id]=\"titleId\">{{ 'nh-assistant.panel.title' | translate }}</h2>\n @if (store.enabled()) {\n <nh-assistant-agent-picker\n [agents]=\"store.agents()\"\n [selectedAgentId]=\"store.selectedAgentId()\"\n [disabled]=\"store.streaming()\"\n (agentChange)=\"store.selectAgent($event)\" />\n }\n </div>\n <div class=\"header-actions\">\n @if (store.enabled()) {\n <button\n class=\"icon-button\"\n type=\"button\"\n [class.active]=\"showConversations()\"\n [attr.aria-expanded]=\"showConversations()\"\n [attr.aria-label]=\"(showConversations() ? 'nh-assistant.panel.hide-conversations' : 'nh-assistant.panel.show-conversations') | translate\"\n [attr.title]=\"(showConversations() ? 'nh-assistant.panel.hide-conversations' : 'nh-assistant.panel.show-conversations') | translate\"\n (click)=\"toggleConversations()\">\n <nh-assistant-icon name=\"list\" />\n </button>\n <button\n class=\"icon-button\"\n type=\"button\"\n [class.active]=\"showPreferences()\"\n [attr.aria-expanded]=\"showPreferences()\"\n [attr.aria-label]=\"'nh-assistant.panel.open-preferences' | translate\"\n [attr.title]=\"'nh-assistant.panel.open-preferences' | translate\"\n (click)=\"togglePreferences()\">\n <nh-assistant-icon name=\"settings\" />\n </button>\n @if (adminRoute && store.canAdminister()) {\n <a\n class=\"icon-button\"\n [routerLink]=\"adminRoute\"\n [attr.aria-label]=\"'nh-assistant.panel.open-admin' | translate\"\n [attr.title]=\"'nh-assistant.panel.open-admin' | translate\"\n (click)=\"panel.close()\">\n <nh-assistant-icon name=\"admin\" />\n </a>\n }\n <button\n class=\"icon-button\"\n type=\"button\"\n [disabled]=\"store.streaming()\"\n [attr.aria-label]=\"'nh-assistant.panel.new-conversation' | translate\"\n [attr.title]=\"'nh-assistant.panel.new-conversation' | translate\"\n (click)=\"newConversation()\">\n <nh-assistant-icon name=\"plus\" />\n </button>\n }\n <button\n class=\"icon-button\"\n type=\"button\"\n [attr.aria-label]=\"'nh-assistant.panel.close' | translate\"\n [attr.title]=\"'nh-assistant.panel.close' | translate\"\n (click)=\"panel.close()\">\n <nh-assistant-icon name=\"close\" />\n </button>\n </div>\n </header>\n\n <div class=\"body\">\n @if (store.statusLoading() && !store.enabled()) {\n <div class=\"state\" role=\"status\">\n <p class=\"state-title\">{{ 'nh-assistant.panel.loading' | translate }}</p>\n </div>\n } @else if (!store.enabled()) {\n <div class=\"state\">\n <nh-assistant-icon class=\"state-icon\" name=\"warning\" />\n <p class=\"state-title\">{{ 'nh-assistant.panel.unavailable-title' | translate }}</p>\n <p>{{ 'nh-assistant.panel.unavailable-description' | translate }}</p>\n </div>\n } @else {\n @if (showPreferences()) {\n <nh-assistant-preferences class=\"overlay-view\" (closed)=\"showPreferences.set(false)\" />\n }\n\n @if (showConversations()) {\n <nh-assistant-conversation-list\n class=\"overlay-view\"\n [conversations]=\"store.conversations()\"\n [total]=\"store.conversationsTotal()\"\n [activeConversationId]=\"conversation()?.id ?? null\"\n [loading]=\"store.conversationsLoading()\"\n [disabled]=\"store.streaming()\"\n (conversationSelect)=\"openConversation($event)\"\n (conversationDelete)=\"deleteConversation($event)\"\n (conversationCreate)=\"newConversation()\" />\n }\n\n @if (store.conversationLoading()) {\n <div class=\"state\" role=\"status\">\n <p class=\"state-title\">{{ 'nh-assistant.thread.loading' | translate }}</p>\n </div>\n } @else if (messages().length === 0 && !store.streaming()) {\n <div class=\"state\">\n <nh-assistant-icon class=\"state-icon\" name=\"assistant\" />\n <p class=\"state-title\">{{ 'nh-assistant.panel.empty-title' | translate }}</p>\n <p>{{ 'nh-assistant.panel.empty-description' | translate }}</p>\n </div>\n } @else {\n <nh-assistant-thread\n class=\"thread\"\n [messages]=\"messages()\"\n [streaming]=\"store.streaming()\"\n [deciding]=\"store.deciding()\"\n [markdown]=\"markdown\"\n (approvalDecision)=\"decide($event)\" />\n }\n }\n </div>\n\n @if (store.error(); as error) {\n <div class=\"error\" role=\"alert\">\n <nh-assistant-icon name=\"warning\" />\n <p>{{ errorKeys() | nhAssistantTranslate: error.code }}</p>\n <button\n class=\"icon-button\"\n type=\"button\"\n [attr.aria-label]=\"'nh-assistant.panel.dismiss-error' | translate\"\n [attr.title]=\"'nh-assistant.panel.dismiss-error' | translate\"\n (click)=\"store.clearError()\">\n <nh-assistant-icon name=\"close\" />\n </button>\n </div>\n }\n\n @if (store.enabled() && !showPreferences()) {\n <footer class=\"footer\" (focusin)=\"refreshPageContext()\">\n @if (closed()) {\n <p class=\"notice\">{{ 'nh-assistant.panel.conversation-closed' | translate }}</p>\n }\n @if (store.pageContext(); as context) {\n <div class=\"context-chip\" [class.excluded]=\"store.pageContextExcluded()\" [attr.title]=\"context.route\">\n <nh-assistant-icon name=\"page\" />\n <span class=\"context-text\">\n {{ (store.pageContextExcluded() ? 'nh-assistant.page-context.excluded' : 'nh-assistant.page-context.included') | translate: { page: describeContext(context) } }}\n </span>\n <button\n class=\"icon-button small\"\n type=\"button\"\n [attr.aria-pressed]=\"store.pageContextExcluded()\"\n [attr.aria-label]=\"(store.pageContextExcluded() ? 'nh-assistant.page-context.include' : 'nh-assistant.page-context.exclude') | translate: { page: describeContext(context) }\"\n [attr.title]=\"(store.pageContextExcluded() ? 'nh-assistant.page-context.include' : 'nh-assistant.page-context.exclude') | translate: { page: describeContext(context) }\"\n (click)=\"store.setPageContextExcluded(!store.pageContextExcluded())\">\n <nh-assistant-icon [name]=\"store.pageContextExcluded() ? 'plus' : 'x'\" />\n </button>\n </div>\n }\n <nh-assistant-composer\n [disabled]=\"composerDisabled()\"\n [busy]=\"busy()\"\n [maxLength]=\"store.limits()?.maxMessageChars ?? null\"\n [restore]=\"store.restoredDraft()\"\n (restored)=\"store.consumeRestoredDraft()\"\n (send)=\"send($event)\"\n (cancel)=\"cancel()\" />\n </footer>\n }\n </section>\n</ng-template>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}.drawer{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){.drawer{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:contents}.drawer{position:relative;display:flex;flex-direction:column;box-sizing:border-box;width:100%;height:100%;border-left:1px solid var(--nha-border);color:var(--nha-text);background:var(--nha-background);box-shadow:var(--nha-shadow);font-family:var(--nha-font);font-size:14px}.drawer *,.drawer *:before,.drawer *:after{box-sizing:border-box}.header{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:6px 12px;border-bottom:1px solid var(--nha-border);padding:12px 12px 12px 16px}.heading{display:contents}nh-assistant-agent-picker{grid-column:1/-1;grid-row:2}h2{grid-row:1;min-width:0;margin:0;overflow:hidden;font-size:16px;font-weight:750;letter-spacing:-.01em;text-overflow:ellipsis;white-space:nowrap}.header-actions{display:flex;flex:none;grid-column:2;grid-row:1;gap:4px}.icon-button{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1px solid transparent;border-radius:var(--nha-radius-control);color:var(--nha-text-muted);background:transparent;cursor:pointer;text-decoration:none}.icon-button:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.icon-button:hover:not(:disabled){color:var(--nha-text);background:var(--nha-surface-strong)}.icon-button:disabled{cursor:not-allowed;opacity:.5}.icon-button.active{color:var(--nha-highlight);background:var(--nha-surface-strong)}.body{position:relative;display:flex;flex:1;flex-direction:column;min-height:0;background:var(--nha-background)}.thread{flex:1;min-height:0}.overlay-view{position:absolute;z-index:1;inset:0;border-bottom:1px solid var(--nha-border);background:var(--nha-background)}.state{display:flex;flex:1;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:24px;color:var(--nha-text-muted);text-align:center}.state p{max-width:32ch;margin:0;font-size:13px}.state-icon{margin-bottom:8px;color:var(--nha-highlight);font-size:28px}.state .state-title{color:var(--nha-text);font-size:15px;font-weight:700}.error{display:flex;align-items:center;gap:8px;margin:0 12px 8px;border:1px solid var(--nha-danger);border-radius:var(--nha-radius-control);padding:4px 4px 4px 12px;color:var(--nha-danger);background:var(--nha-danger-soft)}.error p{flex:1;margin:0;color:var(--nha-text);font-size:13px}.footer{border-top:1px solid var(--nha-border);padding:12px 16px 14px}.context-chip{display:flex;align-items:center;gap:6px;max-width:100%;margin:0 0 8px;border:1px solid var(--nha-border);border-radius:999px;padding:2px 2px 2px 10px;width:fit-content;color:var(--nha-text-muted);background:var(--nha-surface);font-size:12px}.context-chip.excluded .context-text{text-decoration:line-through}.context-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.icon-button.small{flex:none;width:26px;height:26px;border-radius:999px}.notice{margin:0 0 8px;color:var(--nha-text-muted);font-size:12px}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: NhAssistantIconComponent, selector: "nh-assistant-icon", inputs: ["name"] }, { kind: "component", type: NhAssistantAgentPickerComponent, selector: "nh-assistant-agent-picker", inputs: ["agents", "selectedAgentId", "disabled"], outputs: ["agentChange"] }, { kind: "component", type: NhAssistantComposerComponent, selector: "nh-assistant-composer", inputs: ["disabled", "busy", "maxLength", "restore"], outputs: ["send", "cancel", "restored"] }, { kind: "component", type: NhAssistantConversationListComponent, selector: "nh-assistant-conversation-list", inputs: ["conversations", "total", "activeConversationId", "loading", "disabled"], outputs: ["conversationSelect", "conversationDelete", "conversationCreate"] }, { kind: "component", type: NhAssistantPreferencesComponent, selector: "nh-assistant-preferences", outputs: ["closed", "saved"] }, { kind: "component", type: NhAssistantThreadComponent, selector: "nh-assistant-thread", inputs: ["messages", "streaming", "deciding", "markdown"], outputs: ["approvalDecision"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "pipe", type: NhAssistantTranslatePipe, name: "nhAssistantTranslate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3064
|
+
}
|
|
3065
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhAssistantPanelComponent, decorators: [{
|
|
3066
|
+
type: Component,
|
|
3067
|
+
args: [{ selector: 'nh-assistant-panel', standalone: true, imports: [
|
|
3068
|
+
RouterLink,
|
|
3069
|
+
TranslatePipe,
|
|
3070
|
+
NhAssistantTranslatePipe,
|
|
3071
|
+
NhAssistantIconComponent,
|
|
3072
|
+
NhAssistantAgentPickerComponent,
|
|
3073
|
+
NhAssistantComposerComponent,
|
|
3074
|
+
NhAssistantConversationListComponent,
|
|
3075
|
+
NhAssistantPreferencesComponent,
|
|
3076
|
+
NhAssistantThreadComponent
|
|
3077
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-template #drawer>\n <section class=\"drawer\" role=\"dialog\" aria-modal=\"false\" [attr.aria-labelledby]=\"titleId\">\n <header class=\"header\">\n <div class=\"heading\">\n <h2 [id]=\"titleId\">{{ 'nh-assistant.panel.title' | translate }}</h2>\n @if (store.enabled()) {\n <nh-assistant-agent-picker\n [agents]=\"store.agents()\"\n [selectedAgentId]=\"store.selectedAgentId()\"\n [disabled]=\"store.streaming()\"\n (agentChange)=\"store.selectAgent($event)\" />\n }\n </div>\n <div class=\"header-actions\">\n @if (store.enabled()) {\n <button\n class=\"icon-button\"\n type=\"button\"\n [class.active]=\"showConversations()\"\n [attr.aria-expanded]=\"showConversations()\"\n [attr.aria-label]=\"(showConversations() ? 'nh-assistant.panel.hide-conversations' : 'nh-assistant.panel.show-conversations') | translate\"\n [attr.title]=\"(showConversations() ? 'nh-assistant.panel.hide-conversations' : 'nh-assistant.panel.show-conversations') | translate\"\n (click)=\"toggleConversations()\">\n <nh-assistant-icon name=\"list\" />\n </button>\n <button\n class=\"icon-button\"\n type=\"button\"\n [class.active]=\"showPreferences()\"\n [attr.aria-expanded]=\"showPreferences()\"\n [attr.aria-label]=\"'nh-assistant.panel.open-preferences' | translate\"\n [attr.title]=\"'nh-assistant.panel.open-preferences' | translate\"\n (click)=\"togglePreferences()\">\n <nh-assistant-icon name=\"settings\" />\n </button>\n @if (adminRoute && store.canAdminister()) {\n <a\n class=\"icon-button\"\n [routerLink]=\"adminRoute\"\n [attr.aria-label]=\"'nh-assistant.panel.open-admin' | translate\"\n [attr.title]=\"'nh-assistant.panel.open-admin' | translate\"\n (click)=\"panel.close()\">\n <nh-assistant-icon name=\"admin\" />\n </a>\n }\n <button\n class=\"icon-button\"\n type=\"button\"\n [disabled]=\"store.streaming()\"\n [attr.aria-label]=\"'nh-assistant.panel.new-conversation' | translate\"\n [attr.title]=\"'nh-assistant.panel.new-conversation' | translate\"\n (click)=\"newConversation()\">\n <nh-assistant-icon name=\"plus\" />\n </button>\n }\n <button\n class=\"icon-button\"\n type=\"button\"\n [attr.aria-label]=\"'nh-assistant.panel.close' | translate\"\n [attr.title]=\"'nh-assistant.panel.close' | translate\"\n (click)=\"panel.close()\">\n <nh-assistant-icon name=\"close\" />\n </button>\n </div>\n </header>\n\n <div class=\"body\">\n @if (store.statusLoading() && !store.enabled()) {\n <div class=\"state\" role=\"status\">\n <p class=\"state-title\">{{ 'nh-assistant.panel.loading' | translate }}</p>\n </div>\n } @else if (!store.enabled()) {\n <div class=\"state\">\n <nh-assistant-icon class=\"state-icon\" name=\"warning\" />\n <p class=\"state-title\">{{ 'nh-assistant.panel.unavailable-title' | translate }}</p>\n <p>{{ 'nh-assistant.panel.unavailable-description' | translate }}</p>\n </div>\n } @else {\n @if (showPreferences()) {\n <nh-assistant-preferences class=\"overlay-view\" (closed)=\"showPreferences.set(false)\" />\n }\n\n @if (showConversations()) {\n <nh-assistant-conversation-list\n class=\"overlay-view\"\n [conversations]=\"store.conversations()\"\n [total]=\"store.conversationsTotal()\"\n [activeConversationId]=\"conversation()?.id ?? null\"\n [loading]=\"store.conversationsLoading()\"\n [disabled]=\"store.streaming()\"\n (conversationSelect)=\"openConversation($event)\"\n (conversationDelete)=\"deleteConversation($event)\"\n (conversationCreate)=\"newConversation()\" />\n }\n\n @if (store.conversationLoading()) {\n <div class=\"state\" role=\"status\">\n <p class=\"state-title\">{{ 'nh-assistant.thread.loading' | translate }}</p>\n </div>\n } @else if (messages().length === 0 && !store.streaming()) {\n <div class=\"state\">\n <nh-assistant-icon class=\"state-icon\" name=\"assistant\" />\n <p class=\"state-title\">{{ 'nh-assistant.panel.empty-title' | translate }}</p>\n <p>{{ 'nh-assistant.panel.empty-description' | translate }}</p>\n </div>\n } @else {\n <nh-assistant-thread\n class=\"thread\"\n [messages]=\"messages()\"\n [streaming]=\"store.streaming()\"\n [deciding]=\"store.deciding()\"\n [markdown]=\"markdown\"\n (approvalDecision)=\"decide($event)\" />\n }\n }\n </div>\n\n @if (store.error(); as error) {\n <div class=\"error\" role=\"alert\">\n <nh-assistant-icon name=\"warning\" />\n <p>{{ errorKeys() | nhAssistantTranslate: error.code }}</p>\n <button\n class=\"icon-button\"\n type=\"button\"\n [attr.aria-label]=\"'nh-assistant.panel.dismiss-error' | translate\"\n [attr.title]=\"'nh-assistant.panel.dismiss-error' | translate\"\n (click)=\"store.clearError()\">\n <nh-assistant-icon name=\"close\" />\n </button>\n </div>\n }\n\n @if (store.enabled() && !showPreferences()) {\n <footer class=\"footer\" (focusin)=\"refreshPageContext()\">\n @if (closed()) {\n <p class=\"notice\">{{ 'nh-assistant.panel.conversation-closed' | translate }}</p>\n }\n @if (store.pageContext(); as context) {\n <div class=\"context-chip\" [class.excluded]=\"store.pageContextExcluded()\" [attr.title]=\"context.route\">\n <nh-assistant-icon name=\"page\" />\n <span class=\"context-text\">\n {{ (store.pageContextExcluded() ? 'nh-assistant.page-context.excluded' : 'nh-assistant.page-context.included') | translate: { page: describeContext(context) } }}\n </span>\n <button\n class=\"icon-button small\"\n type=\"button\"\n [attr.aria-pressed]=\"store.pageContextExcluded()\"\n [attr.aria-label]=\"(store.pageContextExcluded() ? 'nh-assistant.page-context.include' : 'nh-assistant.page-context.exclude') | translate: { page: describeContext(context) }\"\n [attr.title]=\"(store.pageContextExcluded() ? 'nh-assistant.page-context.include' : 'nh-assistant.page-context.exclude') | translate: { page: describeContext(context) }\"\n (click)=\"store.setPageContextExcluded(!store.pageContextExcluded())\">\n <nh-assistant-icon [name]=\"store.pageContextExcluded() ? 'plus' : 'x'\" />\n </button>\n </div>\n }\n <nh-assistant-composer\n [disabled]=\"composerDisabled()\"\n [busy]=\"busy()\"\n [maxLength]=\"store.limits()?.maxMessageChars ?? null\"\n [restore]=\"store.restoredDraft()\"\n (restored)=\"store.consumeRestoredDraft()\"\n (send)=\"send($event)\"\n (cancel)=\"cancel()\" />\n </footer>\n }\n </section>\n</ng-template>\n", styles: [":host{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){:host{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}.drawer{--nha-font: var(--nh-assistant-font, inherit);--nha-background: var(--nh-assistant-background, #ffffff);--nha-surface: var(--nh-assistant-surface, #f5f7fb);--nha-surface-strong: var(--nh-assistant-surface-strong, #eceff6);--nha-text: var(--nh-assistant-text, #080c1d);--nha-text-muted: var(--nh-assistant-text-muted, #5b6482);--nha-border: var(--nh-assistant-border, #dce0ec);--nha-accent: var(--nh-assistant-accent, #022962);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #c84f00);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #247d4e);--nha-success-soft: var(--nh-assistant-success-soft, #e6f5ec);--nha-warning: var(--nh-assistant-warning, #8a5a00);--nha-warning-soft: var(--nh-assistant-warning-soft, #fff3d9);--nha-danger: var(--nh-assistant-danger, #b92e52);--nha-danger-soft: var(--nh-assistant-danger-soft, #ffe9ef);--nha-focus: var(--nh-assistant-focus, #ff6700);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(2 41 98 / .16));--nha-radius: var(--nh-assistant-radius, 16px);--nha-radius-control: var(--nh-assistant-radius-control, 12px);--nha-mono: var(--nh-assistant-font-mono, ui-monospace, \"Cascadia Code\", Consolas, monospace)}@media(prefers-color-scheme:dark){.drawer{--nha-background: var(--nh-assistant-background, #0b0e22);--nha-surface: var(--nh-assistant-surface, #11152d);--nha-surface-strong: var(--nh-assistant-surface-strong, #181d3b);--nha-text: var(--nh-assistant-text, #ffffff);--nha-text-muted: var(--nh-assistant-text-muted, #b8bed8);--nha-border: var(--nh-assistant-border, #2b3155);--nha-accent: var(--nh-assistant-accent, #0f4087);--nha-accent-contrast: var(--nh-assistant-accent-contrast, #ffffff);--nha-highlight: var(--nh-assistant-highlight, #ff6700);--nha-user-bubble: var(--nh-assistant-user-bubble, #022962);--nha-user-text: var(--nh-assistant-user-text, #ffffff);--nha-success: var(--nh-assistant-success, #67c58f);--nha-success-soft: var(--nh-assistant-success-soft, #102d24);--nha-warning: var(--nh-assistant-warning, #ffbf5b);--nha-warning-soft: var(--nh-assistant-warning-soft, #342818);--nha-danger: var(--nh-assistant-danger, #ff6d8a);--nha-danger-soft: var(--nh-assistant-danger-soft, #351b2a);--nha-shadow: var(--nh-assistant-shadow, 0 16px 36px rgb(0 0 0 / .32))}}:host{display:contents}.drawer{position:relative;display:flex;flex-direction:column;box-sizing:border-box;width:100%;height:100%;border-left:1px solid var(--nha-border);color:var(--nha-text);background:var(--nha-background);box-shadow:var(--nha-shadow);font-family:var(--nha-font);font-size:14px}.drawer *,.drawer *:before,.drawer *:after{box-sizing:border-box}.header{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:6px 12px;border-bottom:1px solid var(--nha-border);padding:12px 12px 12px 16px}.heading{display:contents}nh-assistant-agent-picker{grid-column:1/-1;grid-row:2}h2{grid-row:1;min-width:0;margin:0;overflow:hidden;font-size:16px;font-weight:750;letter-spacing:-.01em;text-overflow:ellipsis;white-space:nowrap}.header-actions{display:flex;flex:none;grid-column:2;grid-row:1;gap:4px}.icon-button{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:1px solid transparent;border-radius:var(--nha-radius-control);color:var(--nha-text-muted);background:transparent;cursor:pointer;text-decoration:none}.icon-button:focus-visible{outline:3px solid color-mix(in srgb,var(--nha-focus) 80%,transparent);outline-offset:2px}.icon-button:hover:not(:disabled){color:var(--nha-text);background:var(--nha-surface-strong)}.icon-button:disabled{cursor:not-allowed;opacity:.5}.icon-button.active{color:var(--nha-highlight);background:var(--nha-surface-strong)}.body{position:relative;display:flex;flex:1;flex-direction:column;min-height:0;background:var(--nha-background)}.thread{flex:1;min-height:0}.overlay-view{position:absolute;z-index:1;inset:0;border-bottom:1px solid var(--nha-border);background:var(--nha-background)}.state{display:flex;flex:1;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:24px;color:var(--nha-text-muted);text-align:center}.state p{max-width:32ch;margin:0;font-size:13px}.state-icon{margin-bottom:8px;color:var(--nha-highlight);font-size:28px}.state .state-title{color:var(--nha-text);font-size:15px;font-weight:700}.error{display:flex;align-items:center;gap:8px;margin:0 12px 8px;border:1px solid var(--nha-danger);border-radius:var(--nha-radius-control);padding:4px 4px 4px 12px;color:var(--nha-danger);background:var(--nha-danger-soft)}.error p{flex:1;margin:0;color:var(--nha-text);font-size:13px}.footer{border-top:1px solid var(--nha-border);padding:12px 16px 14px}.context-chip{display:flex;align-items:center;gap:6px;max-width:100%;margin:0 0 8px;border:1px solid var(--nha-border);border-radius:999px;padding:2px 2px 2px 10px;width:fit-content;color:var(--nha-text-muted);background:var(--nha-surface);font-size:12px}.context-chip.excluded .context-text{text-decoration:line-through}.context-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.icon-button.small{flex:none;width:26px;height:26px;border-radius:999px}.notice{margin:0 0 8px;color:var(--nha-text-muted);font-size:12px}\n"] }]
|
|
3078
|
+
}], ctorParameters: () => [], propDecorators: { drawer: [{ type: i0.ViewChild, args: ['drawer', { isSignal: true }] }] } });
|
|
3079
|
+
|
|
3080
|
+
/** Converts any failure into a user-facing code and translation key, never raw text. */
|
|
3081
|
+
function toNhAssistantError(error) {
|
|
3082
|
+
if (error instanceof NhAssistantApiError) {
|
|
3083
|
+
return { code: error.code, messageKey: error.messageKey };
|
|
3084
|
+
}
|
|
3085
|
+
return { code: NhAssistantClientErrorCodes.server, messageKey: nhAssistantErrorMessageKey(NhAssistantClientErrorCodes.server) };
|
|
3086
|
+
}
|
|
3087
|
+
/** Translation keys for an error, from the most to the least specific. */
|
|
3088
|
+
function nhAssistantErrorKeys(error) {
|
|
3089
|
+
return error ? [error.messageKey, nhAssistantErrorMessageKey(error.code), 'nh-assistant.errors.generic'] : [];
|
|
3090
|
+
}
|
|
3091
|
+
/** Dash-case identifiers as the contract requires for agent and MCP server ids. */
|
|
3092
|
+
const NH_ASSISTANT_DASH_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3093
|
+
|
|
3094
|
+
/*
|
|
3095
|
+
* Public API surface of @newheap/platform-ai-chat.
|
|
3096
|
+
*/
|
|
3097
|
+
|
|
3098
|
+
/**
|
|
3099
|
+
* Generated bundle index. Do not edit.
|
|
3100
|
+
*/
|
|
3101
|
+
|
|
3102
|
+
export { NH_ASSISTANT_ACCESS_POLICY, NH_ASSISTANT_CONFIG, NH_ASSISTANT_FETCH, NH_ASSISTANT_ICONS, NH_ASSISTANT_MAX_CUSTOM_INSTRUCTIONS, NH_ASSISTANT_PAGE_CONTEXT_LIMITS, NH_ASSISTANT_PANEL_MOBILE_QUERY, NH_ASSISTANT_PANEL_WIDTH, NH_ASSISTANT_SSE_EVENT_TYPES, NH_ASSISTANT_TRANSLATIONS, NH_ASSISTANT_VIRTUAL_SCROLL_THRESHOLD, NhAssistantAdminApiService, NhAssistantAgentPickerComponent, NhAssistantAllowAllAccessPolicy, NhAssistantApiError, NhAssistantApiService, NhAssistantApprovalCardComponent, NhAssistantClientErrorCodes, NhAssistantComposerComponent, NhAssistantConversationListComponent, NhAssistantLauncherComponent, NhAssistantPanelComponent, NhAssistantPanelService, NhAssistantPreferencesComponent, NhAssistantSseParser, NhAssistantStore, NhAssistantThreadComponent, NhAssistantToolCallCardComponent, NhAssistantTranslationMerger, applyNhAssistantApprovalDecision, applyNhAssistantEvent, describeNhAssistantClientContext, formatNhAssistantCountdown, nhAssistantCodeForStatus, nhAssistantErrorMessageKey, normalizeNhAssistantClientContext, provideNhAssistant, NH_ASSISTANT_DASH_CASE as ɵNH_ASSISTANT_DASH_CASE, NhAssistantIconComponent as ɵNhAssistantIconComponent, NhAssistantTranslatePipe as ɵNhAssistantTranslatePipe, nhAssistantErrorKeys as ɵnhAssistantErrorKeys, toNhAssistantError as ɵtoNhAssistantError };
|
|
3103
|
+
//# sourceMappingURL=newheap-platform-ai-chat.mjs.map
|