@m6d/cortex-client 1.0.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/README.md +42 -0
- package/fesm2022/m6d-cortex-client.mjs +1286 -0
- package/fesm2022/m6d-cortex-client.mjs.map +1 -0
- package/package.json +38 -0
- package/styles.css +2 -0
- package/types/m6d-cortex-client.d.ts +310 -0
|
@@ -0,0 +1,1286 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, NgZone, Injectable, signal, DestroyRef, Pipe, input, ChangeDetectionStrategy, Component, computed, forwardRef, effect, viewChild, model, Injector } from '@angular/core';
|
|
3
|
+
import { Chat } from '@ai-sdk/angular';
|
|
4
|
+
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, isStaticToolUIPart, generateId } from 'ai';
|
|
5
|
+
import { Observable, retry, timer, share, Subject, of, defer, finalize, tap, switchMap, map, concatMap } from 'rxjs';
|
|
6
|
+
import { fromPromise } from 'rxjs/internal/observable/innerFrom';
|
|
7
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
8
|
+
import * as i1$1 from '@angular/forms';
|
|
9
|
+
import { FormControl, Validators, FormBuilder, ReactiveFormsModule, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
|
|
10
|
+
import { NgClass, NgTemplateOutlet } from '@angular/common';
|
|
11
|
+
import { TranslatePipe, TranslateService, provideTranslateService, provideTranslateLoader } from '@ngx-translate/core';
|
|
12
|
+
import { marked } from 'marked';
|
|
13
|
+
import DOMPurify from 'dompurify';
|
|
14
|
+
import * as i1 from '@angular/platform-browser';
|
|
15
|
+
import hljs from 'highlight.js/lib/core';
|
|
16
|
+
import javascript from 'highlight.js/lib/languages/javascript';
|
|
17
|
+
import json from 'highlight.js/lib/languages/json';
|
|
18
|
+
import sql from 'highlight.js/lib/languages/sql';
|
|
19
|
+
|
|
20
|
+
const CORTEX_CLIENT_CONFIG = new InjectionToken('CORTEX_CLIENT_CONFIG');
|
|
21
|
+
|
|
22
|
+
class CortexClientWebSocketService {
|
|
23
|
+
events$;
|
|
24
|
+
constructor() {
|
|
25
|
+
const config = inject(CORTEX_CLIENT_CONFIG);
|
|
26
|
+
const ngZone = inject(NgZone);
|
|
27
|
+
this.events$ = new Observable((subscriber) => {
|
|
28
|
+
const wsUrl = config.wsUrl ?? deriveWsUrl(config.transport.baseUrl);
|
|
29
|
+
let ws;
|
|
30
|
+
Promise.resolve(config.transport.getAuthHeaders()).then((headers) => {
|
|
31
|
+
if (subscriber.closed)
|
|
32
|
+
return;
|
|
33
|
+
const fullWsUrl = appendTokenToUrl(wsUrl, headers);
|
|
34
|
+
ws = new WebSocket(fullWsUrl);
|
|
35
|
+
ws.addEventListener('message', (event) => {
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(event.data);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
ngZone.run(() => subscriber.next(parsed));
|
|
44
|
+
});
|
|
45
|
+
ws.addEventListener('error', (e) => subscriber.error(e));
|
|
46
|
+
ws.addEventListener('close', () => subscriber.error(new Error('WebSocket closed')));
|
|
47
|
+
});
|
|
48
|
+
return () => ws?.close();
|
|
49
|
+
}).pipe(retry({
|
|
50
|
+
delay: (_, retryCount) => timer(Math.min(1000 * 2 ** (retryCount - 1), 30_000)),
|
|
51
|
+
}), share());
|
|
52
|
+
}
|
|
53
|
+
get events() {
|
|
54
|
+
return this.events$;
|
|
55
|
+
}
|
|
56
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CortexClientWebSocketService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
57
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CortexClientWebSocketService });
|
|
58
|
+
}
|
|
59
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CortexClientWebSocketService, decorators: [{
|
|
60
|
+
type: Injectable
|
|
61
|
+
}], ctorParameters: () => [] });
|
|
62
|
+
function deriveWsUrl(baseUrl) {
|
|
63
|
+
const url = typeof baseUrl === 'string' ? baseUrl : baseUrl();
|
|
64
|
+
const parsed = new URL(url, window.location.origin);
|
|
65
|
+
parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
66
|
+
parsed.pathname = parsed.pathname.replace(/\/$/, '') + '/ws';
|
|
67
|
+
return parsed.toString();
|
|
68
|
+
}
|
|
69
|
+
function appendTokenToUrl(wsUrl, headers) {
|
|
70
|
+
const authHeader = headers['Authorization'] ?? headers['authorization'];
|
|
71
|
+
if (!authHeader?.startsWith('Bearer '))
|
|
72
|
+
return wsUrl;
|
|
73
|
+
const token = authHeader.slice(7);
|
|
74
|
+
const separator = wsUrl.includes('?') ? '&' : '?';
|
|
75
|
+
return `${wsUrl}${separator}token=${encodeURIComponent(token)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class CortexChatService {
|
|
79
|
+
threads = signal(undefined, ...(ngDevMode ? [{ debugName: "threads" }] : []));
|
|
80
|
+
selectedThread = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedThread" }] : []));
|
|
81
|
+
chat = signal(undefined, ...(ngDevMode ? [{ debugName: "chat" }] : []));
|
|
82
|
+
isLoadingMessages = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingMessages" }] : []));
|
|
83
|
+
isAgentWorking = signal(false, ...(ngDevMode ? [{ debugName: "isAgentWorking" }] : []));
|
|
84
|
+
hasPendingToolCalls = signal(false, ...(ngDevMode ? [{ debugName: "hasPendingToolCalls" }] : []));
|
|
85
|
+
events = new Subject();
|
|
86
|
+
config = inject(CORTEX_CLIENT_CONFIG);
|
|
87
|
+
wsService = inject(CortexClientWebSocketService);
|
|
88
|
+
destroyRef = inject(DestroyRef);
|
|
89
|
+
constructor() {
|
|
90
|
+
this.reloadThreads().subscribe();
|
|
91
|
+
this.subscribeToWsEvents();
|
|
92
|
+
}
|
|
93
|
+
get events$() {
|
|
94
|
+
return this.events.asObservable();
|
|
95
|
+
}
|
|
96
|
+
selectThread(thread, options) {
|
|
97
|
+
if (this.isLoadingMessages())
|
|
98
|
+
return of();
|
|
99
|
+
if (this.selectedThread()?.id === thread.id)
|
|
100
|
+
return of();
|
|
101
|
+
return (options?.skipLoadingMessages
|
|
102
|
+
? of([])
|
|
103
|
+
: defer(() => {
|
|
104
|
+
this.isLoadingMessages.set(true);
|
|
105
|
+
return fromPromise(this.fetchJson(`/threads/${thread.id}/messages`)).pipe(finalize(() => this.isLoadingMessages.set(false)));
|
|
106
|
+
})).pipe(tap((messages) => {
|
|
107
|
+
const chat = new Chat({
|
|
108
|
+
id: thread.id,
|
|
109
|
+
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
110
|
+
onData: () => {
|
|
111
|
+
this.events.next('onData');
|
|
112
|
+
},
|
|
113
|
+
onToolCall: async ({ toolCall }) => {
|
|
114
|
+
if (!this.config.onToolCall)
|
|
115
|
+
return;
|
|
116
|
+
const tc = toolCall;
|
|
117
|
+
const result = this.config.onToolCall(tc);
|
|
118
|
+
if (result == null)
|
|
119
|
+
return;
|
|
120
|
+
const output = result instanceof Promise ? await result : result;
|
|
121
|
+
chat.addToolOutput({
|
|
122
|
+
tool: tc.toolName,
|
|
123
|
+
toolCallId: tc.toolCallId,
|
|
124
|
+
output,
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
onError: () => {
|
|
128
|
+
this.isAgentWorking.set(false);
|
|
129
|
+
this.hasPendingToolCalls.set(false);
|
|
130
|
+
this.events.next('onError');
|
|
131
|
+
},
|
|
132
|
+
onFinish: () => {
|
|
133
|
+
const lastAssistantMsg = [...chat.messages]
|
|
134
|
+
.reverse()
|
|
135
|
+
.find((m) => m.role === 'assistant');
|
|
136
|
+
const pending = lastAssistantMsg?.parts?.some((p) => p.type.startsWith('tool-') &&
|
|
137
|
+
p.state !== 'output-available' &&
|
|
138
|
+
p.state !== 'output-error' &&
|
|
139
|
+
p.state !== 'output-denied');
|
|
140
|
+
if (pending) {
|
|
141
|
+
this.hasPendingToolCalls.set(true);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
this.isAgentWorking.set(false);
|
|
145
|
+
this.hasPendingToolCalls.set(false);
|
|
146
|
+
}
|
|
147
|
+
this.events.next('onFinish');
|
|
148
|
+
},
|
|
149
|
+
messages: messages,
|
|
150
|
+
transport: new DefaultChatTransport({
|
|
151
|
+
api: this.resolveUrl('/chat'),
|
|
152
|
+
headers: async () => await this.resolveHeaders(),
|
|
153
|
+
prepareSendMessagesRequest: ({ messages, id, body, ...rest }) => {
|
|
154
|
+
this.isAgentWorking.set(true);
|
|
155
|
+
this.hasPendingToolCalls.set(false);
|
|
156
|
+
return {
|
|
157
|
+
...rest,
|
|
158
|
+
body: {
|
|
159
|
+
...body,
|
|
160
|
+
id,
|
|
161
|
+
messages: [messages.at(-1)],
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
fixChat(chat);
|
|
168
|
+
this.chat.set(chat);
|
|
169
|
+
this.selectedThread.set(thread);
|
|
170
|
+
// Check if loaded messages have pending tool calls
|
|
171
|
+
const lastAssistantMsg = [...messages].reverse().find((m) => m.role === 'assistant');
|
|
172
|
+
const pending = lastAssistantMsg?.parts?.some((p) => p.type.startsWith('tool-') &&
|
|
173
|
+
p.state !== 'output-available' &&
|
|
174
|
+
p.state !== 'output-error' &&
|
|
175
|
+
p.state !== 'output-denied');
|
|
176
|
+
if (pending) {
|
|
177
|
+
this.hasPendingToolCalls.set(true);
|
|
178
|
+
}
|
|
179
|
+
this.events.next('onThreadSelected');
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
deselectThread() {
|
|
183
|
+
if (!this.selectedThread())
|
|
184
|
+
return;
|
|
185
|
+
this.abort();
|
|
186
|
+
this.selectedThread.set(undefined);
|
|
187
|
+
this.chat.set(undefined);
|
|
188
|
+
}
|
|
189
|
+
send(prompt) {
|
|
190
|
+
if (this.isAgentWorking())
|
|
191
|
+
return of();
|
|
192
|
+
const ensureThread = this.selectedThread()
|
|
193
|
+
? of(undefined)
|
|
194
|
+
: fromPromise(this.fetchJson('/threads', {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
body: JSON.stringify({ prompt }),
|
|
197
|
+
})).pipe(switchMap((thread) => this.selectThread(thread, { skipLoadingMessages: true }).pipe(map(() => undefined))), tap(() => this.reloadThreads().subscribe()));
|
|
198
|
+
return ensureThread.pipe(switchMap(() => fromPromise(this.chat().sendMessage({ text: prompt }))), tap(() => this.events.next('onSend')));
|
|
199
|
+
}
|
|
200
|
+
abort() {
|
|
201
|
+
const chat = this.chat();
|
|
202
|
+
if (!chat || !this.isAgentWorking())
|
|
203
|
+
return;
|
|
204
|
+
chat.stop();
|
|
205
|
+
this.isAgentWorking.set(false);
|
|
206
|
+
this.hasPendingToolCalls.set(false);
|
|
207
|
+
const threadId = this.selectedThread()?.id;
|
|
208
|
+
if (!threadId)
|
|
209
|
+
return;
|
|
210
|
+
this.reloadMessages(threadId).subscribe((messages) => {
|
|
211
|
+
if (!this.chat())
|
|
212
|
+
return;
|
|
213
|
+
this.chat().messages = messages;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
deleteThread(threadId) {
|
|
217
|
+
if (this.selectedThread()?.id === threadId)
|
|
218
|
+
this.deselectThread();
|
|
219
|
+
return fromPromise(this.fetchJson(`/threads/${threadId}`, { method: 'DELETE' })).pipe(concatMap(() => this.reloadThreads()));
|
|
220
|
+
}
|
|
221
|
+
subscribeToWsEvents() {
|
|
222
|
+
this.wsService.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
|
|
223
|
+
switch (event.type) {
|
|
224
|
+
case 'thread:title-updated':
|
|
225
|
+
this.onThreadTitleUpdated(event.payload);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
onThreadTitleUpdated(payload) {
|
|
231
|
+
const current = this.threads();
|
|
232
|
+
if (current) {
|
|
233
|
+
this.threads.set(current.map((t) => (t.id === payload.threadId ? { ...t, title: payload.title } : t)));
|
|
234
|
+
}
|
|
235
|
+
const selected = this.selectedThread();
|
|
236
|
+
if (selected?.id === payload.threadId) {
|
|
237
|
+
this.selectedThread.set({ ...selected, title: payload.title });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
reloadThreads() {
|
|
241
|
+
return fromPromise(this.fetchJson('/threads')).pipe(tap((threads) => this.threads.set(threads)));
|
|
242
|
+
}
|
|
243
|
+
reloadMessages(threadId) {
|
|
244
|
+
return fromPromise(this.fetchJson(`/threads/${threadId}/messages`));
|
|
245
|
+
}
|
|
246
|
+
resolveUrl(path) {
|
|
247
|
+
const baseUrl = this.config.transport.baseUrl;
|
|
248
|
+
const base = typeof baseUrl === 'string' ? baseUrl : baseUrl();
|
|
249
|
+
return base.replace(/\/$/, '') + path;
|
|
250
|
+
}
|
|
251
|
+
async resolveHeaders() {
|
|
252
|
+
return await Promise.resolve(this.config.transport.getAuthHeaders());
|
|
253
|
+
}
|
|
254
|
+
async fetchJson(path, init) {
|
|
255
|
+
const url = this.resolveUrl(path);
|
|
256
|
+
const headers = await this.resolveHeaders();
|
|
257
|
+
const res = await fetch(url, {
|
|
258
|
+
...init,
|
|
259
|
+
headers: {
|
|
260
|
+
'Content-Type': 'application/json',
|
|
261
|
+
...headers,
|
|
262
|
+
...init?.headers,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
if (!res.ok)
|
|
266
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
267
|
+
const text = await res.text();
|
|
268
|
+
return text ? JSON.parse(text) : undefined;
|
|
269
|
+
}
|
|
270
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CortexChatService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
271
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CortexChatService });
|
|
272
|
+
}
|
|
273
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CortexChatService, decorators: [{
|
|
274
|
+
type: Injectable
|
|
275
|
+
}], ctorParameters: () => [] });
|
|
276
|
+
/**
|
|
277
|
+
* Override snapshot to avoid structuredClone failing on
|
|
278
|
+
* Object.defineProperty getter/setter parts.
|
|
279
|
+
*/
|
|
280
|
+
function fixChat(chat) {
|
|
281
|
+
function deepClone(value) {
|
|
282
|
+
if (value === null || value === undefined)
|
|
283
|
+
return value;
|
|
284
|
+
if (typeof value !== 'object')
|
|
285
|
+
return value;
|
|
286
|
+
if (Array.isArray(value))
|
|
287
|
+
return value.map(deepClone);
|
|
288
|
+
const result = {};
|
|
289
|
+
for (const key of Object.keys(value)) {
|
|
290
|
+
result[key] = deepClone(value[key]);
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
chat.state.snapshot = deepClone;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
class MarkedPipe {
|
|
298
|
+
sanitizer;
|
|
299
|
+
constructor(sanitizer) {
|
|
300
|
+
this.sanitizer = sanitizer;
|
|
301
|
+
}
|
|
302
|
+
transform(value) {
|
|
303
|
+
if (!value) {
|
|
304
|
+
return '';
|
|
305
|
+
}
|
|
306
|
+
let html = marked.parse(value, { async: false });
|
|
307
|
+
html = html
|
|
308
|
+
.replace(/<table>/g, '<div style="overflow-x:auto"><table style="width:auto">')
|
|
309
|
+
.replace(/<\/table>/g, '</table></div>')
|
|
310
|
+
.replace(/<(t[hd])([\s>])/g, '<$1 style="padding:0.5rem 1rem"$2');
|
|
311
|
+
return this.sanitizer.bypassSecurityTrustHtml(DOMPurify.sanitize(html));
|
|
312
|
+
}
|
|
313
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MarkedPipe, deps: [{ token: i1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Pipe });
|
|
314
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.0", ngImport: i0, type: MarkedPipe, isStandalone: true, name: "marked" });
|
|
315
|
+
}
|
|
316
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MarkedPipe, decorators: [{
|
|
317
|
+
type: Pipe,
|
|
318
|
+
args: [{
|
|
319
|
+
name: 'marked',
|
|
320
|
+
standalone: true,
|
|
321
|
+
}]
|
|
322
|
+
}], ctorParameters: () => [{ type: i1.DomSanitizer }] });
|
|
323
|
+
|
|
324
|
+
class MessageTextPartComponent {
|
|
325
|
+
role = input.required(...(ngDevMode ? [{ debugName: "role" }] : []));
|
|
326
|
+
textPart = input.required(...(ngDevMode ? [{ debugName: "textPart" }] : []));
|
|
327
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageTextPartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
328
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.0", type: MessageTextPartComponent, isStandalone: true, selector: "cortex-message-text-part", inputs: { role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: true, transformFunction: null }, textPart: { classPropertyName: "textPart", publicName: "textPart", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"w-full flex flex-col\"\n [ngClass]=\"{\n 'items-end': role() === 'assistant',\n 'items-start': role() === 'user',\n }\"\n>\n <div\n class=\"max-w-4/5 text-[13px] leading-relaxed p-4 rounded-2xl border border-slate-200 prose\"\n [ngClass]=\"{\n 'bg-slate-100 text-slate-700 ltr:rounded-br-none rtl:rounded-bl-none': role() === 'assistant',\n 'bg-slate-50 text-slate-700 ltr:rounded-bl-none rtl:rounded-br-none': role() === 'user',\n }\"\n [innerHTML]=\"textPart().text | marked\"\n ></div>\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: MarkedPipe, name: "marked" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
329
|
+
}
|
|
330
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageTextPartComponent, decorators: [{
|
|
331
|
+
type: Component,
|
|
332
|
+
args: [{ selector: 'cortex-message-text-part', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, MarkedPipe], template: "<div\n class=\"w-full flex flex-col\"\n [ngClass]=\"{\n 'items-end': role() === 'assistant',\n 'items-start': role() === 'user',\n }\"\n>\n <div\n class=\"max-w-4/5 text-[13px] leading-relaxed p-4 rounded-2xl border border-slate-200 prose\"\n [ngClass]=\"{\n 'bg-slate-100 text-slate-700 ltr:rounded-br-none rtl:rounded-bl-none': role() === 'assistant',\n 'bg-slate-50 text-slate-700 ltr:rounded-bl-none rtl:rounded-br-none': role() === 'user',\n }\"\n [innerHTML]=\"textPart().text | marked\"\n ></div>\n</div>\n" }]
|
|
333
|
+
}], propDecorators: { role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: true }] }], textPart: [{ type: i0.Input, args: [{ isSignal: true, alias: "textPart", required: true }] }] } });
|
|
334
|
+
|
|
335
|
+
class MessageReasoningPartComponent {
|
|
336
|
+
reasoningPart = input.required(...(ngDevMode ? [{ debugName: "reasoningPart" }] : []));
|
|
337
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageReasoningPartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
338
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: MessageReasoningPartComponent, isStandalone: true, selector: "cortex-message-reasoning-part", inputs: { reasoningPart: { classPropertyName: "reasoningPart", publicName: "reasoningPart", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "@let p = reasoningPart();\n<details class=\"group w-full rounded-lg border border-slate-200 bg-slate-50 overflow-hidden\">\n <summary\n class=\"list-none [&::-webkit-details-marker]:hidden cursor-pointer select-none px-4 py-3 flex items-center gap-3\"\n >\n <div class=\"flex items-center gap-2 min-w-0 flex-1\">\n <span\n class=\"inline-flex h-6 w-6 items-center justify-center rounded bg-indigo-100 text-indigo-600\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path\n d=\"M10 2C6.686 2 4 4.686 4 8c0 1.655.672 3.154 1.757 4.243.362.363.576.858.576 1.371V14.5a1 1 0 0 0 1 1h5.334a1 1 0 0 0 1-1v-.886c0-.513.214-1.008.576-1.371A5.978 5.978 0 0 0 16 8c0-3.314-2.686-6-6-6Z\"\n stroke=\"currentColor\"\n stroke-width=\"1.4\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n <path\n d=\"M7.5 17.5h5M8.5 8a2 2 0 0 1 2-2\"\n stroke=\"currentColor\"\n stroke-width=\"1.4\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </span>\n\n <div class=\"min-w-0\">\n <div class=\"flex items-center gap-2\">\n <div class=\"font-medium text-slate-700 text-sm\">Reasoning</div>\n @if (p.state) {\n <span\n class=\"inline-flex items-center rounded px-1.5 py-0.5 text-[11px] font-medium\"\n [class.bg-amber-100.text-amber-700]=\"p.state === 'streaming'\"\n [class.bg-emerald-100.text-emerald-700]=\"p.state === 'done'\"\n >\n {{ p.state === 'streaming' ? 'Streaming' : 'Done' }}\n </span>\n }\n </div>\n </div>\n </div>\n\n <span\n class=\"ml-auto inline-flex h-6 w-6 items-center justify-center rounded text-slate-400 transition group-open:rotate-180\"\n aria-hidden=\"true\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path\n d=\"m5.75 8.25 4.25 4.25 4.25-4.25\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </span>\n </summary>\n\n <div class=\"px-4 pb-4\">\n <div class=\"mt-1 rounded bg-white border border-slate-200 p-3\">\n <pre\n class=\"m-0 whitespace-pre-wrap wrap-break-word font-mono text-xs leading-relaxed text-slate-700\"\n >{{ p.text.trim() ? p.text : 'No reasoning provided.' }}</pre\n >\n </div>\n </div>\n</details>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
339
|
+
}
|
|
340
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageReasoningPartComponent, decorators: [{
|
|
341
|
+
type: Component,
|
|
342
|
+
args: [{ selector: 'cortex-message-reasoning-part', changeDetection: ChangeDetectionStrategy.OnPush, template: "@let p = reasoningPart();\n<details class=\"group w-full rounded-lg border border-slate-200 bg-slate-50 overflow-hidden\">\n <summary\n class=\"list-none [&::-webkit-details-marker]:hidden cursor-pointer select-none px-4 py-3 flex items-center gap-3\"\n >\n <div class=\"flex items-center gap-2 min-w-0 flex-1\">\n <span\n class=\"inline-flex h-6 w-6 items-center justify-center rounded bg-indigo-100 text-indigo-600\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path\n d=\"M10 2C6.686 2 4 4.686 4 8c0 1.655.672 3.154 1.757 4.243.362.363.576.858.576 1.371V14.5a1 1 0 0 0 1 1h5.334a1 1 0 0 0 1-1v-.886c0-.513.214-1.008.576-1.371A5.978 5.978 0 0 0 16 8c0-3.314-2.686-6-6-6Z\"\n stroke=\"currentColor\"\n stroke-width=\"1.4\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n <path\n d=\"M7.5 17.5h5M8.5 8a2 2 0 0 1 2-2\"\n stroke=\"currentColor\"\n stroke-width=\"1.4\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </span>\n\n <div class=\"min-w-0\">\n <div class=\"flex items-center gap-2\">\n <div class=\"font-medium text-slate-700 text-sm\">Reasoning</div>\n @if (p.state) {\n <span\n class=\"inline-flex items-center rounded px-1.5 py-0.5 text-[11px] font-medium\"\n [class.bg-amber-100.text-amber-700]=\"p.state === 'streaming'\"\n [class.bg-emerald-100.text-emerald-700]=\"p.state === 'done'\"\n >\n {{ p.state === 'streaming' ? 'Streaming' : 'Done' }}\n </span>\n }\n </div>\n </div>\n </div>\n\n <span\n class=\"ml-auto inline-flex h-6 w-6 items-center justify-center rounded text-slate-400 transition group-open:rotate-180\"\n aria-hidden=\"true\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path\n d=\"m5.75 8.25 4.25 4.25 4.25-4.25\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </span>\n </summary>\n\n <div class=\"px-4 pb-4\">\n <div class=\"mt-1 rounded bg-white border border-slate-200 p-3\">\n <pre\n class=\"m-0 whitespace-pre-wrap wrap-break-word font-mono text-xs leading-relaxed text-slate-700\"\n >{{ p.text.trim() ? p.text : 'No reasoning provided.' }}</pre\n >\n </div>\n </div>\n</details>\n" }]
|
|
343
|
+
}], propDecorators: { reasoningPart: [{ type: i0.Input, args: [{ isSignal: true, alias: "reasoningPart", required: true }] }] } });
|
|
344
|
+
|
|
345
|
+
hljs.registerLanguage('javascript', javascript);
|
|
346
|
+
hljs.registerLanguage('json', json);
|
|
347
|
+
hljs.registerLanguage('cypher', sql);
|
|
348
|
+
class HighlightPipe {
|
|
349
|
+
sanitizer;
|
|
350
|
+
constructor(sanitizer) {
|
|
351
|
+
this.sanitizer = sanitizer;
|
|
352
|
+
}
|
|
353
|
+
transform(code, lang) {
|
|
354
|
+
if (!code)
|
|
355
|
+
return '';
|
|
356
|
+
const highlighted = hljs.getLanguage(lang)
|
|
357
|
+
? hljs.highlight(code, { language: lang }).value
|
|
358
|
+
: this.escapeHtml(code);
|
|
359
|
+
return this.sanitizer.bypassSecurityTrustHtml(highlighted);
|
|
360
|
+
}
|
|
361
|
+
escapeHtml(str) {
|
|
362
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
363
|
+
}
|
|
364
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: HighlightPipe, deps: [{ token: i1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Pipe });
|
|
365
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.0", ngImport: i0, type: HighlightPipe, isStandalone: true, name: "highlight" });
|
|
366
|
+
}
|
|
367
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: HighlightPipe, decorators: [{
|
|
368
|
+
type: Pipe,
|
|
369
|
+
args: [{ name: 'highlight', standalone: true }]
|
|
370
|
+
}], ctorParameters: () => [{ type: i1.DomSanitizer }] });
|
|
371
|
+
|
|
372
|
+
class JsonTreeComponent {
|
|
373
|
+
data = input.required(...(ngDevMode ? [{ debugName: "data" }] : []));
|
|
374
|
+
expandDepth = input(1, ...(ngDevMode ? [{ debugName: "expandDepth" }] : []));
|
|
375
|
+
collapsedPaths = new Set();
|
|
376
|
+
userToggled = new Set();
|
|
377
|
+
isCollapsed(path, depth) {
|
|
378
|
+
if (this.userToggled.has(path)) {
|
|
379
|
+
return this.collapsedPaths.has(path);
|
|
380
|
+
}
|
|
381
|
+
return depth >= this.expandDepth();
|
|
382
|
+
}
|
|
383
|
+
toggle(path, depth) {
|
|
384
|
+
this.userToggled.add(path);
|
|
385
|
+
const currentlyCollapsed = this.isCollapsed(path, depth);
|
|
386
|
+
if (currentlyCollapsed) {
|
|
387
|
+
this.collapsedPaths.delete(path);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
this.collapsedPaths.add(path);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
isObject(value) {
|
|
394
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
395
|
+
}
|
|
396
|
+
isArray(value) {
|
|
397
|
+
return Array.isArray(value);
|
|
398
|
+
}
|
|
399
|
+
objectEntries(value) {
|
|
400
|
+
if (!this.isObject(value))
|
|
401
|
+
return [];
|
|
402
|
+
return Object.entries(value);
|
|
403
|
+
}
|
|
404
|
+
arrayItems(value) {
|
|
405
|
+
return Array.isArray(value) ? value : [];
|
|
406
|
+
}
|
|
407
|
+
objectLength(value) {
|
|
408
|
+
if (!this.isObject(value))
|
|
409
|
+
return 0;
|
|
410
|
+
return Object.keys(value).length;
|
|
411
|
+
}
|
|
412
|
+
arrayLength(value) {
|
|
413
|
+
return Array.isArray(value) ? value.length : 0;
|
|
414
|
+
}
|
|
415
|
+
formatPrimitive(value) {
|
|
416
|
+
if (value === null || value === undefined)
|
|
417
|
+
return 'null';
|
|
418
|
+
if (typeof value === 'string')
|
|
419
|
+
return JSON.stringify(value);
|
|
420
|
+
return String(value);
|
|
421
|
+
}
|
|
422
|
+
primitiveClass(value) {
|
|
423
|
+
if (value === null || value === undefined)
|
|
424
|
+
return 'jt-null';
|
|
425
|
+
if (typeof value === 'string')
|
|
426
|
+
return 'jt-string';
|
|
427
|
+
if (typeof value === 'number')
|
|
428
|
+
return 'jt-number';
|
|
429
|
+
if (typeof value === 'boolean')
|
|
430
|
+
return 'jt-boolean';
|
|
431
|
+
return '';
|
|
432
|
+
}
|
|
433
|
+
isContainer(value) {
|
|
434
|
+
return this.isObject(value) || this.isArray(value);
|
|
435
|
+
}
|
|
436
|
+
parsed(value) {
|
|
437
|
+
return this.deepParse(value);
|
|
438
|
+
}
|
|
439
|
+
deepParse(value) {
|
|
440
|
+
if (typeof value === 'string') {
|
|
441
|
+
const trimmed = value.trim();
|
|
442
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
443
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
444
|
+
try {
|
|
445
|
+
return this.deepParse(JSON.parse(trimmed));
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return value;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return value;
|
|
452
|
+
}
|
|
453
|
+
if (Array.isArray(value)) {
|
|
454
|
+
return value.map((item) => this.deepParse(item));
|
|
455
|
+
}
|
|
456
|
+
if (value !== null && typeof value === 'object') {
|
|
457
|
+
const result = {};
|
|
458
|
+
for (const [key, val] of Object.entries(value)) {
|
|
459
|
+
result[key] = this.deepParse(val);
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: JsonTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
466
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: JsonTreeComponent, isStandalone: true, selector: "cortex-json-tree", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, expandDepth: { classPropertyName: "expandDepth", publicName: "expandDepth", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block font-mono text-[12px] leading-5 select-text" }, ngImport: i0, template: "<!-- Entry point: parse and render the root value -->\n<ng-container\n *ngTemplateOutlet=\"valueTemplate; context: { $implicit: parsed(data()), path: '$', depth: 0 }\"\n/>\n\n<!-- Recursive value renderer -->\n<ng-template #valueTemplate let-value let-path=\"path\" let-depth=\"depth\">\n @if (isObject(value)) {\n @if (objectLength(value) === 0) {\n <span class=\"jt-bracket\">{{ '{' }}{{ '}' }}</span>\n } @else {\n <span class=\"jt-toggle\" (click)=\"toggle(path, depth); $event.stopPropagation()\" role=\"button\">\n <span class=\"jt-arrow\" [class.jt-arrow--collapsed]=\"isCollapsed(path, depth)\">▾</span>\n <span class=\"jt-bracket\">{{ '{' }}</span>\n </span>\n @if (isCollapsed(path, depth)) {\n <span\n class=\"jt-collapsed-hint\"\n (click)=\"toggle(path, depth); $event.stopPropagation()\"\n role=\"button\"\n >{{ objectLength(value) }}\n {{ objectLength(value) === 1 ? 'property' : 'properties' }}</span\n ><span class=\"jt-bracket\">{{ '}' }}</span>\n } @else {\n <div class=\"jt-indent\">\n @for (entry of objectEntries(value); track entry[0]; let last = $last) {\n <div class=\"jt-line\">\n <span class=\"jt-key\">\"{{ entry[0] }}\"</span><span class=\"jt-colon\">: </span>\n @if (isContainer(entry[1])) {\n <ng-container\n *ngTemplateOutlet=\"\n valueTemplate;\n context: { $implicit: entry[1], path: path + '.' + entry[0], depth: depth + 1 }\n \"\n />\n } @else {\n <span [class]=\"primitiveClass(entry[1])\">{{ formatPrimitive(entry[1]) }}</span>\n }\n @if (!last) {\n <span class=\"jt-comma\">,</span>\n }\n </div>\n }\n </div>\n <span class=\"jt-bracket\">{{ '}' }}</span>\n }\n }\n } @else if (isArray(value)) {\n @if (arrayLength(value) === 0) {\n <span class=\"jt-bracket\">[]</span>\n } @else {\n <span class=\"jt-toggle\" (click)=\"toggle(path, depth); $event.stopPropagation()\" role=\"button\">\n <span class=\"jt-arrow\" [class.jt-arrow--collapsed]=\"isCollapsed(path, depth)\">▾</span>\n <span class=\"jt-bracket\">[</span>\n </span>\n @if (isCollapsed(path, depth)) {\n <span\n class=\"jt-collapsed-hint\"\n (click)=\"toggle(path, depth); $event.stopPropagation()\"\n role=\"button\"\n >{{ arrayLength(value) }} {{ arrayLength(value) === 1 ? 'item' : 'items' }}</span\n ><span class=\"jt-bracket\">]</span>\n } @else {\n <div class=\"jt-indent\">\n @for (item of arrayItems(value); track $index; let last = $last) {\n <div class=\"jt-line\">\n @if (isContainer(item)) {\n <ng-container\n *ngTemplateOutlet=\"\n valueTemplate;\n context: { $implicit: item, path: path + '[' + $index + ']', depth: depth + 1 }\n \"\n />\n } @else {\n <span [class]=\"primitiveClass(item)\">{{ formatPrimitive(item) }}</span>\n }\n @if (!last) {\n <span class=\"jt-comma\">,</span>\n }\n </div>\n }\n </div>\n <span class=\"jt-bracket\">]</span>\n }\n }\n } @else {\n <span [class]=\"primitiveClass(value)\">{{ formatPrimitive(value) }}</span>\n }\n</ng-template>\n", styles: [":host{white-space:pre-wrap;word-break:break-word}.jt-indent{padding-inline-start:1.25em;border-inline-start:1px solid #e2e8f0;margin-inline-start:.3em}.jt-line{position:relative}.jt-toggle{cursor:pointer;-webkit-user-select:none;user-select:none}.jt-toggle:hover .jt-arrow{color:#475569}.jt-arrow{display:inline-block;width:1em;text-align:center;color:#94a3b8;font-size:.7em;transition:transform .15s ease;vertical-align:middle}.jt-arrow--collapsed{transform:rotate(-90deg)}.jt-bracket{color:#64748b}.jt-key{color:#6366f1}.jt-colon{color:#64748b}.jt-comma{color:#94a3b8}.jt-string{color:#059669}.jt-number{color:#2563eb}.jt-boolean{color:#d97706}.jt-null{color:#94a3b8;font-style:italic}.jt-collapsed-hint{display:inline-block;padding:0 .4em;margin:0 .15em;font-size:10px;line-height:1.5;color:#94a3b8;background:#f8fafc;border:1px solid #e2e8f0;border-radius:3px;font-style:italic;cursor:pointer;-webkit-user-select:none;user-select:none;vertical-align:middle}.jt-collapsed-hint:hover{color:#64748b;border-color:#cbd5e1;background:#f1f5f9}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
467
|
+
}
|
|
468
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: JsonTreeComponent, decorators: [{
|
|
469
|
+
type: Component,
|
|
470
|
+
args: [{ selector: 'cortex-json-tree', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], host: { class: 'block font-mono text-[12px] leading-5 select-text' }, template: "<!-- Entry point: parse and render the root value -->\n<ng-container\n *ngTemplateOutlet=\"valueTemplate; context: { $implicit: parsed(data()), path: '$', depth: 0 }\"\n/>\n\n<!-- Recursive value renderer -->\n<ng-template #valueTemplate let-value let-path=\"path\" let-depth=\"depth\">\n @if (isObject(value)) {\n @if (objectLength(value) === 0) {\n <span class=\"jt-bracket\">{{ '{' }}{{ '}' }}</span>\n } @else {\n <span class=\"jt-toggle\" (click)=\"toggle(path, depth); $event.stopPropagation()\" role=\"button\">\n <span class=\"jt-arrow\" [class.jt-arrow--collapsed]=\"isCollapsed(path, depth)\">▾</span>\n <span class=\"jt-bracket\">{{ '{' }}</span>\n </span>\n @if (isCollapsed(path, depth)) {\n <span\n class=\"jt-collapsed-hint\"\n (click)=\"toggle(path, depth); $event.stopPropagation()\"\n role=\"button\"\n >{{ objectLength(value) }}\n {{ objectLength(value) === 1 ? 'property' : 'properties' }}</span\n ><span class=\"jt-bracket\">{{ '}' }}</span>\n } @else {\n <div class=\"jt-indent\">\n @for (entry of objectEntries(value); track entry[0]; let last = $last) {\n <div class=\"jt-line\">\n <span class=\"jt-key\">\"{{ entry[0] }}\"</span><span class=\"jt-colon\">: </span>\n @if (isContainer(entry[1])) {\n <ng-container\n *ngTemplateOutlet=\"\n valueTemplate;\n context: { $implicit: entry[1], path: path + '.' + entry[0], depth: depth + 1 }\n \"\n />\n } @else {\n <span [class]=\"primitiveClass(entry[1])\">{{ formatPrimitive(entry[1]) }}</span>\n }\n @if (!last) {\n <span class=\"jt-comma\">,</span>\n }\n </div>\n }\n </div>\n <span class=\"jt-bracket\">{{ '}' }}</span>\n }\n }\n } @else if (isArray(value)) {\n @if (arrayLength(value) === 0) {\n <span class=\"jt-bracket\">[]</span>\n } @else {\n <span class=\"jt-toggle\" (click)=\"toggle(path, depth); $event.stopPropagation()\" role=\"button\">\n <span class=\"jt-arrow\" [class.jt-arrow--collapsed]=\"isCollapsed(path, depth)\">▾</span>\n <span class=\"jt-bracket\">[</span>\n </span>\n @if (isCollapsed(path, depth)) {\n <span\n class=\"jt-collapsed-hint\"\n (click)=\"toggle(path, depth); $event.stopPropagation()\"\n role=\"button\"\n >{{ arrayLength(value) }} {{ arrayLength(value) === 1 ? 'item' : 'items' }}</span\n ><span class=\"jt-bracket\">]</span>\n } @else {\n <div class=\"jt-indent\">\n @for (item of arrayItems(value); track $index; let last = $last) {\n <div class=\"jt-line\">\n @if (isContainer(item)) {\n <ng-container\n *ngTemplateOutlet=\"\n valueTemplate;\n context: { $implicit: item, path: path + '[' + $index + ']', depth: depth + 1 }\n \"\n />\n } @else {\n <span [class]=\"primitiveClass(item)\">{{ formatPrimitive(item) }}</span>\n }\n @if (!last) {\n <span class=\"jt-comma\">,</span>\n }\n </div>\n }\n </div>\n <span class=\"jt-bracket\">]</span>\n }\n }\n } @else {\n <span [class]=\"primitiveClass(value)\">{{ formatPrimitive(value) }}</span>\n }\n</ng-template>\n", styles: [":host{white-space:pre-wrap;word-break:break-word}.jt-indent{padding-inline-start:1.25em;border-inline-start:1px solid #e2e8f0;margin-inline-start:.3em}.jt-line{position:relative}.jt-toggle{cursor:pointer;-webkit-user-select:none;user-select:none}.jt-toggle:hover .jt-arrow{color:#475569}.jt-arrow{display:inline-block;width:1em;text-align:center;color:#94a3b8;font-size:.7em;transition:transform .15s ease;vertical-align:middle}.jt-arrow--collapsed{transform:rotate(-90deg)}.jt-bracket{color:#64748b}.jt-key{color:#6366f1}.jt-colon{color:#64748b}.jt-comma{color:#94a3b8}.jt-string{color:#059669}.jt-number{color:#2563eb}.jt-boolean{color:#d97706}.jt-null{color:#94a3b8;font-style:italic}.jt-collapsed-hint{display:inline-block;padding:0 .4em;margin:0 .15em;font-size:10px;line-height:1.5;color:#94a3b8;background:#f8fafc;border:1px solid #e2e8f0;border-radius:3px;font-style:italic;cursor:pointer;-webkit-user-select:none;user-select:none;vertical-align:middle}.jt-collapsed-hint:hover{color:#64748b;border-color:#cbd5e1;background:#f1f5f9}\n"] }]
|
|
471
|
+
}], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], expandDepth: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandDepth", required: false }] }] } });
|
|
472
|
+
|
|
473
|
+
class MessageToolCallPartComponent {
|
|
474
|
+
static CODE_FIELDS = {
|
|
475
|
+
code: 'javascript',
|
|
476
|
+
query: 'cypher',
|
|
477
|
+
};
|
|
478
|
+
toolCallPart = input.required(...(ngDevMode ? [{ debugName: "toolCallPart" }] : []));
|
|
479
|
+
codeSnippets = computed(() => {
|
|
480
|
+
const inp = this.toolCallPart().input;
|
|
481
|
+
if (!inp || typeof inp !== 'object')
|
|
482
|
+
return [];
|
|
483
|
+
const record = inp;
|
|
484
|
+
const snippets = [];
|
|
485
|
+
for (const [key, lang] of Object.entries(MessageToolCallPartComponent.CODE_FIELDS)) {
|
|
486
|
+
if (key in record && typeof record[key] === 'string') {
|
|
487
|
+
snippets.push({
|
|
488
|
+
key,
|
|
489
|
+
lang,
|
|
490
|
+
value: record[key].replace(/\\n/g, '\n'),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return snippets;
|
|
495
|
+
}, ...(ngDevMode ? [{ debugName: "codeSnippets" }] : []));
|
|
496
|
+
remainingInput = computed(() => {
|
|
497
|
+
const inp = this.toolCallPart().input;
|
|
498
|
+
if (!inp || typeof inp !== 'object')
|
|
499
|
+
return inp;
|
|
500
|
+
const record = inp;
|
|
501
|
+
const codeKeys = Object.keys(MessageToolCallPartComponent.CODE_FIELDS);
|
|
502
|
+
const remaining = {};
|
|
503
|
+
let hasKeys = false;
|
|
504
|
+
for (const [key, val] of Object.entries(record)) {
|
|
505
|
+
if (!codeKeys.includes(key)) {
|
|
506
|
+
remaining[key] = val;
|
|
507
|
+
hasKeys = true;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return hasKeys ? remaining : null;
|
|
511
|
+
}, ...(ngDevMode ? [{ debugName: "remainingInput" }] : []));
|
|
512
|
+
toolName() {
|
|
513
|
+
const type = this.toolCallPart().type;
|
|
514
|
+
if (!type)
|
|
515
|
+
return '';
|
|
516
|
+
return type.startsWith('tool-') ? type.slice('tool-'.length) : type;
|
|
517
|
+
}
|
|
518
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageToolCallPartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
519
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: MessageToolCallPartComponent, isStandalone: true, selector: "cortex-message-tool-call-part", inputs: { toolCallPart: { classPropertyName: "toolCallPart", publicName: "toolCallPart", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<details class=\"group w-full rounded-lg border border-slate-200 bg-slate-50 overflow-hidden\">\n <summary\n class=\"list-none [&::-webkit-details-marker]:hidden cursor-pointer select-none px-4 py-3 group-open:border-b group-open:border-slate-200\"\n >\n <div class=\"flex items-start gap-3\">\n <div\n class=\"shrink-0 mt-0.5 size-7 rounded bg-slate-800 text-white flex items-center justify-center\"\n aria-hidden=\"true\"\n >\n <svg viewBox=\"0 0 20 20\" class=\"size-4\" fill=\"none\">\n <path\n d=\"M6.5 6.5 3.75 10l2.75 3.5M13.5 6.5 16.25 10l-2.75 3.5M11.25 5.75 8.75 14.25\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </div>\n\n <div class=\"min-w-0 flex-1\">\n <div class=\"flex items-center gap-2\">\n <div class=\"min-w-0 flex-1\">\n <div class=\"flex items-center gap-2 min-w-0\">\n <div\n class=\"font-medium text-slate-800 text-sm truncate\"\n [attr.title]=\"toolCallPart().title || null\"\n >\n {{ toolCallPart().title ?? 'Tool call' }}\n </div>\n @if (toolName()) {\n <span\n class=\"hidden sm:inline-flex items-center rounded bg-slate-200 px-1.5 py-0.5 text-[11px] font-medium text-slate-600 font-mono\"\n [attr.title]=\"toolCallPart().type || null\"\n >{{ toolName() }}</span\n >\n }\n </div>\n <div class=\"mt-0.5 text-[11px] text-slate-500 break-all\">\n <span class=\"font-medium text-slate-600\">ID</span>\n <span class=\"font-mono\"> {{ toolCallPart().toolCallId }}</span>\n @if (toolCallPart().providerExecuted) {\n <span class=\"ml-2 inline-flex items-center gap-1 text-emerald-700\">\n <span class=\"size-1.5 rounded-full bg-emerald-500\"></span>\n provider\n </span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <span\n class=\"inline-flex items-center gap-1.5 rounded px-2 py-1 text-[11px] font-medium border\"\n [ngClass]=\"{\n 'bg-emerald-50 text-emerald-700 border-emerald-200':\n toolCallPart().state === 'output-available',\n 'bg-red-50 text-red-700 border-red-200': toolCallPart().state === 'output-error',\n 'bg-slate-100 text-slate-600 border-slate-200':\n toolCallPart().state === 'output-denied',\n 'bg-violet-50 text-violet-700 border-violet-200':\n toolCallPart().state === 'approval-requested' ||\n toolCallPart().state === 'approval-responded',\n 'bg-amber-50 text-amber-700 border-amber-200':\n toolCallPart().state === 'input-streaming' ||\n toolCallPart().state === 'input-available',\n }\"\n >\n @switch (toolCallPart().state) {\n @case ('input-streaming') {\n <span class=\"size-1.5 rounded-full bg-amber-500 animate-pulse\"></span>\n Calling\n }\n @case ('input-available') {\n <span class=\"size-1.5 rounded-full bg-amber-500\"></span> Input ready\n }\n @case ('approval-requested') {\n <span class=\"size-1.5 rounded-full bg-violet-500 animate-pulse\"></span>\n Needs approval\n }\n @case ('approval-responded') {\n @if (toolCallPart().approval?.approved) {\n <span class=\"size-1.5 rounded-full bg-emerald-500\"></span> Approved\n } @else {\n <span class=\"size-1.5 rounded-full bg-slate-500\"></span> Responded\n }\n }\n @case ('output-available') {\n <span class=\"size-1.5 rounded-full bg-emerald-500\"></span>\n {{ 'translate_completed' | translate }}\n }\n @case ('output-error') {\n <span class=\"size-1.5 rounded-full bg-red-500\"></span> Error\n }\n @case ('output-denied') {\n <span class=\"size-1.5 rounded-full bg-slate-500\"></span> Denied\n }\n }\n </span>\n\n <span\n class=\"inline-flex size-6 items-center justify-center rounded text-slate-400 transition group-open:rotate-180\"\n aria-hidden=\"true\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path\n d=\"m5.75 8.25 4.25 4.25 4.25-4.25\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </span>\n </div>\n </div>\n </div>\n </div>\n </summary>\n\n <div class=\"bg-white px-4 pb-4 pt-3\">\n <div class=\"grid gap-3\">\n @for (snippet of codeSnippets(); track snippet.key) {\n <div class=\"rounded-lg border border-slate-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-slate-200 bg-slate-50\"\n >\n <div class=\"text-[11px] font-medium text-slate-600\">\n {{ snippet.key }}\n </div>\n <div class=\"text-[11px] text-slate-400 font-mono\">\n {{ snippet.lang }}\n </div>\n </div>\n <pre\n dir=\"ltr\"\n class=\"m-0 max-h-80 overflow-auto p-3 text-[12px] leading-5 font-mono whitespace-pre wrap-break-word\"\n ><code class=\"hljs\" [innerHTML]=\"snippet.value | highlight : snippet.lang\"></code></pre>\n </div>\n }\n\n @if (remainingInput(); as remaining) {\n <div class=\"rounded-lg border border-slate-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-slate-200 bg-slate-50\"\n >\n <div class=\"text-[11px] font-medium text-slate-600\">Input</div>\n <div class=\"text-[11px] text-slate-400 font-mono\">json</div>\n </div>\n <div dir=\"ltr\" class=\"max-h-64 overflow-auto p-3\">\n <cortex-json-tree [data]=\"remaining\" [expandDepth]=\"2\" />\n </div>\n </div>\n }\n\n @if (toolCallPart().state === 'output-available') {\n <div class=\"rounded-lg border border-emerald-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-emerald-200 bg-emerald-50\"\n >\n <div class=\"text-[11px] font-medium text-emerald-700\">Output</div>\n <div class=\"text-[11px] text-emerald-600 font-mono\">json</div>\n </div>\n <div dir=\"ltr\" class=\"max-h-64 overflow-auto p-3\">\n <cortex-json-tree [data]=\"toolCallPart().output\" [expandDepth]=\"2\" />\n </div>\n </div>\n }\n\n @if (toolCallPart().state === 'output-error') {\n <div class=\"rounded-lg border border-red-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-red-200 bg-red-50\"\n >\n <div class=\"text-[11px] font-medium text-red-700\">Error</div>\n <div class=\"text-[11px] text-red-600 font-mono\">text</div>\n </div>\n <pre\n dir=\"ltr\"\n class=\"m-0 max-h-64 overflow-auto p-3 text-[12px] leading-5 font-mono text-red-800 whitespace-pre-wrap wrap-break-word\"\n >{{ toolCallPart().errorText }}</pre\n >\n </div>\n }\n\n @if (toolCallPart().state === 'output-denied') {\n <div class=\"rounded-lg border border-slate-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-slate-200 bg-slate-50\"\n >\n <div class=\"text-[11px] font-medium text-slate-600\">Denied</div>\n <div class=\"text-[11px] text-slate-500 font-mono\">approval</div>\n </div>\n <div class=\"p-3 text-[12px] text-slate-700\">\n Tool execution was denied.\n @if (toolCallPart().approval?.reason) {\n <div class=\"mt-2 rounded bg-slate-50 border border-slate-200 p-2\">\n <div class=\"text-[11px] font-medium text-slate-600\">Reason</div>\n <div class=\"mt-1 font-mono text-[12px] text-slate-700 wrap-break-word\">\n {{ toolCallPart().approval?.reason }}\n </div>\n </div>\n }\n </div>\n </div>\n }\n\n @if (toolCallPart().state === 'approval-requested') {\n <div class=\"rounded-lg border border-violet-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-violet-200 bg-violet-50\"\n >\n <div class=\"text-[11px] font-medium text-violet-700\">Approval requested</div>\n <div class=\"text-[11px] text-violet-600 font-mono\">\n {{ toolCallPart().approval?.id }}\n </div>\n </div>\n <div class=\"p-3 text-[12px] text-violet-800\">\n Waiting for approval to execute this tool.\n </div>\n </div>\n }\n\n @if (toolCallPart().state === 'approval-responded') {\n <div class=\"rounded-lg border border-violet-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-violet-200 bg-violet-50\"\n >\n <div class=\"text-[11px] font-medium text-violet-700\">Approval response</div>\n <div class=\"text-[11px] text-violet-600 font-mono\">\n {{ toolCallPart().approval?.id }}\n </div>\n </div>\n <div class=\"p-3 text-[12px] text-violet-800\">\n @if (toolCallPart().approval?.approved) {\n Approved.\n } @else {\n Response received.\n }\n @if (toolCallPart().approval?.reason) {\n <div class=\"mt-2 rounded bg-violet-50 border border-violet-200 p-2\">\n <div class=\"text-[11px] font-medium text-violet-700\">Reason</div>\n <div class=\"mt-1 font-mono text-[12px] text-violet-800 wrap-break-word\">\n {{ toolCallPart().approval?.reason }}\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n </div>\n</details>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: JsonTreeComponent, selector: "cortex-json-tree", inputs: ["data", "expandDepth"] }, { kind: "pipe", type: HighlightPipe, name: "highlight" }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
520
|
+
}
|
|
521
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageToolCallPartComponent, decorators: [{
|
|
522
|
+
type: Component,
|
|
523
|
+
args: [{ selector: 'cortex-message-tool-call-part', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HighlightPipe, NgClass, TranslatePipe, JsonTreeComponent], template: "<details class=\"group w-full rounded-lg border border-slate-200 bg-slate-50 overflow-hidden\">\n <summary\n class=\"list-none [&::-webkit-details-marker]:hidden cursor-pointer select-none px-4 py-3 group-open:border-b group-open:border-slate-200\"\n >\n <div class=\"flex items-start gap-3\">\n <div\n class=\"shrink-0 mt-0.5 size-7 rounded bg-slate-800 text-white flex items-center justify-center\"\n aria-hidden=\"true\"\n >\n <svg viewBox=\"0 0 20 20\" class=\"size-4\" fill=\"none\">\n <path\n d=\"M6.5 6.5 3.75 10l2.75 3.5M13.5 6.5 16.25 10l-2.75 3.5M11.25 5.75 8.75 14.25\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </div>\n\n <div class=\"min-w-0 flex-1\">\n <div class=\"flex items-center gap-2\">\n <div class=\"min-w-0 flex-1\">\n <div class=\"flex items-center gap-2 min-w-0\">\n <div\n class=\"font-medium text-slate-800 text-sm truncate\"\n [attr.title]=\"toolCallPart().title || null\"\n >\n {{ toolCallPart().title ?? 'Tool call' }}\n </div>\n @if (toolName()) {\n <span\n class=\"hidden sm:inline-flex items-center rounded bg-slate-200 px-1.5 py-0.5 text-[11px] font-medium text-slate-600 font-mono\"\n [attr.title]=\"toolCallPart().type || null\"\n >{{ toolName() }}</span\n >\n }\n </div>\n <div class=\"mt-0.5 text-[11px] text-slate-500 break-all\">\n <span class=\"font-medium text-slate-600\">ID</span>\n <span class=\"font-mono\"> {{ toolCallPart().toolCallId }}</span>\n @if (toolCallPart().providerExecuted) {\n <span class=\"ml-2 inline-flex items-center gap-1 text-emerald-700\">\n <span class=\"size-1.5 rounded-full bg-emerald-500\"></span>\n provider\n </span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <span\n class=\"inline-flex items-center gap-1.5 rounded px-2 py-1 text-[11px] font-medium border\"\n [ngClass]=\"{\n 'bg-emerald-50 text-emerald-700 border-emerald-200':\n toolCallPart().state === 'output-available',\n 'bg-red-50 text-red-700 border-red-200': toolCallPart().state === 'output-error',\n 'bg-slate-100 text-slate-600 border-slate-200':\n toolCallPart().state === 'output-denied',\n 'bg-violet-50 text-violet-700 border-violet-200':\n toolCallPart().state === 'approval-requested' ||\n toolCallPart().state === 'approval-responded',\n 'bg-amber-50 text-amber-700 border-amber-200':\n toolCallPart().state === 'input-streaming' ||\n toolCallPart().state === 'input-available',\n }\"\n >\n @switch (toolCallPart().state) {\n @case ('input-streaming') {\n <span class=\"size-1.5 rounded-full bg-amber-500 animate-pulse\"></span>\n Calling\n }\n @case ('input-available') {\n <span class=\"size-1.5 rounded-full bg-amber-500\"></span> Input ready\n }\n @case ('approval-requested') {\n <span class=\"size-1.5 rounded-full bg-violet-500 animate-pulse\"></span>\n Needs approval\n }\n @case ('approval-responded') {\n @if (toolCallPart().approval?.approved) {\n <span class=\"size-1.5 rounded-full bg-emerald-500\"></span> Approved\n } @else {\n <span class=\"size-1.5 rounded-full bg-slate-500\"></span> Responded\n }\n }\n @case ('output-available') {\n <span class=\"size-1.5 rounded-full bg-emerald-500\"></span>\n {{ 'translate_completed' | translate }}\n }\n @case ('output-error') {\n <span class=\"size-1.5 rounded-full bg-red-500\"></span> Error\n }\n @case ('output-denied') {\n <span class=\"size-1.5 rounded-full bg-slate-500\"></span> Denied\n }\n }\n </span>\n\n <span\n class=\"inline-flex size-6 items-center justify-center rounded text-slate-400 transition group-open:rotate-180\"\n aria-hidden=\"true\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path\n d=\"m5.75 8.25 4.25 4.25 4.25-4.25\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </span>\n </div>\n </div>\n </div>\n </div>\n </summary>\n\n <div class=\"bg-white px-4 pb-4 pt-3\">\n <div class=\"grid gap-3\">\n @for (snippet of codeSnippets(); track snippet.key) {\n <div class=\"rounded-lg border border-slate-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-slate-200 bg-slate-50\"\n >\n <div class=\"text-[11px] font-medium text-slate-600\">\n {{ snippet.key }}\n </div>\n <div class=\"text-[11px] text-slate-400 font-mono\">\n {{ snippet.lang }}\n </div>\n </div>\n <pre\n dir=\"ltr\"\n class=\"m-0 max-h-80 overflow-auto p-3 text-[12px] leading-5 font-mono whitespace-pre wrap-break-word\"\n ><code class=\"hljs\" [innerHTML]=\"snippet.value | highlight : snippet.lang\"></code></pre>\n </div>\n }\n\n @if (remainingInput(); as remaining) {\n <div class=\"rounded-lg border border-slate-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-slate-200 bg-slate-50\"\n >\n <div class=\"text-[11px] font-medium text-slate-600\">Input</div>\n <div class=\"text-[11px] text-slate-400 font-mono\">json</div>\n </div>\n <div dir=\"ltr\" class=\"max-h-64 overflow-auto p-3\">\n <cortex-json-tree [data]=\"remaining\" [expandDepth]=\"2\" />\n </div>\n </div>\n }\n\n @if (toolCallPart().state === 'output-available') {\n <div class=\"rounded-lg border border-emerald-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-emerald-200 bg-emerald-50\"\n >\n <div class=\"text-[11px] font-medium text-emerald-700\">Output</div>\n <div class=\"text-[11px] text-emerald-600 font-mono\">json</div>\n </div>\n <div dir=\"ltr\" class=\"max-h-64 overflow-auto p-3\">\n <cortex-json-tree [data]=\"toolCallPart().output\" [expandDepth]=\"2\" />\n </div>\n </div>\n }\n\n @if (toolCallPart().state === 'output-error') {\n <div class=\"rounded-lg border border-red-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-red-200 bg-red-50\"\n >\n <div class=\"text-[11px] font-medium text-red-700\">Error</div>\n <div class=\"text-[11px] text-red-600 font-mono\">text</div>\n </div>\n <pre\n dir=\"ltr\"\n class=\"m-0 max-h-64 overflow-auto p-3 text-[12px] leading-5 font-mono text-red-800 whitespace-pre-wrap wrap-break-word\"\n >{{ toolCallPart().errorText }}</pre\n >\n </div>\n }\n\n @if (toolCallPart().state === 'output-denied') {\n <div class=\"rounded-lg border border-slate-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-slate-200 bg-slate-50\"\n >\n <div class=\"text-[11px] font-medium text-slate-600\">Denied</div>\n <div class=\"text-[11px] text-slate-500 font-mono\">approval</div>\n </div>\n <div class=\"p-3 text-[12px] text-slate-700\">\n Tool execution was denied.\n @if (toolCallPart().approval?.reason) {\n <div class=\"mt-2 rounded bg-slate-50 border border-slate-200 p-2\">\n <div class=\"text-[11px] font-medium text-slate-600\">Reason</div>\n <div class=\"mt-1 font-mono text-[12px] text-slate-700 wrap-break-word\">\n {{ toolCallPart().approval?.reason }}\n </div>\n </div>\n }\n </div>\n </div>\n }\n\n @if (toolCallPart().state === 'approval-requested') {\n <div class=\"rounded-lg border border-violet-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-violet-200 bg-violet-50\"\n >\n <div class=\"text-[11px] font-medium text-violet-700\">Approval requested</div>\n <div class=\"text-[11px] text-violet-600 font-mono\">\n {{ toolCallPart().approval?.id }}\n </div>\n </div>\n <div class=\"p-3 text-[12px] text-violet-800\">\n Waiting for approval to execute this tool.\n </div>\n </div>\n }\n\n @if (toolCallPart().state === 'approval-responded') {\n <div class=\"rounded-lg border border-violet-200 overflow-hidden\">\n <div\n class=\"px-3 py-2 flex items-center justify-between border-b border-violet-200 bg-violet-50\"\n >\n <div class=\"text-[11px] font-medium text-violet-700\">Approval response</div>\n <div class=\"text-[11px] text-violet-600 font-mono\">\n {{ toolCallPart().approval?.id }}\n </div>\n </div>\n <div class=\"p-3 text-[12px] text-violet-800\">\n @if (toolCallPart().approval?.approved) {\n Approved.\n } @else {\n Response received.\n }\n @if (toolCallPart().approval?.reason) {\n <div class=\"mt-2 rounded bg-violet-50 border border-violet-200 p-2\">\n <div class=\"text-[11px] font-medium text-violet-700\">Reason</div>\n <div class=\"mt-1 font-mono text-[12px] text-violet-800 wrap-break-word\">\n {{ toolCallPart().approval?.reason }}\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n </div>\n</details>\n" }]
|
|
524
|
+
}], propDecorators: { toolCallPart: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolCallPart", required: true }] }] } });
|
|
525
|
+
|
|
526
|
+
class MessageCaptureFilesPartComponent {
|
|
527
|
+
message = input.required(...(ngDevMode ? [{ debugName: "message" }] : []));
|
|
528
|
+
toolPart = input.required(...(ngDevMode ? [{ debugName: "toolPart" }] : []));
|
|
529
|
+
data = computed(() => this.toolPart().input, ...(ngDevMode ? [{ debugName: "data" }] : []));
|
|
530
|
+
isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
531
|
+
outputData = computed(() => {
|
|
532
|
+
const raw = this.toolPart().output;
|
|
533
|
+
if (!raw)
|
|
534
|
+
return null;
|
|
535
|
+
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
536
|
+
return parsed;
|
|
537
|
+
}, ...(ngDevMode ? [{ debugName: "outputData" }] : []));
|
|
538
|
+
form = computed(() => {
|
|
539
|
+
const form = this.fb.group({
|
|
540
|
+
files: this.fb.array(this.data().files.map((x) => this.fb.group({
|
|
541
|
+
id: new FormControl(x.id, { nonNullable: true, validators: [Validators.required] }),
|
|
542
|
+
file: new FormControl(null, {
|
|
543
|
+
nonNullable: true,
|
|
544
|
+
validators: [Validators.required],
|
|
545
|
+
}),
|
|
546
|
+
}))),
|
|
547
|
+
});
|
|
548
|
+
if (this.toolPart().output) {
|
|
549
|
+
form.disable();
|
|
550
|
+
}
|
|
551
|
+
return form;
|
|
552
|
+
}, ...(ngDevMode ? [{ debugName: "form" }] : []));
|
|
553
|
+
fb = inject(FormBuilder);
|
|
554
|
+
chatService = inject(CortexChatService);
|
|
555
|
+
config = inject(CORTEX_CLIENT_CONFIG);
|
|
556
|
+
async submit() {
|
|
557
|
+
if (this.form().invalid || this.isLoading())
|
|
558
|
+
return;
|
|
559
|
+
this.isLoading.set(true);
|
|
560
|
+
const { files } = this.form().getRawValue();
|
|
561
|
+
const baseUrl = this.config.transport.baseUrl;
|
|
562
|
+
const base = typeof baseUrl === 'string' ? baseUrl : baseUrl();
|
|
563
|
+
const url = base.replace(/\/$/, '') + '/files';
|
|
564
|
+
fromPromise((async () => {
|
|
565
|
+
const headers = await Promise.resolve(this.config.transport.getAuthHeaders());
|
|
566
|
+
const res = await fetch(url, {
|
|
567
|
+
method: 'POST',
|
|
568
|
+
headers: {
|
|
569
|
+
'Content-Type': 'application/json',
|
|
570
|
+
...headers,
|
|
571
|
+
},
|
|
572
|
+
body: JSON.stringify({
|
|
573
|
+
files,
|
|
574
|
+
toolCallId: this.toolPart().toolCallId,
|
|
575
|
+
message: this.message(),
|
|
576
|
+
}),
|
|
577
|
+
});
|
|
578
|
+
if (!res.ok)
|
|
579
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
580
|
+
return res.json();
|
|
581
|
+
})())
|
|
582
|
+
.pipe(finalize(() => this.isLoading.set(false)))
|
|
583
|
+
.subscribe((result) => {
|
|
584
|
+
this.chatService.chat()?.addToolOutput({
|
|
585
|
+
tool: 'captureFiles',
|
|
586
|
+
toolCallId: this.toolPart().toolCallId,
|
|
587
|
+
output: {
|
|
588
|
+
status: 'success',
|
|
589
|
+
result,
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
async downloadFile(uploadId) {
|
|
595
|
+
const baseUrl = this.config.transport.baseUrl;
|
|
596
|
+
const base = typeof baseUrl === 'string' ? baseUrl : baseUrl();
|
|
597
|
+
const url = base.replace(/\/$/, '') + '/files/' + uploadId;
|
|
598
|
+
const headers = await Promise.resolve(this.config.transport.getAuthHeaders());
|
|
599
|
+
const response = await fetch(url, { headers });
|
|
600
|
+
const disposition = response.headers.get('Content-Disposition');
|
|
601
|
+
const name = disposition?.match(/filename="?([^";\n]+)"?/)?.[1] ?? uploadId;
|
|
602
|
+
const blob = await response.blob();
|
|
603
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
604
|
+
const a = document.createElement('a');
|
|
605
|
+
a.href = blobUrl;
|
|
606
|
+
a.download = name;
|
|
607
|
+
a.click();
|
|
608
|
+
URL.revokeObjectURL(blobUrl);
|
|
609
|
+
}
|
|
610
|
+
cancel() {
|
|
611
|
+
this.chatService.chat()?.addToolOutput({
|
|
612
|
+
tool: 'captureFiles',
|
|
613
|
+
toolCallId: this.toolPart().toolCallId,
|
|
614
|
+
output: {
|
|
615
|
+
status: 'canceled',
|
|
616
|
+
message: 'User canceled upload',
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageCaptureFilesPartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
621
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: MessageCaptureFilesPartComponent, isStandalone: true, selector: "cortex-message-capture-files-part", inputs: { message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: true, transformFunction: null }, toolPart: { classPropertyName: "toolPart", publicName: "toolPart", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "@if (outputData(); as output) {\n <!-- \u2500\u2500 Result state \u2500\u2500 -->\n @if (output.status === 'success') {\n <div\n class=\"overflow-hidden rounded-2xl border border-emerald-200 bg-linear-to-b from-emerald-50/80 to-white\"\n >\n <div class=\"flex items-center gap-3 px-4 py-3 sm:px-5\">\n <div\n class=\"inline-flex size-8 shrink-0 items-center justify-center rounded-lg border border-emerald-200 bg-emerald-100 text-emerald-600\"\n >\n <svg viewBox=\"0 0 20 20\" class=\"size-4\" fill=\"none\">\n <path\n d=\"M5.5 10.5 L8.5 13.5 L14.5 7\"\n stroke=\"currentColor\"\n stroke-width=\"1.75\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </div>\n <div class=\"min-w-0\">\n <h3 class=\"text-[13px] font-semibold tracking-tight text-emerald-800\">\n {{ 'translate_files_attached' | translate: { count: data().files.length } }}\n </h3>\n </div>\n </div>\n\n <div class=\"border-t border-emerald-100 px-4 py-2.5 sm:px-5\">\n <ul class=\"flex flex-wrap gap-2\">\n @for (file of outputData()!.result!; track file.uploadId) {\n <li>\n <button\n type=\"button\"\n (click)=\"downloadFile(file.uploadId)\"\n class=\"inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-emerald-100 bg-white px-2.5 py-1.5 text-[12px] font-medium text-emerald-700 shadow-xs transition-all hover:border-emerald-200 hover:bg-emerald-50 hover:shadow-sm active:scale-[0.97]\"\n >\n <svg viewBox=\"0 0 16 16\" class=\"size-3.5 shrink-0 text-emerald-400\" fill=\"none\">\n <path\n d=\"M4 1.5h5.172a2 2 0 0 1 1.414.586l2.328 2.328a2 2 0 0 1 .586 1.414V12.5a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2Z\"\n stroke=\"currentColor\"\n stroke-width=\"1.25\"\n />\n <path\n d=\"M9.5 1.5v2a2 2 0 0 0 2 2h2\"\n stroke=\"currentColor\"\n stroke-width=\"1.25\"\n stroke-linecap=\"round\"\n />\n </svg>\n <span class=\"truncate max-w-[180px]\">{{ data().files[$index].label }}</span>\n <svg viewBox=\"0 0 16 16\" class=\"size-3 shrink-0 text-emerald-300\" fill=\"none\">\n <path\n d=\"M8 3v7m0 0L5.5 7.5M8 10l2.5-2.5M3 13h10\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n } @else {\n <!-- Canceled state -->\n <div class=\"flex justify-center\">\n <div\n class=\"inline-flex items-center gap-2.5 rounded-lg border border-slate-200 bg-slate-50 px-4 py-2.5\"\n >\n <div\n class=\"inline-flex size-6 shrink-0 items-center justify-center rounded-md border border-slate-200 bg-slate-100 text-slate-400\"\n >\n <svg viewBox=\"0 0 20 20\" class=\"size-3\" fill=\"none\">\n <path d=\"M6 10h8\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\n </svg>\n </div>\n <span class=\"text-[13px] font-medium text-slate-400\">{{\n 'translate_file_upload_skipped' | translate\n }}</span>\n </div>\n </div>\n }\n} @else {\n <!-- \u2500\u2500 Upload form \u2500\u2500 -->\n <form [formGroup]=\"form()\" class=\"w-full\">\n @if (data().files.length === 1) {\n <!-- Single file: minimal inline -->\n <fieldset [formArray]=\"form().controls.files\">\n <div [formGroupName]=\"0\" class=\"rounded-xl border border-slate-200 bg-slate-50 p-3\">\n <label for=\"capture-file-0\" class=\"mb-1.5 block text-[12px] text-slate-500\">\n {{ data().files[0].label }}\n </label>\n <input hidden formControlName=\"id\" />\n <div class=\"flex items-center gap-2\">\n <cortex-file-input id=\"capture-file-0\" formControlName=\"file\" class=\"min-w-0 flex-1\" />\n <button\n (click)=\"cancel()\"\n [disabled]=\"isLoading()\"\n type=\"button\"\n class=\"shrink-0 text-[12px] font-medium text-slate-400 transition-colors hover:text-slate-600\"\n >\n {{ 'translate_cancel' | translate }}\n </button>\n <button\n (click)=\"submit()\"\n type=\"button\"\n class=\"inline-flex shrink-0 items-center rounded-md bg-slate-800 px-3 py-1.5 text-[12px] font-medium text-slate-100 transition-colors hover:bg-slate-700 active:bg-slate-900 disabled:cursor-not-allowed disabled:bg-slate-300\"\n [disabled]=\"form().invalid || isLoading()\"\n >\n @if (isLoading()) {\n <svg class=\"mr-1.5 size-3 animate-spin\" viewBox=\"0 0 16 16\" fill=\"none\">\n <circle\n cx=\"8\"\n cy=\"8\"\n r=\"6\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n class=\"opacity-25\"\n />\n <path\n d=\"M14 8a6 6 0 0 0-6-6\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n {{ 'translate_uploading' | translate }}\n } @else {\n {{ 'translate_submit' | translate }}\n }\n </button>\n </div>\n </div>\n </fieldset>\n } @else {\n <!-- Multi-file: full form -->\n <div\n class=\"overflow-hidden rounded-2xl border border-slate-200 bg-gradient-to-b from-white to-slate-50\"\n >\n <div class=\"border-b border-slate-200 bg-white/85 px-4 py-3 sm:px-5\">\n <div class=\"flex items-start gap-3\">\n <div\n class=\"mt-0.5 inline-flex size-8 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-slate-100 text-slate-600\"\n aria-hidden=\"true\"\n >\n <svg viewBox=\"0 0 20 20\" class=\"size-4\" fill=\"none\">\n <path\n d=\"M4 13.5a3.5 3.5 0 0 1-.5-6.97 5.002 5.002 0 0 1 9.78-1.03A4.5 4.5 0 0 1 15.5 14\"\n stroke=\"currentColor\"\n stroke-width=\"1.4\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n <path\n d=\"M10 10v7m0-7-2.5 2.5M10 10l2.5 2.5\"\n stroke=\"currentColor\"\n stroke-width=\"1.4\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </div>\n\n <div class=\"min-w-0\">\n <h3 class=\"text-[13px] font-semibold tracking-tight text-slate-800\">\n {{ 'translate_attach_requested_files' | translate }}\n </h3>\n <p class=\"mt-0.5 text-[11px] text-slate-500\">\n {{ 'translate_n_files_required' | translate: { count: data().files.length } }}\n </p>\n </div>\n </div>\n </div>\n\n <fieldset [formArray]=\"form().controls.files\" class=\"grid gap-3 p-4 sm:p-5\">\n @for (file of form().controls.files.value; track $index) {\n <div\n [formGroupName]=\"$index\"\n class=\"rounded-xl border border-slate-200 bg-white p-3 transition-colors hover:border-slate-300\"\n >\n <div class=\"mb-2 flex items-center justify-between gap-2\">\n <label\n [for]=\"'capture-file-' + $index\"\n class=\"block text-[12px] font-medium leading-none text-slate-700\"\n >\n {{ data().files[$index].label }}\n </label>\n <span\n class=\"inline-flex items-center rounded border border-slate-200 bg-slate-50 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-slate-500\"\n >\n {{ 'translate_required' | translate }}\n </span>\n </div>\n\n <input hidden formControlName=\"id\" />\n <cortex-file-input [id]=\"'capture-file-' + $index\" formControlName=\"file\" />\n </div>\n }\n </fieldset>\n\n <div\n class=\"flex items-center justify-end gap-2 border-t border-slate-200 bg-white/90 px-4 py-3 sm:px-5\"\n >\n <button\n (click)=\"cancel()\"\n [disabled]=\"isLoading()\"\n type=\"button\"\n class=\"inline-flex items-center rounded-md border border-slate-300 bg-white px-3 py-1.5 text-[12px] font-medium text-slate-600 transition-colors hover:bg-slate-50 active:bg-slate-100\"\n >\n {{ 'translate_cancel' | translate }}\n </button>\n <button\n (click)=\"submit()\"\n type=\"button\"\n class=\"inline-flex items-center rounded-md bg-slate-800 px-3 py-1.5 text-[12px] font-medium text-slate-100 transition-colors hover:bg-slate-700 active:bg-slate-900 disabled:cursor-not-allowed disabled:bg-slate-300\"\n [disabled]=\"form().invalid || isLoading()\"\n >\n @if (isLoading()) {\n <svg class=\"mr-1.5 size-3 animate-spin\" viewBox=\"0 0 16 16\" fill=\"none\">\n <circle\n cx=\"8\"\n cy=\"8\"\n r=\"6\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n class=\"opacity-25\"\n />\n <path\n d=\"M14 8a6 6 0 0 0-6-6\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n {{ 'translate_uploading' | translate }}\n } @else {\n {{ 'translate_submit_files' | translate }}\n }\n </button>\n </div>\n </div>\n }\n </form>\n}\n", styles: [":host{display:block;width:100%}\n/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "directive", type: i0.forwardRef(() => i1$1.ɵNgNoValidate), selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i0.forwardRef(() => i1$1.DefaultValueAccessor), selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i0.forwardRef(() => i1$1.NgControlStatus), selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i0.forwardRef(() => i1$1.NgControlStatusGroup), selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i0.forwardRef(() => i1$1.FormGroupDirective), selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i0.forwardRef(() => i1$1.FormArrayDirective), selector: "[formArray]", inputs: ["formArray"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i0.forwardRef(() => i1$1.FormControlName), selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i0.forwardRef(() => i1$1.FormGroupName), selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "component", type: i0.forwardRef(() => FileInputComponent), selector: "cortex-file-input", inputs: ["allowedMimeTypes"] }, { kind: "pipe", type: i0.forwardRef(() => TranslatePipe), name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
622
|
+
}
|
|
623
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageCaptureFilesPartComponent, decorators: [{
|
|
624
|
+
type: Component,
|
|
625
|
+
args: [{ selector: 'cortex-message-capture-files-part', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ReactiveFormsModule, forwardRef(() => FileInputComponent), TranslatePipe], template: "@if (outputData(); as output) {\n <!-- \u2500\u2500 Result state \u2500\u2500 -->\n @if (output.status === 'success') {\n <div\n class=\"overflow-hidden rounded-2xl border border-emerald-200 bg-linear-to-b from-emerald-50/80 to-white\"\n >\n <div class=\"flex items-center gap-3 px-4 py-3 sm:px-5\">\n <div\n class=\"inline-flex size-8 shrink-0 items-center justify-center rounded-lg border border-emerald-200 bg-emerald-100 text-emerald-600\"\n >\n <svg viewBox=\"0 0 20 20\" class=\"size-4\" fill=\"none\">\n <path\n d=\"M5.5 10.5 L8.5 13.5 L14.5 7\"\n stroke=\"currentColor\"\n stroke-width=\"1.75\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </div>\n <div class=\"min-w-0\">\n <h3 class=\"text-[13px] font-semibold tracking-tight text-emerald-800\">\n {{ 'translate_files_attached' | translate: { count: data().files.length } }}\n </h3>\n </div>\n </div>\n\n <div class=\"border-t border-emerald-100 px-4 py-2.5 sm:px-5\">\n <ul class=\"flex flex-wrap gap-2\">\n @for (file of outputData()!.result!; track file.uploadId) {\n <li>\n <button\n type=\"button\"\n (click)=\"downloadFile(file.uploadId)\"\n class=\"inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-emerald-100 bg-white px-2.5 py-1.5 text-[12px] font-medium text-emerald-700 shadow-xs transition-all hover:border-emerald-200 hover:bg-emerald-50 hover:shadow-sm active:scale-[0.97]\"\n >\n <svg viewBox=\"0 0 16 16\" class=\"size-3.5 shrink-0 text-emerald-400\" fill=\"none\">\n <path\n d=\"M4 1.5h5.172a2 2 0 0 1 1.414.586l2.328 2.328a2 2 0 0 1 .586 1.414V12.5a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2Z\"\n stroke=\"currentColor\"\n stroke-width=\"1.25\"\n />\n <path\n d=\"M9.5 1.5v2a2 2 0 0 0 2 2h2\"\n stroke=\"currentColor\"\n stroke-width=\"1.25\"\n stroke-linecap=\"round\"\n />\n </svg>\n <span class=\"truncate max-w-[180px]\">{{ data().files[$index].label }}</span>\n <svg viewBox=\"0 0 16 16\" class=\"size-3 shrink-0 text-emerald-300\" fill=\"none\">\n <path\n d=\"M8 3v7m0 0L5.5 7.5M8 10l2.5-2.5M3 13h10\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n } @else {\n <!-- Canceled state -->\n <div class=\"flex justify-center\">\n <div\n class=\"inline-flex items-center gap-2.5 rounded-lg border border-slate-200 bg-slate-50 px-4 py-2.5\"\n >\n <div\n class=\"inline-flex size-6 shrink-0 items-center justify-center rounded-md border border-slate-200 bg-slate-100 text-slate-400\"\n >\n <svg viewBox=\"0 0 20 20\" class=\"size-3\" fill=\"none\">\n <path d=\"M6 10h8\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\n </svg>\n </div>\n <span class=\"text-[13px] font-medium text-slate-400\">{{\n 'translate_file_upload_skipped' | translate\n }}</span>\n </div>\n </div>\n }\n} @else {\n <!-- \u2500\u2500 Upload form \u2500\u2500 -->\n <form [formGroup]=\"form()\" class=\"w-full\">\n @if (data().files.length === 1) {\n <!-- Single file: minimal inline -->\n <fieldset [formArray]=\"form().controls.files\">\n <div [formGroupName]=\"0\" class=\"rounded-xl border border-slate-200 bg-slate-50 p-3\">\n <label for=\"capture-file-0\" class=\"mb-1.5 block text-[12px] text-slate-500\">\n {{ data().files[0].label }}\n </label>\n <input hidden formControlName=\"id\" />\n <div class=\"flex items-center gap-2\">\n <cortex-file-input id=\"capture-file-0\" formControlName=\"file\" class=\"min-w-0 flex-1\" />\n <button\n (click)=\"cancel()\"\n [disabled]=\"isLoading()\"\n type=\"button\"\n class=\"shrink-0 text-[12px] font-medium text-slate-400 transition-colors hover:text-slate-600\"\n >\n {{ 'translate_cancel' | translate }}\n </button>\n <button\n (click)=\"submit()\"\n type=\"button\"\n class=\"inline-flex shrink-0 items-center rounded-md bg-slate-800 px-3 py-1.5 text-[12px] font-medium text-slate-100 transition-colors hover:bg-slate-700 active:bg-slate-900 disabled:cursor-not-allowed disabled:bg-slate-300\"\n [disabled]=\"form().invalid || isLoading()\"\n >\n @if (isLoading()) {\n <svg class=\"mr-1.5 size-3 animate-spin\" viewBox=\"0 0 16 16\" fill=\"none\">\n <circle\n cx=\"8\"\n cy=\"8\"\n r=\"6\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n class=\"opacity-25\"\n />\n <path\n d=\"M14 8a6 6 0 0 0-6-6\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n {{ 'translate_uploading' | translate }}\n } @else {\n {{ 'translate_submit' | translate }}\n }\n </button>\n </div>\n </div>\n </fieldset>\n } @else {\n <!-- Multi-file: full form -->\n <div\n class=\"overflow-hidden rounded-2xl border border-slate-200 bg-gradient-to-b from-white to-slate-50\"\n >\n <div class=\"border-b border-slate-200 bg-white/85 px-4 py-3 sm:px-5\">\n <div class=\"flex items-start gap-3\">\n <div\n class=\"mt-0.5 inline-flex size-8 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-slate-100 text-slate-600\"\n aria-hidden=\"true\"\n >\n <svg viewBox=\"0 0 20 20\" class=\"size-4\" fill=\"none\">\n <path\n d=\"M4 13.5a3.5 3.5 0 0 1-.5-6.97 5.002 5.002 0 0 1 9.78-1.03A4.5 4.5 0 0 1 15.5 14\"\n stroke=\"currentColor\"\n stroke-width=\"1.4\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n <path\n d=\"M10 10v7m0-7-2.5 2.5M10 10l2.5 2.5\"\n stroke=\"currentColor\"\n stroke-width=\"1.4\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </div>\n\n <div class=\"min-w-0\">\n <h3 class=\"text-[13px] font-semibold tracking-tight text-slate-800\">\n {{ 'translate_attach_requested_files' | translate }}\n </h3>\n <p class=\"mt-0.5 text-[11px] text-slate-500\">\n {{ 'translate_n_files_required' | translate: { count: data().files.length } }}\n </p>\n </div>\n </div>\n </div>\n\n <fieldset [formArray]=\"form().controls.files\" class=\"grid gap-3 p-4 sm:p-5\">\n @for (file of form().controls.files.value; track $index) {\n <div\n [formGroupName]=\"$index\"\n class=\"rounded-xl border border-slate-200 bg-white p-3 transition-colors hover:border-slate-300\"\n >\n <div class=\"mb-2 flex items-center justify-between gap-2\">\n <label\n [for]=\"'capture-file-' + $index\"\n class=\"block text-[12px] font-medium leading-none text-slate-700\"\n >\n {{ data().files[$index].label }}\n </label>\n <span\n class=\"inline-flex items-center rounded border border-slate-200 bg-slate-50 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-slate-500\"\n >\n {{ 'translate_required' | translate }}\n </span>\n </div>\n\n <input hidden formControlName=\"id\" />\n <cortex-file-input [id]=\"'capture-file-' + $index\" formControlName=\"file\" />\n </div>\n }\n </fieldset>\n\n <div\n class=\"flex items-center justify-end gap-2 border-t border-slate-200 bg-white/90 px-4 py-3 sm:px-5\"\n >\n <button\n (click)=\"cancel()\"\n [disabled]=\"isLoading()\"\n type=\"button\"\n class=\"inline-flex items-center rounded-md border border-slate-300 bg-white px-3 py-1.5 text-[12px] font-medium text-slate-600 transition-colors hover:bg-slate-50 active:bg-slate-100\"\n >\n {{ 'translate_cancel' | translate }}\n </button>\n <button\n (click)=\"submit()\"\n type=\"button\"\n class=\"inline-flex items-center rounded-md bg-slate-800 px-3 py-1.5 text-[12px] font-medium text-slate-100 transition-colors hover:bg-slate-700 active:bg-slate-900 disabled:cursor-not-allowed disabled:bg-slate-300\"\n [disabled]=\"form().invalid || isLoading()\"\n >\n @if (isLoading()) {\n <svg class=\"mr-1.5 size-3 animate-spin\" viewBox=\"0 0 16 16\" fill=\"none\">\n <circle\n cx=\"8\"\n cy=\"8\"\n r=\"6\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n class=\"opacity-25\"\n />\n <path\n d=\"M14 8a6 6 0 0 0-6-6\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n {{ 'translate_uploading' | translate }}\n } @else {\n {{ 'translate_submit_files' | translate }}\n }\n </button>\n </div>\n </div>\n }\n </form>\n}\n", styles: [":host{display:block;width:100%}\n/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */\n"] }]
|
|
626
|
+
}], propDecorators: { message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: true }] }], toolPart: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolPart", required: true }] }] } });
|
|
627
|
+
class FileInputComponent {
|
|
628
|
+
allowedMimeTypes = input('*/*', ...(ngDevMode ? [{ debugName: "allowedMimeTypes" }] : []));
|
|
629
|
+
value = signal(undefined, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
630
|
+
isDisabled = signal(false, ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
631
|
+
onTouch;
|
|
632
|
+
isProcessing = signal(false, ...(ngDevMode ? [{ debugName: "isProcessing" }] : []));
|
|
633
|
+
onChange;
|
|
634
|
+
writeValue(value) {
|
|
635
|
+
this.value.set(value);
|
|
636
|
+
}
|
|
637
|
+
registerOnChange(fn) {
|
|
638
|
+
this.onChange = fn;
|
|
639
|
+
}
|
|
640
|
+
registerOnTouched(fn) {
|
|
641
|
+
this.onTouch = fn;
|
|
642
|
+
}
|
|
643
|
+
setDisabledState(isDisabled) {
|
|
644
|
+
this.isDisabled.set(isDisabled);
|
|
645
|
+
}
|
|
646
|
+
async loadFile(event) {
|
|
647
|
+
const fileInput = event.target;
|
|
648
|
+
if (fileInput.files?.length !== 1)
|
|
649
|
+
return;
|
|
650
|
+
const inputFile = fileInput.files[0];
|
|
651
|
+
const result = await convertFileToBase64(inputFile);
|
|
652
|
+
this.updateValue({
|
|
653
|
+
name: result.name,
|
|
654
|
+
bytes: result.bytes,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
updateValue(value) {
|
|
658
|
+
this.value.set(value);
|
|
659
|
+
this.onChange?.(value);
|
|
660
|
+
}
|
|
661
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
662
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.0", type: FileInputComponent, isStandalone: true, selector: "cortex-file-input", inputs: { allowedMimeTypes: { classPropertyName: "allowedMimeTypes", publicName: "allowedMimeTypes", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
663
|
+
{
|
|
664
|
+
provide: NG_VALUE_ACCESSOR,
|
|
665
|
+
multi: true,
|
|
666
|
+
useExisting: forwardRef(() => FileInputComponent),
|
|
667
|
+
},
|
|
668
|
+
], ngImport: i0, template: `<div
|
|
669
|
+
class="flex w-full cursor-pointer items-center rounded-lg border border-slate-300 bg-slate-50 transition hover:border-slate-400 focus-within:ring-2 focus-within:ring-slate-300"
|
|
670
|
+
>
|
|
671
|
+
<button
|
|
672
|
+
type="button"
|
|
673
|
+
(click)="fileInput.click()"
|
|
674
|
+
class="m-1 shrink-0 cursor-pointer rounded-md border-0 bg-slate-800 px-3 py-1.5 text-[11px] font-semibold tracking-wide text-slate-100 hover:bg-slate-700"
|
|
675
|
+
>
|
|
676
|
+
{{ 'translate_choose_file' | translate }}
|
|
677
|
+
</button>
|
|
678
|
+
<span class="truncate px-3 text-[12px] text-slate-600">{{
|
|
679
|
+
value()?.name ?? ('translate_no_file_chosen' | translate)
|
|
680
|
+
}}</span>
|
|
681
|
+
<input #fileInput (change)="loadFile($event)" type="file" class="hidden" />
|
|
682
|
+
</div>`, isInline: true, dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
683
|
+
}
|
|
684
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FileInputComponent, decorators: [{
|
|
685
|
+
type: Component,
|
|
686
|
+
args: [{
|
|
687
|
+
selector: 'cortex-file-input',
|
|
688
|
+
template: `<div
|
|
689
|
+
class="flex w-full cursor-pointer items-center rounded-lg border border-slate-300 bg-slate-50 transition hover:border-slate-400 focus-within:ring-2 focus-within:ring-slate-300"
|
|
690
|
+
>
|
|
691
|
+
<button
|
|
692
|
+
type="button"
|
|
693
|
+
(click)="fileInput.click()"
|
|
694
|
+
class="m-1 shrink-0 cursor-pointer rounded-md border-0 bg-slate-800 px-3 py-1.5 text-[11px] font-semibold tracking-wide text-slate-100 hover:bg-slate-700"
|
|
695
|
+
>
|
|
696
|
+
{{ 'translate_choose_file' | translate }}
|
|
697
|
+
</button>
|
|
698
|
+
<span class="truncate px-3 text-[12px] text-slate-600">{{
|
|
699
|
+
value()?.name ?? ('translate_no_file_chosen' | translate)
|
|
700
|
+
}}</span>
|
|
701
|
+
<input #fileInput (change)="loadFile($event)" type="file" class="hidden" />
|
|
702
|
+
</div>`,
|
|
703
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
704
|
+
imports: [TranslatePipe],
|
|
705
|
+
providers: [
|
|
706
|
+
{
|
|
707
|
+
provide: NG_VALUE_ACCESSOR,
|
|
708
|
+
multi: true,
|
|
709
|
+
useExisting: forwardRef(() => FileInputComponent),
|
|
710
|
+
},
|
|
711
|
+
],
|
|
712
|
+
}]
|
|
713
|
+
}], propDecorators: { allowedMimeTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedMimeTypes", required: false }] }] } });
|
|
714
|
+
async function convertFileToBase64(file) {
|
|
715
|
+
return new Promise((resolve, reject) => {
|
|
716
|
+
const tokens = file.name.split('.');
|
|
717
|
+
const format = tokens.length > 1 ? tokens[tokens.length - 1] : '';
|
|
718
|
+
const reader = new FileReader();
|
|
719
|
+
reader.readAsDataURL(file);
|
|
720
|
+
reader.onload = () => {
|
|
721
|
+
const parts = reader.result.split(',');
|
|
722
|
+
const mimeType = parts[0].replace('data:', '').replace(';base64', '');
|
|
723
|
+
resolve({
|
|
724
|
+
name: file.name,
|
|
725
|
+
bytes: parts[1],
|
|
726
|
+
mimeType,
|
|
727
|
+
format,
|
|
728
|
+
readerResult: reader.result,
|
|
729
|
+
});
|
|
730
|
+
};
|
|
731
|
+
reader.onerror = () => reject();
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const STATUS_COUNT = 10;
|
|
736
|
+
class MessageToolCallAnimatedComponent {
|
|
737
|
+
message = input.required(...(ngDevMode ? [{ debugName: "message" }] : []));
|
|
738
|
+
toolCallPart = input.required(...(ngDevMode ? [{ debugName: "toolCallPart" }] : []));
|
|
739
|
+
config = inject(CORTEX_CLIENT_CONFIG);
|
|
740
|
+
customComponent = computed(() => {
|
|
741
|
+
const toolName = this.toolCallPart().type;
|
|
742
|
+
const name = toolName?.startsWith('tool-') ? toolName.slice('tool-'.length) : toolName;
|
|
743
|
+
return name ? (this.config.toolComponents?.[name] ?? null) : null;
|
|
744
|
+
}, ...(ngDevMode ? [{ debugName: "customComponent" }] : []));
|
|
745
|
+
animState = computed(() => {
|
|
746
|
+
const state = this.toolCallPart().state;
|
|
747
|
+
switch (state) {
|
|
748
|
+
case 'input-streaming':
|
|
749
|
+
return 'starting';
|
|
750
|
+
case 'input-available':
|
|
751
|
+
case 'approval-requested':
|
|
752
|
+
case 'approval-responded':
|
|
753
|
+
return 'processing';
|
|
754
|
+
case 'output-available':
|
|
755
|
+
return 'complete';
|
|
756
|
+
case 'output-error':
|
|
757
|
+
return 'error';
|
|
758
|
+
case 'output-denied':
|
|
759
|
+
return 'denied';
|
|
760
|
+
default:
|
|
761
|
+
return 'processing';
|
|
762
|
+
}
|
|
763
|
+
}, ...(ngDevMode ? [{ debugName: "animState" }] : []));
|
|
764
|
+
isActive = computed(() => {
|
|
765
|
+
const s = this.animState();
|
|
766
|
+
return s === 'starting' || s === 'processing';
|
|
767
|
+
}, ...(ngDevMode ? [{ debugName: "isActive" }] : []));
|
|
768
|
+
displayTitleKey = computed(() => {
|
|
769
|
+
const state = this.animState();
|
|
770
|
+
switch (state) {
|
|
771
|
+
case 'starting':
|
|
772
|
+
case 'processing':
|
|
773
|
+
return `translate_tool_status_${this.stableIndex()}`;
|
|
774
|
+
case 'complete':
|
|
775
|
+
return `translate_tool_done_${this.stableIndex()}`;
|
|
776
|
+
case 'error':
|
|
777
|
+
return 'translate_tool_error';
|
|
778
|
+
case 'denied':
|
|
779
|
+
return 'translate_tool_denied';
|
|
780
|
+
default:
|
|
781
|
+
return `translate_tool_status_${this.stableIndex()}`;
|
|
782
|
+
}
|
|
783
|
+
}, ...(ngDevMode ? [{ debugName: "displayTitleKey" }] : []));
|
|
784
|
+
stableIndex = computed(() => {
|
|
785
|
+
const id = this.toolCallPart().toolCallId ?? '';
|
|
786
|
+
let hash = 0;
|
|
787
|
+
for (let i = 0; i < id.length; i++) {
|
|
788
|
+
hash = (hash * 31 + id.charCodeAt(i)) | 0;
|
|
789
|
+
}
|
|
790
|
+
return Math.abs(hash) % STATUS_COUNT;
|
|
791
|
+
}, ...(ngDevMode ? [{ debugName: "stableIndex" }] : []));
|
|
792
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageToolCallAnimatedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
793
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: MessageToolCallAnimatedComponent, isStandalone: true, selector: "cortex-message-tool-call-animated", inputs: { message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: true, transformFunction: null }, toolCallPart: { classPropertyName: "toolCallPart", publicName: "toolCallPart", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "block w-full" }, ngImport: i0, template: "@if (customComponent(); as custom) {\n <ng-container\n *ngComponentOutlet=\"custom; inputs: { toolCallPart: toolCallPart(), message: message() }\"\n />\n} @else {\n @switch (toolCallPart().type) {\n @case ('tool-captureFiles') {\n <cortex-message-capture-files-part [toolPart]=\"toolCallPart()\" [message]=\"message()\" />\n }\n @default {\n @let state = animState();\n @let active = isActive();\n\n <div class=\"flex justify-center\">\n <div\n class=\"inline-flex items-center gap-2.5 py-2.5 px-4 rounded-lg border relative overflow-hidden transition-colors duration-300\"\n [ngClass]=\"{\n 'border-indigo-200 bg-indigo-50': active,\n 'border-emerald-200 bg-emerald-50': state === 'complete',\n 'border-red-200 bg-red-50': state === 'error',\n 'border-slate-200 bg-slate-50 opacity-60': state === 'denied',\n }\"\n >\n <!-- Icon box -->\n <div\n class=\"relative size-6 flex items-center justify-center rounded-md border shrink-0 transition-colors duration-300\"\n [ngClass]=\"{\n 'bg-indigo-100 border-indigo-200': active,\n 'bg-emerald-100 border-emerald-200': state === 'complete',\n 'bg-red-100 border-red-200': state === 'error',\n 'bg-slate-100 border-slate-200': state === 'denied',\n }\"\n >\n <!-- Spinner -->\n <div\n class=\"absolute size-3 border-[1.5px] border-indigo-200 border-t-indigo-500 rounded-full animate-spin transition-opacity duration-200\"\n [ngClass]=\"active ? 'opacity-100' : 'opacity-0'\"\n ></div>\n <!-- Check -->\n <svg\n class=\"absolute size-3 transition-all duration-200\"\n [ngClass]=\"\n state === 'complete'\n ? 'opacity-100 scale-100 text-emerald-600'\n : 'opacity-0 scale-[0.6]'\n \"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\n d=\"M5.5 10.5 L8.5 13.5 L14.5 7\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n <!-- Error -->\n <svg\n class=\"absolute size-3 transition-all duration-200\"\n [ngClass]=\"\n state === 'error' ? 'opacity-100 scale-100 text-red-600' : 'opacity-0 scale-[0.6]'\n \"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\n d=\"M6.5 6.5 L13.5 13.5 M13.5 6.5 L6.5 13.5\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n <!-- Denied -->\n <svg\n class=\"absolute size-3 transition-all duration-200\"\n [ngClass]=\"\n state === 'denied'\n ? 'opacity-100 scale-100 text-slate-400'\n : 'opacity-0 scale-[0.6]'\n \"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\n d=\"M6 10 L14 10\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </div>\n\n <!-- Title -->\n <span\n class=\"text-[13px] font-medium whitespace-nowrap overflow-hidden text-ellipsis transition-colors duration-300\"\n [ngClass]=\"{\n 'text-indigo-700': active,\n 'text-emerald-700': state === 'complete',\n 'text-red-600': state === 'error',\n 'text-slate-400': state === 'denied',\n }\"\n >\n {{ displayTitleKey() | translate }}\n </span>\n\n <!-- Bottom bar -->\n <div class=\"absolute bottom-0 inset-x-0 h-0.5 overflow-hidden\">\n <div\n class=\"h-full w-full origin-left\"\n [ngClass]=\"{\n 'bg-indigo-400 animate-bar-slide': active,\n 'bg-emerald-400 animate-bar-fill': state === 'complete',\n 'bg-red-400 animate-bar-fill': state === 'error',\n 'opacity-0': state === 'denied',\n }\"\n ></div>\n </div>\n </div>\n </div>\n }\n }\n}\n", styles: [".animate-bar-slide{animation:bar-slide 1.5s ease-in-out infinite}.animate-bar-fill{animation:bar-fill .3s ease-out both}@keyframes bar-slide{0%{transform:translate(-100%)}to{transform:translate(100%)}}@keyframes bar-fill{0%{transform:scaleX(0)}to{transform:scaleX(1)}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MessageCaptureFilesPartComponent, selector: "cortex-message-capture-files-part", inputs: ["message", "toolPart"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
794
|
+
}
|
|
795
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageToolCallAnimatedComponent, decorators: [{
|
|
796
|
+
type: Component,
|
|
797
|
+
args: [{ selector: 'cortex-message-tool-call-animated', imports: [NgClass, MessageCaptureFilesPartComponent, TranslatePipe], host: { class: 'block w-full' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (customComponent(); as custom) {\n <ng-container\n *ngComponentOutlet=\"custom; inputs: { toolCallPart: toolCallPart(), message: message() }\"\n />\n} @else {\n @switch (toolCallPart().type) {\n @case ('tool-captureFiles') {\n <cortex-message-capture-files-part [toolPart]=\"toolCallPart()\" [message]=\"message()\" />\n }\n @default {\n @let state = animState();\n @let active = isActive();\n\n <div class=\"flex justify-center\">\n <div\n class=\"inline-flex items-center gap-2.5 py-2.5 px-4 rounded-lg border relative overflow-hidden transition-colors duration-300\"\n [ngClass]=\"{\n 'border-indigo-200 bg-indigo-50': active,\n 'border-emerald-200 bg-emerald-50': state === 'complete',\n 'border-red-200 bg-red-50': state === 'error',\n 'border-slate-200 bg-slate-50 opacity-60': state === 'denied',\n }\"\n >\n <!-- Icon box -->\n <div\n class=\"relative size-6 flex items-center justify-center rounded-md border shrink-0 transition-colors duration-300\"\n [ngClass]=\"{\n 'bg-indigo-100 border-indigo-200': active,\n 'bg-emerald-100 border-emerald-200': state === 'complete',\n 'bg-red-100 border-red-200': state === 'error',\n 'bg-slate-100 border-slate-200': state === 'denied',\n }\"\n >\n <!-- Spinner -->\n <div\n class=\"absolute size-3 border-[1.5px] border-indigo-200 border-t-indigo-500 rounded-full animate-spin transition-opacity duration-200\"\n [ngClass]=\"active ? 'opacity-100' : 'opacity-0'\"\n ></div>\n <!-- Check -->\n <svg\n class=\"absolute size-3 transition-all duration-200\"\n [ngClass]=\"\n state === 'complete'\n ? 'opacity-100 scale-100 text-emerald-600'\n : 'opacity-0 scale-[0.6]'\n \"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\n d=\"M5.5 10.5 L8.5 13.5 L14.5 7\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n <!-- Error -->\n <svg\n class=\"absolute size-3 transition-all duration-200\"\n [ngClass]=\"\n state === 'error' ? 'opacity-100 scale-100 text-red-600' : 'opacity-0 scale-[0.6]'\n \"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\n d=\"M6.5 6.5 L13.5 13.5 M13.5 6.5 L6.5 13.5\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n <!-- Denied -->\n <svg\n class=\"absolute size-3 transition-all duration-200\"\n [ngClass]=\"\n state === 'denied'\n ? 'opacity-100 scale-100 text-slate-400'\n : 'opacity-0 scale-[0.6]'\n \"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\n d=\"M6 10 L14 10\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </div>\n\n <!-- Title -->\n <span\n class=\"text-[13px] font-medium whitespace-nowrap overflow-hidden text-ellipsis transition-colors duration-300\"\n [ngClass]=\"{\n 'text-indigo-700': active,\n 'text-emerald-700': state === 'complete',\n 'text-red-600': state === 'error',\n 'text-slate-400': state === 'denied',\n }\"\n >\n {{ displayTitleKey() | translate }}\n </span>\n\n <!-- Bottom bar -->\n <div class=\"absolute bottom-0 inset-x-0 h-0.5 overflow-hidden\">\n <div\n class=\"h-full w-full origin-left\"\n [ngClass]=\"{\n 'bg-indigo-400 animate-bar-slide': active,\n 'bg-emerald-400 animate-bar-fill': state === 'complete',\n 'bg-red-400 animate-bar-fill': state === 'error',\n 'opacity-0': state === 'denied',\n }\"\n ></div>\n </div>\n </div>\n </div>\n }\n }\n}\n", styles: [".animate-bar-slide{animation:bar-slide 1.5s ease-in-out infinite}.animate-bar-fill{animation:bar-fill .3s ease-out both}@keyframes bar-slide{0%{transform:translate(-100%)}to{transform:translate(100%)}}@keyframes bar-fill{0%{transform:scaleX(0)}to{transform:scaleX(1)}}\n"] }]
|
|
798
|
+
}], propDecorators: { message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: true }] }], toolCallPart: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolCallPart", required: true }] }] } });
|
|
799
|
+
|
|
800
|
+
class MessageReasoningAnimatedComponent {
|
|
801
|
+
reasoningPart = input.required(...(ngDevMode ? [{ debugName: "reasoningPart" }] : []));
|
|
802
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageReasoningAnimatedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
803
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.0", type: MessageReasoningAnimatedComponent, isStandalone: true, selector: "cortex-message-reasoning-animated", inputs: { reasoningPart: { classPropertyName: "reasoningPart", publicName: "reasoningPart", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "flex justify-center" }, ngImport: i0, template: "@let p = reasoningPart();\n@let streaming = p.state === 'streaming';\n\n<div\n class=\"inline-flex items-center gap-2.5 py-2 pl-3 pr-4 rounded-lg border transition-colors duration-300\"\n [ngClass]=\"streaming ? 'border-indigo-200 bg-indigo-50' : 'border-slate-200 bg-slate-50'\"\n>\n <div class=\"flex items-center gap-1\">\n <span\n class=\"size-[5px] rounded-full transition-all duration-400\"\n [ngClass]=\"streaming ? 'animate-dot-pulse bg-indigo-300' : 'bg-slate-300 opacity-40'\"\n ></span>\n <span\n class=\"size-[5px] rounded-full transition-all duration-400\"\n [ngClass]=\"streaming ? 'animate-dot-pulse bg-indigo-300' : 'bg-slate-300 opacity-40'\"\n [style.animation-delay]=\"streaming ? '200ms' : '0ms'\"\n ></span>\n <span\n class=\"size-[5px] rounded-full transition-all duration-400\"\n [ngClass]=\"streaming ? 'animate-dot-pulse bg-indigo-300' : 'bg-slate-300 opacity-40'\"\n [style.animation-delay]=\"streaming ? '400ms' : '0ms'\"\n ></span>\n <span\n class=\"size-[5px] rounded-full transition-all duration-400\"\n [ngClass]=\"streaming ? 'animate-dot-pulse bg-indigo-300' : 'bg-slate-300 opacity-40'\"\n [style.animation-delay]=\"streaming ? '600ms' : '0ms'\"\n ></span>\n </div>\n\n <span\n class=\"text-[13px] font-medium transition-colors duration-300\"\n [ngClass]=\"streaming ? 'text-indigo-600' : 'text-slate-400'\"\n >\n {{ streaming ? ('translate_thinking' | translate) : ('translate_reasoned' | translate) }}\n </span>\n</div>\n", styles: [".animate-dot-pulse{animation:dot-pulse 1.4s ease-in-out infinite}@keyframes dot-pulse{0%,to{opacity:.3;transform:scale(.8)}40%{opacity:1;transform:scale(1.3);background-color:#6366f1}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
804
|
+
}
|
|
805
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageReasoningAnimatedComponent, decorators: [{
|
|
806
|
+
type: Component,
|
|
807
|
+
args: [{ selector: 'cortex-message-reasoning-animated', imports: [NgClass, TranslatePipe], host: { class: 'flex justify-center' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@let p = reasoningPart();\n@let streaming = p.state === 'streaming';\n\n<div\n class=\"inline-flex items-center gap-2.5 py-2 pl-3 pr-4 rounded-lg border transition-colors duration-300\"\n [ngClass]=\"streaming ? 'border-indigo-200 bg-indigo-50' : 'border-slate-200 bg-slate-50'\"\n>\n <div class=\"flex items-center gap-1\">\n <span\n class=\"size-[5px] rounded-full transition-all duration-400\"\n [ngClass]=\"streaming ? 'animate-dot-pulse bg-indigo-300' : 'bg-slate-300 opacity-40'\"\n ></span>\n <span\n class=\"size-[5px] rounded-full transition-all duration-400\"\n [ngClass]=\"streaming ? 'animate-dot-pulse bg-indigo-300' : 'bg-slate-300 opacity-40'\"\n [style.animation-delay]=\"streaming ? '200ms' : '0ms'\"\n ></span>\n <span\n class=\"size-[5px] rounded-full transition-all duration-400\"\n [ngClass]=\"streaming ? 'animate-dot-pulse bg-indigo-300' : 'bg-slate-300 opacity-40'\"\n [style.animation-delay]=\"streaming ? '400ms' : '0ms'\"\n ></span>\n <span\n class=\"size-[5px] rounded-full transition-all duration-400\"\n [ngClass]=\"streaming ? 'animate-dot-pulse bg-indigo-300' : 'bg-slate-300 opacity-40'\"\n [style.animation-delay]=\"streaming ? '600ms' : '0ms'\"\n ></span>\n </div>\n\n <span\n class=\"text-[13px] font-medium transition-colors duration-300\"\n [ngClass]=\"streaming ? 'text-indigo-600' : 'text-slate-400'\"\n >\n {{ streaming ? ('translate_thinking' | translate) : ('translate_reasoned' | translate) }}\n </span>\n</div>\n", styles: [".animate-dot-pulse{animation:dot-pulse 1.4s ease-in-out infinite}@keyframes dot-pulse{0%,to{opacity:.3;transform:scale(.8)}40%{opacity:1;transform:scale(1.3);background-color:#6366f1}}\n"] }]
|
|
808
|
+
}], propDecorators: { reasoningPart: [{ type: i0.Input, args: [{ isSignal: true, alias: "reasoningPart", required: true }] }] } });
|
|
809
|
+
|
|
810
|
+
class MessagePartComponent {
|
|
811
|
+
message = input.required(...(ngDevMode ? [{ debugName: "message" }] : []));
|
|
812
|
+
part = input.required(...(ngDevMode ? [{ debugName: "part" }] : []));
|
|
813
|
+
debugMode = input(false, ...(ngDevMode ? [{ debugName: "debugMode" }] : []));
|
|
814
|
+
animate = input(false, ...(ngDevMode ? [{ debugName: "animate" }] : []));
|
|
815
|
+
config = inject(CORTEX_CLIENT_CONFIG);
|
|
816
|
+
isStaticToolUIPart = isStaticToolUIPart;
|
|
817
|
+
customToolComponent = computed(() => {
|
|
818
|
+
const p = this.part();
|
|
819
|
+
if (!isStaticToolUIPart(p))
|
|
820
|
+
return null;
|
|
821
|
+
const toolName = p.type?.startsWith('tool-') ? p.type.slice('tool-'.length) : p.type;
|
|
822
|
+
return toolName ? (this.config.toolComponents?.[toolName] ?? null) : null;
|
|
823
|
+
}, ...(ngDevMode ? [{ debugName: "customToolComponent" }] : []));
|
|
824
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessagePartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
825
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: MessagePartComponent, isStandalone: true, selector: "cortex-message-part", inputs: { message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: true, transformFunction: null }, part: { classPropertyName: "part", publicName: "part", isSignal: true, isRequired: true, transformFunction: null }, debugMode: { classPropertyName: "debugMode", publicName: "debugMode", isSignal: true, isRequired: false, transformFunction: null }, animate: { classPropertyName: "animate", publicName: "animate", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.animate-part-enter": "animate()" } }, ngImport: i0, template: "@let myPart = part();\n@switch (myPart.type) {\n @case ('text') {\n <cortex-message-text-part [textPart]=\"myPart\" [role]=\"message().role\" />\n }\n @case ('reasoning') {\n @if (debugMode()) {\n <cortex-message-reasoning-part [reasoningPart]=\"myPart\" />\n } @else {\n <cortex-message-reasoning-animated [reasoningPart]=\"myPart\" />\n }\n }\n @default {\n @if (isStaticToolUIPart(myPart)) {\n @if (!debugMode() || customToolComponent()) {\n <cortex-message-tool-call-animated [toolCallPart]=\"myPart\" [message]=\"message()\" />\n } @else {\n <cortex-message-tool-call-part [toolCallPart]=\"myPart\" />\n }\n } @else {\n <p class=\"text-center text-xs text-slate-400\">Unhandled type: {{ part().type }}</p>\n }\n }\n}\n", styles: [":host{display:block}:host:empty{display:none}:host.animate-part-enter{animation:partFadeInUp .2s cubic-bezier(.34,1.56,.64,1) both}@keyframes partFadeInUp{0%{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}\n/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: MessageTextPartComponent, selector: "cortex-message-text-part", inputs: ["role", "textPart"] }, { kind: "component", type: MessageReasoningPartComponent, selector: "cortex-message-reasoning-part", inputs: ["reasoningPart"] }, { kind: "component", type: MessageToolCallPartComponent, selector: "cortex-message-tool-call-part", inputs: ["toolCallPart"] }, { kind: "component", type: MessageToolCallAnimatedComponent, selector: "cortex-message-tool-call-animated", inputs: ["message", "toolCallPart"] }, { kind: "component", type: MessageReasoningAnimatedComponent, selector: "cortex-message-reasoning-animated", inputs: ["reasoningPart"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
826
|
+
}
|
|
827
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessagePartComponent, decorators: [{
|
|
828
|
+
type: Component,
|
|
829
|
+
args: [{ selector: 'cortex-message-part', imports: [
|
|
830
|
+
MessageTextPartComponent,
|
|
831
|
+
MessageReasoningPartComponent,
|
|
832
|
+
MessageToolCallPartComponent,
|
|
833
|
+
MessageToolCallAnimatedComponent,
|
|
834
|
+
MessageReasoningAnimatedComponent,
|
|
835
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
836
|
+
'[class.animate-part-enter]': 'animate()',
|
|
837
|
+
}, template: "@let myPart = part();\n@switch (myPart.type) {\n @case ('text') {\n <cortex-message-text-part [textPart]=\"myPart\" [role]=\"message().role\" />\n }\n @case ('reasoning') {\n @if (debugMode()) {\n <cortex-message-reasoning-part [reasoningPart]=\"myPart\" />\n } @else {\n <cortex-message-reasoning-animated [reasoningPart]=\"myPart\" />\n }\n }\n @default {\n @if (isStaticToolUIPart(myPart)) {\n @if (!debugMode() || customToolComponent()) {\n <cortex-message-tool-call-animated [toolCallPart]=\"myPart\" [message]=\"message()\" />\n } @else {\n <cortex-message-tool-call-part [toolCallPart]=\"myPart\" />\n }\n } @else {\n <p class=\"text-center text-xs text-slate-400\">Unhandled type: {{ part().type }}</p>\n }\n }\n}\n", styles: [":host{display:block}:host:empty{display:none}:host.animate-part-enter{animation:partFadeInUp .2s cubic-bezier(.34,1.56,.64,1) both}@keyframes partFadeInUp{0%{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}\n/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */\n"] }]
|
|
838
|
+
}], propDecorators: { message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: true }] }], part: [{ type: i0.Input, args: [{ isSignal: true, alias: "part", required: true }] }], debugMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "debugMode", required: false }] }], animate: [{ type: i0.Input, args: [{ isSignal: true, alias: "animate", required: false }] }] } });
|
|
839
|
+
|
|
840
|
+
class MessageComponent {
|
|
841
|
+
message = input.required(...(ngDevMode ? [{ debugName: "message" }] : []));
|
|
842
|
+
debugMode = input(false, ...(ngDevMode ? [{ debugName: "debugMode" }] : []));
|
|
843
|
+
animate = input(false, ...(ngDevMode ? [{ debugName: "animate" }] : []));
|
|
844
|
+
visibleParts = computed(() => this.parts().filter((x) => x.type !== 'step-start'), ...(ngDevMode ? [{ debugName: "visibleParts" }] : []));
|
|
845
|
+
parts = signal([], ...(ngDevMode ? [{ debugName: "parts" }] : []));
|
|
846
|
+
constructor() {
|
|
847
|
+
effect(() => {
|
|
848
|
+
const parts = this.message().parts;
|
|
849
|
+
this.parts.set(parts);
|
|
850
|
+
const originalPush = parts.push;
|
|
851
|
+
parts.push = (...items) => {
|
|
852
|
+
const newParts = items;
|
|
853
|
+
newParts.forEach((x) => (x.__id = generateId()));
|
|
854
|
+
this.parts.update((x) => [...x, ...newParts]);
|
|
855
|
+
newParts.forEach((part) => {
|
|
856
|
+
Object.keys(part).forEach((prop) => {
|
|
857
|
+
let value = part[prop];
|
|
858
|
+
Object.defineProperty(part, prop, {
|
|
859
|
+
get: () => value,
|
|
860
|
+
set: (newValue) => {
|
|
861
|
+
value = newValue;
|
|
862
|
+
this.parts.update((existingParts) => {
|
|
863
|
+
return existingParts.map((x) => {
|
|
864
|
+
return x.__id === part.__id ? { ...x, [prop]: newValue } : x;
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
},
|
|
868
|
+
configurable: true,
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
return originalPush.apply(parts, newParts);
|
|
873
|
+
};
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
877
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: MessageComponent, isStandalone: true, selector: "cortex-message", inputs: { message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: true, transformFunction: null }, debugMode: { classPropertyName: "debugMode", publicName: "debugMode", isSignal: true, isRequired: false, transformFunction: null }, animate: { classPropertyName: "animate", publicName: "animate", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (visibleParts().length) {\n <div class=\"w-full flex flex-col gap-2\">\n @for (part of visibleParts(); track $index) {\n <cortex-message-part\n [part]=\"part\"\n [message]=\"message()\"\n [debugMode]=\"debugMode()\"\n [animate]=\"animate()\"\n />\n }\n </div>\n}\n", styles: [":host:empty{display:none}\n/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: MessagePartComponent, selector: "cortex-message-part", inputs: ["message", "part", "debugMode", "animate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
878
|
+
}
|
|
879
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageComponent, decorators: [{
|
|
880
|
+
type: Component,
|
|
881
|
+
args: [{ selector: 'cortex-message', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MessagePartComponent], template: "@if (visibleParts().length) {\n <div class=\"w-full flex flex-col gap-2\">\n @for (part of visibleParts(); track $index) {\n <cortex-message-part\n [part]=\"part\"\n [message]=\"message()\"\n [debugMode]=\"debugMode()\"\n [animate]=\"animate()\"\n />\n }\n </div>\n}\n", styles: [":host:empty{display:none}\n/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */\n"] }]
|
|
882
|
+
}], ctorParameters: () => [], propDecorators: { message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: true }] }], debugMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "debugMode", required: false }] }], animate: [{ type: i0.Input, args: [{ isSignal: true, alias: "animate", required: false }] }] } });
|
|
883
|
+
|
|
884
|
+
class MessageListComponent {
|
|
885
|
+
chatService;
|
|
886
|
+
debugMode = input(false, ...(ngDevMode ? [{ debugName: "debugMode" }] : []));
|
|
887
|
+
messagesContainer = viewChild('messagesContainer', ...(ngDevMode ? [{ debugName: "messagesContainer" }] : []));
|
|
888
|
+
shouldScrollToBottom = true;
|
|
889
|
+
isNearBottom = true;
|
|
890
|
+
animateNewParts = signal(false, ...(ngDevMode ? [{ debugName: "animateNewParts" }] : []));
|
|
891
|
+
constructor(chatService, destroyRef) {
|
|
892
|
+
this.chatService = chatService;
|
|
893
|
+
this.scrollToBottom();
|
|
894
|
+
effect(() => {
|
|
895
|
+
this.chatService.chat()?.messages;
|
|
896
|
+
if (!this.shouldScrollToBottom && !this.isNearBottom) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
queueMicrotask(() => {
|
|
900
|
+
this.scrollToBottom();
|
|
901
|
+
this.shouldScrollToBottom = false;
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
chatService.events$.pipe(takeUntilDestroyed(destroyRef)).subscribe((event) => {
|
|
905
|
+
if (event === 'onThreadSelected') {
|
|
906
|
+
this.animateNewParts.set(false);
|
|
907
|
+
this.shouldScrollToBottom = true;
|
|
908
|
+
queueMicrotask(() => this.animateNewParts.set(true));
|
|
909
|
+
}
|
|
910
|
+
if (event === 'onSend')
|
|
911
|
+
this.shouldScrollToBottom = true;
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
ngAfterViewInit() {
|
|
915
|
+
queueMicrotask(() => {
|
|
916
|
+
this.scrollToBottom();
|
|
917
|
+
this.shouldScrollToBottom = false;
|
|
918
|
+
this.animateNewParts.set(true);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
onScroll() {
|
|
922
|
+
const el = this.messagesContainer()?.nativeElement;
|
|
923
|
+
if (!el)
|
|
924
|
+
return;
|
|
925
|
+
this.isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 100;
|
|
926
|
+
}
|
|
927
|
+
scrollToBottom() {
|
|
928
|
+
const el = this.messagesContainer()?.nativeElement;
|
|
929
|
+
if (el) {
|
|
930
|
+
el.scrollTop = el.scrollHeight;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageListComponent, deps: [{ token: CortexChatService }, { token: i0.DestroyRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
934
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: MessageListComponent, isStandalone: true, selector: "cortex-message-list", inputs: { debugMode: { classPropertyName: "debugMode", publicName: "debugMode", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "messagesContainer", first: true, predicate: ["messagesContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"h-full overflow-y-auto w-full flex flex-col gap-2 p-4\"\n #messagesContainer\n (scroll)=\"onScroll()\"\n>\n @for (message of chatService.chat()?.messages; track message.id) {\n <cortex-message [message]=\"message\" [debugMode]=\"debugMode()\" [animate]=\"animateNewParts()\" />\n }\n</div>\n", dependencies: [{ kind: "component", type: MessageComponent, selector: "cortex-message", inputs: ["message", "debugMode", "animate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
935
|
+
}
|
|
936
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MessageListComponent, decorators: [{
|
|
937
|
+
type: Component,
|
|
938
|
+
args: [{ selector: 'cortex-message-list', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MessageComponent], template: "<div\n class=\"h-full overflow-y-auto w-full flex flex-col gap-2 p-4\"\n #messagesContainer\n (scroll)=\"onScroll()\"\n>\n @for (message of chatService.chat()?.messages; track message.id) {\n <cortex-message [message]=\"message\" [debugMode]=\"debugMode()\" [animate]=\"animateNewParts()\" />\n }\n</div>\n" }]
|
|
939
|
+
}], ctorParameters: () => [{ type: CortexChatService }, { type: i0.DestroyRef }], propDecorators: { debugMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "debugMode", required: false }] }], messagesContainer: [{ type: i0.ViewChild, args: ['messagesContainer', { isSignal: true }] }] } });
|
|
940
|
+
|
|
941
|
+
var translate_new$1 = "New";
|
|
942
|
+
var translate_threads$1 = "Threads";
|
|
943
|
+
var translate_n_conversations$1 = "{{count}} conversations";
|
|
944
|
+
var translate_untitled$1 = "Untitled";
|
|
945
|
+
var translate_new_chat$1 = "New Chat";
|
|
946
|
+
var translate_normal$1 = "Normal";
|
|
947
|
+
var translate_debug$1 = "Debug";
|
|
948
|
+
var translate_type_a_message$1 = "Type a message...";
|
|
949
|
+
var translate_thinking$1 = "Thinking";
|
|
950
|
+
var translate_reasoned$1 = "Reasoned";
|
|
951
|
+
var translate_knowledge_graph_query$1 = "Knowledge Graph Query";
|
|
952
|
+
var translate_javascript_code_execution$1 = "JavaScript Code Execution";
|
|
953
|
+
var translate_completed$1 = "Completed";
|
|
954
|
+
var translate_files_attached$1 = "{{count}} file(s) attached";
|
|
955
|
+
var translate_file_upload_skipped$1 = "File upload skipped";
|
|
956
|
+
var translate_cancel$1 = "Cancel";
|
|
957
|
+
var translate_uploading$1 = "Uploading…";
|
|
958
|
+
var translate_submit$1 = "Submit";
|
|
959
|
+
var translate_attach_requested_files$1 = "Attach requested files";
|
|
960
|
+
var translate_n_files_required$1 = "{{count}} file(s) required";
|
|
961
|
+
var translate_required$1 = "Required";
|
|
962
|
+
var translate_submit_files$1 = "Submit files";
|
|
963
|
+
var translate_choose_file$1 = "Choose file";
|
|
964
|
+
var translate_no_file_chosen$1 = "No file chosen";
|
|
965
|
+
var translate_tool_status_0$1 = "Fetching data…";
|
|
966
|
+
var translate_tool_status_1$1 = "Analyzing information…";
|
|
967
|
+
var translate_tool_status_2$1 = "Processing request…";
|
|
968
|
+
var translate_tool_status_3$1 = "Gathering results…";
|
|
969
|
+
var translate_tool_status_4$1 = "Preparing response…";
|
|
970
|
+
var translate_tool_status_5$1 = "Looking things up…";
|
|
971
|
+
var translate_tool_status_6$1 = "Checking records…";
|
|
972
|
+
var translate_tool_status_7$1 = "Reviewing details…";
|
|
973
|
+
var translate_tool_status_8$1 = "Running calculations…";
|
|
974
|
+
var translate_tool_status_9$1 = "Pulling information…";
|
|
975
|
+
var translate_tool_done_0$1 = "Data fetched successfully";
|
|
976
|
+
var translate_tool_done_1$1 = "Analysis completed successfully";
|
|
977
|
+
var translate_tool_done_2$1 = "Request processed successfully";
|
|
978
|
+
var translate_tool_done_3$1 = "Results gathered successfully";
|
|
979
|
+
var translate_tool_done_4$1 = "Response prepared successfully";
|
|
980
|
+
var translate_tool_done_5$1 = "Lookup completed successfully";
|
|
981
|
+
var translate_tool_done_6$1 = "Records checked successfully";
|
|
982
|
+
var translate_tool_done_7$1 = "Review completed successfully";
|
|
983
|
+
var translate_tool_done_8$1 = "Calculations completed successfully";
|
|
984
|
+
var translate_tool_done_9$1 = "Information retrieved successfully";
|
|
985
|
+
var translate_tool_error$1 = "Something went wrong";
|
|
986
|
+
var translate_tool_denied$1 = "Cancelled";
|
|
987
|
+
var en = {
|
|
988
|
+
translate_new: translate_new$1,
|
|
989
|
+
translate_threads: translate_threads$1,
|
|
990
|
+
translate_n_conversations: translate_n_conversations$1,
|
|
991
|
+
translate_untitled: translate_untitled$1,
|
|
992
|
+
translate_new_chat: translate_new_chat$1,
|
|
993
|
+
translate_normal: translate_normal$1,
|
|
994
|
+
translate_debug: translate_debug$1,
|
|
995
|
+
translate_type_a_message: translate_type_a_message$1,
|
|
996
|
+
translate_thinking: translate_thinking$1,
|
|
997
|
+
translate_reasoned: translate_reasoned$1,
|
|
998
|
+
translate_knowledge_graph_query: translate_knowledge_graph_query$1,
|
|
999
|
+
translate_javascript_code_execution: translate_javascript_code_execution$1,
|
|
1000
|
+
translate_completed: translate_completed$1,
|
|
1001
|
+
translate_files_attached: translate_files_attached$1,
|
|
1002
|
+
translate_file_upload_skipped: translate_file_upload_skipped$1,
|
|
1003
|
+
translate_cancel: translate_cancel$1,
|
|
1004
|
+
translate_uploading: translate_uploading$1,
|
|
1005
|
+
translate_submit: translate_submit$1,
|
|
1006
|
+
translate_attach_requested_files: translate_attach_requested_files$1,
|
|
1007
|
+
translate_n_files_required: translate_n_files_required$1,
|
|
1008
|
+
translate_required: translate_required$1,
|
|
1009
|
+
translate_submit_files: translate_submit_files$1,
|
|
1010
|
+
translate_choose_file: translate_choose_file$1,
|
|
1011
|
+
translate_no_file_chosen: translate_no_file_chosen$1,
|
|
1012
|
+
translate_tool_status_0: translate_tool_status_0$1,
|
|
1013
|
+
translate_tool_status_1: translate_tool_status_1$1,
|
|
1014
|
+
translate_tool_status_2: translate_tool_status_2$1,
|
|
1015
|
+
translate_tool_status_3: translate_tool_status_3$1,
|
|
1016
|
+
translate_tool_status_4: translate_tool_status_4$1,
|
|
1017
|
+
translate_tool_status_5: translate_tool_status_5$1,
|
|
1018
|
+
translate_tool_status_6: translate_tool_status_6$1,
|
|
1019
|
+
translate_tool_status_7: translate_tool_status_7$1,
|
|
1020
|
+
translate_tool_status_8: translate_tool_status_8$1,
|
|
1021
|
+
translate_tool_status_9: translate_tool_status_9$1,
|
|
1022
|
+
translate_tool_done_0: translate_tool_done_0$1,
|
|
1023
|
+
translate_tool_done_1: translate_tool_done_1$1,
|
|
1024
|
+
translate_tool_done_2: translate_tool_done_2$1,
|
|
1025
|
+
translate_tool_done_3: translate_tool_done_3$1,
|
|
1026
|
+
translate_tool_done_4: translate_tool_done_4$1,
|
|
1027
|
+
translate_tool_done_5: translate_tool_done_5$1,
|
|
1028
|
+
translate_tool_done_6: translate_tool_done_6$1,
|
|
1029
|
+
translate_tool_done_7: translate_tool_done_7$1,
|
|
1030
|
+
translate_tool_done_8: translate_tool_done_8$1,
|
|
1031
|
+
translate_tool_done_9: translate_tool_done_9$1,
|
|
1032
|
+
translate_tool_error: translate_tool_error$1,
|
|
1033
|
+
translate_tool_denied: translate_tool_denied$1
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
var translate_new = "جديد";
|
|
1037
|
+
var translate_threads = "المحادثات";
|
|
1038
|
+
var translate_n_conversations = "{{count}} محادثات";
|
|
1039
|
+
var translate_untitled = "بدون عنوان";
|
|
1040
|
+
var translate_new_chat = "محادثة جديدة";
|
|
1041
|
+
var translate_normal = "عادي";
|
|
1042
|
+
var translate_debug = "تصحيح";
|
|
1043
|
+
var translate_type_a_message = "اكتب رسالة...";
|
|
1044
|
+
var translate_thinking = "يفكر";
|
|
1045
|
+
var translate_reasoned = "تم الاستنتاج";
|
|
1046
|
+
var translate_knowledge_graph_query = "استعلام الرسم البياني المعرفي";
|
|
1047
|
+
var translate_javascript_code_execution = "تنفيذ كود جافاسكريبت";
|
|
1048
|
+
var translate_completed = "مكتمل";
|
|
1049
|
+
var translate_files_attached = "{{count}} ملف(ات) مرفقة";
|
|
1050
|
+
var translate_file_upload_skipped = "تم تخطي رفع الملف";
|
|
1051
|
+
var translate_cancel = "إلغاء";
|
|
1052
|
+
var translate_uploading = "جارٍ الرفع…";
|
|
1053
|
+
var translate_submit = "إرسال";
|
|
1054
|
+
var translate_attach_requested_files = "إرفاق الملفات المطلوبة";
|
|
1055
|
+
var translate_n_files_required = "{{count}} ملف(ات) مطلوبة";
|
|
1056
|
+
var translate_required = "مطلوب";
|
|
1057
|
+
var translate_submit_files = "إرسال الملفات";
|
|
1058
|
+
var translate_choose_file = "اختر ملف";
|
|
1059
|
+
var translate_no_file_chosen = "لم يتم اختيار ملف";
|
|
1060
|
+
var translate_tool_status_0 = "جارٍ جلب البيانات…";
|
|
1061
|
+
var translate_tool_status_1 = "جارٍ تحليل المعلومات…";
|
|
1062
|
+
var translate_tool_status_2 = "جارٍ معالجة الطلب…";
|
|
1063
|
+
var translate_tool_status_3 = "جارٍ تجميع النتائج…";
|
|
1064
|
+
var translate_tool_status_4 = "جارٍ تحضير الرد…";
|
|
1065
|
+
var translate_tool_status_5 = "جارٍ البحث…";
|
|
1066
|
+
var translate_tool_status_6 = "جارٍ فحص السجلات…";
|
|
1067
|
+
var translate_tool_status_7 = "جارٍ مراجعة التفاصيل…";
|
|
1068
|
+
var translate_tool_status_8 = "جارٍ إجراء الحسابات…";
|
|
1069
|
+
var translate_tool_status_9 = "جارٍ سحب المعلومات…";
|
|
1070
|
+
var translate_tool_done_0 = "تم جلب البيانات بنجاح";
|
|
1071
|
+
var translate_tool_done_1 = "تم التحليل بنجاح";
|
|
1072
|
+
var translate_tool_done_2 = "تمت معالجة الطلب بنجاح";
|
|
1073
|
+
var translate_tool_done_3 = "تم تجميع النتائج بنجاح";
|
|
1074
|
+
var translate_tool_done_4 = "تم تحضير الرد بنجاح";
|
|
1075
|
+
var translate_tool_done_5 = "تم البحث بنجاح";
|
|
1076
|
+
var translate_tool_done_6 = "تم فحص السجلات بنجاح";
|
|
1077
|
+
var translate_tool_done_7 = "تمت المراجعة بنجاح";
|
|
1078
|
+
var translate_tool_done_8 = "تمت الحسابات بنجاح";
|
|
1079
|
+
var translate_tool_done_9 = "تم استرجاع المعلومات بنجاح";
|
|
1080
|
+
var translate_tool_error = "حدث خطأ";
|
|
1081
|
+
var translate_tool_denied = "تم الإلغاء";
|
|
1082
|
+
var ar = {
|
|
1083
|
+
translate_new: translate_new,
|
|
1084
|
+
translate_threads: translate_threads,
|
|
1085
|
+
translate_n_conversations: translate_n_conversations,
|
|
1086
|
+
translate_untitled: translate_untitled,
|
|
1087
|
+
translate_new_chat: translate_new_chat,
|
|
1088
|
+
translate_normal: translate_normal,
|
|
1089
|
+
translate_debug: translate_debug,
|
|
1090
|
+
translate_type_a_message: translate_type_a_message,
|
|
1091
|
+
translate_thinking: translate_thinking,
|
|
1092
|
+
translate_reasoned: translate_reasoned,
|
|
1093
|
+
translate_knowledge_graph_query: translate_knowledge_graph_query,
|
|
1094
|
+
translate_javascript_code_execution: translate_javascript_code_execution,
|
|
1095
|
+
translate_completed: translate_completed,
|
|
1096
|
+
translate_files_attached: translate_files_attached,
|
|
1097
|
+
translate_file_upload_skipped: translate_file_upload_skipped,
|
|
1098
|
+
translate_cancel: translate_cancel,
|
|
1099
|
+
translate_uploading: translate_uploading,
|
|
1100
|
+
translate_submit: translate_submit,
|
|
1101
|
+
translate_attach_requested_files: translate_attach_requested_files,
|
|
1102
|
+
translate_n_files_required: translate_n_files_required,
|
|
1103
|
+
translate_required: translate_required,
|
|
1104
|
+
translate_submit_files: translate_submit_files,
|
|
1105
|
+
translate_choose_file: translate_choose_file,
|
|
1106
|
+
translate_no_file_chosen: translate_no_file_chosen,
|
|
1107
|
+
translate_tool_status_0: translate_tool_status_0,
|
|
1108
|
+
translate_tool_status_1: translate_tool_status_1,
|
|
1109
|
+
translate_tool_status_2: translate_tool_status_2,
|
|
1110
|
+
translate_tool_status_3: translate_tool_status_3,
|
|
1111
|
+
translate_tool_status_4: translate_tool_status_4,
|
|
1112
|
+
translate_tool_status_5: translate_tool_status_5,
|
|
1113
|
+
translate_tool_status_6: translate_tool_status_6,
|
|
1114
|
+
translate_tool_status_7: translate_tool_status_7,
|
|
1115
|
+
translate_tool_status_8: translate_tool_status_8,
|
|
1116
|
+
translate_tool_status_9: translate_tool_status_9,
|
|
1117
|
+
translate_tool_done_0: translate_tool_done_0,
|
|
1118
|
+
translate_tool_done_1: translate_tool_done_1,
|
|
1119
|
+
translate_tool_done_2: translate_tool_done_2,
|
|
1120
|
+
translate_tool_done_3: translate_tool_done_3,
|
|
1121
|
+
translate_tool_done_4: translate_tool_done_4,
|
|
1122
|
+
translate_tool_done_5: translate_tool_done_5,
|
|
1123
|
+
translate_tool_done_6: translate_tool_done_6,
|
|
1124
|
+
translate_tool_done_7: translate_tool_done_7,
|
|
1125
|
+
translate_tool_done_8: translate_tool_done_8,
|
|
1126
|
+
translate_tool_done_9: translate_tool_done_9,
|
|
1127
|
+
translate_tool_error: translate_tool_error,
|
|
1128
|
+
translate_tool_denied: translate_tool_denied
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
const translations = { en, ar };
|
|
1132
|
+
class CortexClientTranslateLoader {
|
|
1133
|
+
getTranslation(lang) {
|
|
1134
|
+
return of(translations[lang] ?? translations['en']);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
class CortexClientConfigRef {
|
|
1139
|
+
config;
|
|
1140
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CortexClientConfigRef, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1141
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CortexClientConfigRef });
|
|
1142
|
+
}
|
|
1143
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CortexClientConfigRef, decorators: [{
|
|
1144
|
+
type: Injectable
|
|
1145
|
+
}] });
|
|
1146
|
+
class CortexChatWidgetComponent {
|
|
1147
|
+
config = input.required(...(ngDevMode ? [{ debugName: "config" }] : []));
|
|
1148
|
+
text = model('', ...(ngDevMode ? [{ debugName: "text" }] : []));
|
|
1149
|
+
debugMode = signal(false, ...(ngDevMode ? [{ debugName: "debugMode" }] : []));
|
|
1150
|
+
screen = signal('threads', ...(ngDevMode ? [{ debugName: "screen" }] : []));
|
|
1151
|
+
theme = signal(undefined, ...(ngDevMode ? [{ debugName: "theme" }] : []));
|
|
1152
|
+
chatService;
|
|
1153
|
+
configRef = inject(CortexClientConfigRef);
|
|
1154
|
+
injector = inject(Injector);
|
|
1155
|
+
ngOnInit() {
|
|
1156
|
+
this.configRef.config = this.config();
|
|
1157
|
+
this.theme.set(this.config().theme);
|
|
1158
|
+
this.chatService = this.injector.get(CortexChatService);
|
|
1159
|
+
const translateService = this.injector.get(TranslateService);
|
|
1160
|
+
translateService.use(this.config().locale());
|
|
1161
|
+
effect(() => {
|
|
1162
|
+
translateService.use(this.config().locale());
|
|
1163
|
+
}, { injector: this.injector });
|
|
1164
|
+
}
|
|
1165
|
+
toggleDebugMode() {
|
|
1166
|
+
this.debugMode.update((v) => !v);
|
|
1167
|
+
}
|
|
1168
|
+
selectThread(thread) {
|
|
1169
|
+
this.chatService.selectThread(thread).subscribe();
|
|
1170
|
+
this.screen.set('chat');
|
|
1171
|
+
}
|
|
1172
|
+
goBack() {
|
|
1173
|
+
this.chatService.deselectThread();
|
|
1174
|
+
this.screen.set('threads');
|
|
1175
|
+
}
|
|
1176
|
+
onKeydown(event) {
|
|
1177
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
1178
|
+
event.preventDefault();
|
|
1179
|
+
this.send();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
send() {
|
|
1183
|
+
if (this.chatService.isAgentWorking())
|
|
1184
|
+
return;
|
|
1185
|
+
const text = this.text().trim();
|
|
1186
|
+
if (!text)
|
|
1187
|
+
return;
|
|
1188
|
+
this.text.set('');
|
|
1189
|
+
this.chatService.send(text).subscribe();
|
|
1190
|
+
}
|
|
1191
|
+
newChat() {
|
|
1192
|
+
this.chatService.deselectThread();
|
|
1193
|
+
this.screen.set('chat');
|
|
1194
|
+
}
|
|
1195
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CortexChatWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1196
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: CortexChatWidgetComponent, isStandalone: true, selector: "cortex-chat-widget", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { text: "textChange" }, host: { properties: { "style.--cortex-primary-color": "theme()?.primaryColor", "style.--cortex-bg-color": "theme()?.backgroundColor", "style.--cortex-surface-color": "theme()?.surfaceColor", "style.--cortex-text-color": "theme()?.textColor", "style.--cortex-border-radius": "theme()?.borderRadius" }, classAttribute: "block" }, providers: [
|
|
1197
|
+
CortexClientConfigRef,
|
|
1198
|
+
{
|
|
1199
|
+
provide: CORTEX_CLIENT_CONFIG,
|
|
1200
|
+
useFactory: () => inject(CortexClientConfigRef).config,
|
|
1201
|
+
},
|
|
1202
|
+
CortexChatService,
|
|
1203
|
+
CortexClientWebSocketService,
|
|
1204
|
+
...provideTranslateService({
|
|
1205
|
+
fallbackLang: 'en',
|
|
1206
|
+
loader: provideTranslateLoader(CortexClientTranslateLoader),
|
|
1207
|
+
}),
|
|
1208
|
+
], ngImport: i0, template: "<div class=\"w-full h-full relative overflow-hidden bg-slate-5\">\n <!-- \u2550\u2550\u2550 SCREEN 1: THREADS \u2550\u2550\u2550 -->\n <div\n class=\"absolute inset-0 flex flex-col transition-transform duration-300 ease-out\"\n [ngClass]=\"\n screen() === 'threads' ? 'translate-x-0' : 'ltr:-translate-x-full rtl:translate-x-full'\n \"\n >\n <!-- Threads header -->\n <div class=\"shrink-0 px-5 pt-5 pb-4 flex items-center justify-between\">\n <div>\n <h2 class=\"text-[15px] font-semibold text-slate-800 tracking-tight\">\n {{ 'translate_threads' | translate }}\n </h2>\n <p class=\"text-[11px] text-slate-400 mt-0.5\">\n {{\n 'translate_n_conversations' | translate: { count: chatService.threads()?.length ?? 0 }\n }}\n </p>\n </div>\n <button\n (click)=\"newChat()\"\n class=\"inline-flex items-center gap-1.5 text-[12px] font-medium text-slate-100 bg-slate-800 px-3 py-1.5 rounded-md hover:bg-slate-700 active:bg-slate-900 transition-colors cursor-pointer\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path\n d=\"M8 3v10M3 8h10\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n {{ 'translate_new' | translate }}\n </button>\n </div>\n\n <!-- Threads list -->\n <div class=\"flex-1 overflow-y-auto px-3 pb-3\">\n <!-- Threads loading skeleton -->\n @if (chatService.threads() === undefined) {\n @for (i of [1, 2, 3, 4]; track i) {\n <div class=\"flex items-center gap-3 px-3 py-3 mb-0.5\">\n <div class=\"skeleton shrink-0 size-8\"></div>\n <div class=\"flex-1 flex flex-col gap-1.5\">\n <div class=\"skeleton h-3.5\" [style.width]=\"40 + i * 12 + '%'\"></div>\n </div>\n </div>\n }\n } @else {\n @for (thread of chatService.threads(); track thread.id) {\n <button\n (click)=\"selectThread(thread)\"\n class=\"group w-full text-start px-3 py-3 rounded-lg flex items-center gap-3 transition-colors cursor-pointer mb-0.5\"\n [ngClass]=\"{\n 'bg-slate-200 text-slate-100': thread.id === chatService.selectedThread()?.id,\n 'text-slate-600 hover:bg-slate-100 active:bg-slate-150':\n thread.id !== chatService.selectedThread()?.id,\n }\"\n >\n <!-- Thread icon -->\n <div\n class=\"shrink-0 size-8 rounded-md flex items-center justify-center text-[11px] font-semibold rtl:transform rtl:rotate-180\"\n [ngClass]=\"{\n 'bg-slate-700 text-slate-300': thread.id === chatService.selectedThread()?.id,\n 'bg-slate-200 text-slate-500': thread.id !== chatService.selectedThread()?.id,\n }\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path\n d=\"M2.5 4.5h11M2.5 8h7M2.5 11.5h9\"\n stroke=\"currentColor\"\n stroke-width=\"1.3\"\n stroke-linecap=\"round\"\n />\n </svg>\n </div>\n\n <!-- Thread info -->\n <div class=\"min-w-0 flex-1\">\n <p\n class=\"text-[13px] font-medium truncate leading-tight\"\n [ngClass]=\"{\n 'text-slate-100': thread.id === chatService.selectedThread()?.id,\n 'text-slate-700': thread.id !== chatService.selectedThread()?.id,\n }\"\n >\n {{ thread.title ?? ('translate_untitled' | translate) }}\n </p>\n </div>\n\n <!-- Delete button -->\n <button\n (click)=\"$event.stopPropagation(); chatService.deleteThread(thread.id).subscribe()\"\n class=\"shrink-0 opacity-0 group-hover:opacity-100 transition-opacity size-7 rounded-md flex items-center justify-center cursor-pointer\"\n [ngClass]=\"{\n 'hover:bg-slate-600 text-slate-400': thread.id === chatService.selectedThread()?.id,\n 'hover:bg-slate-200 text-slate-400': thread.id !== chatService.selectedThread()?.id,\n }\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n stroke=\"currentColor\"\n stroke-width=\"1.3\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n\n <!-- Arrow -->\n <svg\n class=\"shrink-0 transition-colors rtl:transform rtl:rotate-180\"\n [ngClass]=\"{\n 'text-slate-500': thread.id === chatService.selectedThread()?.id,\n 'text-slate-300 group-hover:text-slate-400':\n thread.id !== chatService.selectedThread()?.id,\n }\"\n width=\"10\"\n height=\"10\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n >\n <path\n d=\"M6 4l4 4-4 4\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n }\n\n @if (!chatService.threads()?.length) {\n <div class=\"flex flex-col items-center justify-center py-16 text-center\">\n <div class=\"size-10 rounded-lg bg-slate-200 flex items-center justify-center mb-3\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 16 16\" fill=\"none\" class=\"text-slate-400\">\n <path\n d=\"M2.5 4.5h11M2.5 8h7M2.5 11.5h9\"\n stroke=\"currentColor\"\n stroke-width=\"1.3\"\n stroke-linecap=\"round\"\n />\n </svg>\n </div>\n <p class=\"text-[13px] text-slate-400 font-medium\">No threads yet</p>\n <p class=\"text-[11px] text-slate-350 mt-1\">Start a new conversation</p>\n </div>\n }\n }\n </div>\n </div>\n\n <!-- \u2550\u2550\u2550 SCREEN 2: CHAT \u2550\u2550\u2550 -->\n <div\n class=\"absolute inset-0 flex flex-col transition-transform duration-300 ease-out\"\n [ngClass]=\"screen() === 'chat' ? 'translate-x-0' : 'ltr:translate-x-full rtl:-translate-x-full'\"\n >\n <!-- Chat header -->\n <div class=\"shrink-0 h-12 px-4 flex items-center gap-3 border-b border-slate-200 bg-white\">\n <!-- Back button -->\n <button\n (click)=\"goBack()\"\n class=\"shrink-0 size-8 rounded-md flex items-center justify-center text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors cursor-pointer\"\n >\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"rtl:transform rtl:rotate-180\"\n >\n <path\n d=\"M10 3L5 8l5 5\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n\n <!-- Title -->\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-[13px] font-medium text-slate-700 truncate\">\n {{ chatService.selectedThread()?.title ?? ('translate_new_chat' | translate) }}\n </p>\n </div>\n\n <!-- Debug toggle -->\n <button\n (click)=\"toggleDebugMode()\"\n class=\"shrink-0 text-[11px] font-medium px-2.5 py-1 rounded-md transition-colors cursor-pointer\"\n [ngClass]=\"{\n 'bg-amber-50 text-amber-600 hover:bg-amber-100': debugMode(),\n 'bg-slate-100 text-slate-400 hover:bg-slate-200': !debugMode(),\n }\"\n >\n {{ debugMode() ? ('translate_debug' | translate) : ('translate_normal' | translate) }}\n </button>\n </div>\n\n <!-- Messages loading skeleton -->\n @if (chatService.isLoadingMessages() && !chatService.isAgentWorking()) {\n <div class=\"flex-1 overflow-hidden p-4 flex flex-col gap-2\">\n <!-- User message skeleton -->\n <div class=\"w-full flex flex-col items-start\">\n <div\n class=\"max-w-4/5 p-4 rounded-2xl rounded-bl-none border border-slate-200 bg-slate-50 flex flex-col gap-2\"\n >\n <div class=\"skeleton h-3 w-52\"></div>\n <div class=\"skeleton h-3 w-36\"></div>\n </div>\n </div>\n <!-- Assistant message skeleton -->\n <div class=\"w-full flex flex-col items-end\">\n <div\n class=\"max-w-4/5 p-4 rounded-2xl rounded-br-none border border-slate-200 bg-slate-100 flex flex-col gap-2\"\n >\n <div class=\"skeleton h-3 w-64\"></div>\n <div class=\"skeleton h-3 w-72\"></div>\n <div class=\"skeleton h-3 w-48\"></div>\n </div>\n </div>\n <!-- User message skeleton -->\n <div class=\"w-full flex flex-col items-start\">\n <div\n class=\"max-w-4/5 p-4 rounded-2xl rounded-bl-none border border-slate-200 bg-slate-50 flex flex-col gap-2\"\n >\n <div class=\"skeleton h-3 w-44\"></div>\n </div>\n </div>\n <!-- Assistant message skeleton -->\n <div class=\"w-full flex flex-col items-end\">\n <div\n class=\"max-w-4/5 p-4 rounded-2xl rounded-br-none border border-slate-200 bg-slate-100 flex flex-col gap-2\"\n >\n <div class=\"skeleton h-3 w-56\"></div>\n <div class=\"skeleton h-3 w-60\"></div>\n </div>\n </div>\n </div>\n } @else {\n <!-- Messages area -->\n <cortex-message-list class=\"flex-1 overflow-hidden\" [debugMode]=\"debugMode()\" />\n }\n\n <!-- Agent working indicator -->\n @if (chatService.isAgentWorking() && !chatService.hasPendingToolCalls()) {\n <div class=\"shrink-0 px-4 py-1.5 flex items-center gap-1.5\">\n <div class=\"flex items-center gap-[3px]\">\n <span class=\"cortex-working-dot block size-[5px] rounded-full bg-slate-400\"></span>\n <span class=\"cortex-working-dot block size-[5px] rounded-full bg-slate-400\"></span>\n <span class=\"cortex-working-dot block size-[5px] rounded-full bg-slate-400\"></span>\n </div>\n <span class=\"text-[11px] text-slate-400 font-medium\">{{\n 'translate_thinking' | translate\n }}</span>\n </div>\n }\n\n <!-- Chat input (hidden during pending tool calls) -->\n @if (!chatService.hasPendingToolCalls()) {\n <div class=\"shrink-0 p-3 bg-white border-t border-slate-200\">\n <div\n class=\"flex items-end gap-2 rounded-xl border border-slate-200 p-1.5 transition-colors\"\n [ngClass]=\"{\n 'bg-slate-100': chatService.isAgentWorking(),\n 'bg-slate-50 focus-within:border-slate-300 focus-within:bg-white':\n !chatService.isAgentWorking(),\n }\"\n >\n <textarea\n (keydown)=\"onKeydown($event)\"\n [(ngModel)]=\"text\"\n [placeholder]=\"\n chatService.isAgentWorking() ? '' : ('translate_type_a_message' | translate)\n \"\n [disabled]=\"chatService.isAgentWorking()\"\n rows=\"1\"\n class=\"flex-1 bg-transparent resize-none border-0 outline-none text-[13px] placeholder:text-slate-350 py-1.5 px-2 min-h-[28px] max-h-[120px] leading-snug disabled:cursor-not-allowed disabled:border-0\"\n [ngClass]=\"{\n 'text-slate-400': chatService.isAgentWorking(),\n 'text-slate-700': !chatService.isAgentWorking(),\n }\"\n ></textarea>\n @if (chatService.isAgentWorking()) {\n <!-- Stop button -->\n <button\n (click)=\"chatService.abort()\"\n class=\"stop-btn shrink-0 size-8 rounded-lg flex items-center justify-center cursor-pointer relative\"\n >\n <!-- Pulsing ring -->\n <span class=\"stop-btn-ring absolute inset-0 rounded-lg\"></span>\n <!-- Stop icon (rounded square) -->\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" class=\"relative z-10\">\n <rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" rx=\"2.5\" fill=\"currentColor\" />\n </svg>\n </button>\n } @else {\n <!-- Send button -->\n <button\n (click)=\"send()\"\n class=\"shrink-0 size-8 rounded-lg flex items-center justify-center transition-colors\"\n [ngClass]=\"{\n 'bg-slate-200 text-slate-400': !text().trim(),\n 'bg-slate-800 text-white hover:bg-slate-700 cursor-pointer': text().trim(),\n }\"\n >\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"rtl:transform rtl:rotate-180\"\n >\n <path\n d=\"M3 8h10M9 4l4 4-4 4\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";@keyframes skeleton-shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}.skeleton{background:linear-gradient(90deg,#e2e8f0,#f1f5f9,#e2e8f0 80%);background-size:200% 100%;animation:skeleton-shimmer 1.8s ease-in-out infinite;border-radius:6px}@keyframes cortex-pulse{0%,to{opacity:.4}50%{opacity:1}}@keyframes cortex-dot-bounce{0%,80%,to{transform:translateY(0)}40%{transform:translateY(-3px)}}.cortex-working-dot{animation:cortex-pulse 1.6s ease-in-out infinite,cortex-dot-bounce 1.2s ease-in-out infinite}.cortex-working-dot:nth-child(2){animation-delay:.15s}.cortex-working-dot:nth-child(3){animation-delay:.3s}@keyframes stop-pulse-ring{0%{opacity:.45;transform:scale(1)}50%{opacity:.15;transform:scale(1.12)}to{opacity:.45;transform:scale(1)}}@keyframes stop-fade-in{0%{opacity:0;transform:scale(.7)}to{opacity:1;transform:scale(1)}}.stop-btn{background:#e11d48;color:#fff;animation:stop-fade-in .2s ease-out both;transition:background .15s ease}.stop-btn:hover{background:#be123c}.stop-btn:active{transform:scale(.92)}.stop-btn-ring{background:#e11d48;animation:stop-pulse-ring 2s ease-in-out infinite;pointer-events:none}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MessageListComponent, selector: "cortex-message-list", inputs: ["debugMode"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1209
|
+
}
|
|
1210
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CortexChatWidgetComponent, decorators: [{
|
|
1211
|
+
type: Component,
|
|
1212
|
+
args: [{ selector: 'cortex-chat-widget', imports: [NgClass, MessageListComponent, FormsModule, TranslatePipe], providers: [
|
|
1213
|
+
CortexClientConfigRef,
|
|
1214
|
+
{
|
|
1215
|
+
provide: CORTEX_CLIENT_CONFIG,
|
|
1216
|
+
useFactory: () => inject(CortexClientConfigRef).config,
|
|
1217
|
+
},
|
|
1218
|
+
CortexChatService,
|
|
1219
|
+
CortexClientWebSocketService,
|
|
1220
|
+
...provideTranslateService({
|
|
1221
|
+
fallbackLang: 'en',
|
|
1222
|
+
loader: provideTranslateLoader(CortexClientTranslateLoader),
|
|
1223
|
+
}),
|
|
1224
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1225
|
+
class: 'block',
|
|
1226
|
+
'[style.--cortex-primary-color]': 'theme()?.primaryColor',
|
|
1227
|
+
'[style.--cortex-bg-color]': 'theme()?.backgroundColor',
|
|
1228
|
+
'[style.--cortex-surface-color]': 'theme()?.surfaceColor',
|
|
1229
|
+
'[style.--cortex-text-color]': 'theme()?.textColor',
|
|
1230
|
+
'[style.--cortex-border-radius]': 'theme()?.borderRadius',
|
|
1231
|
+
}, template: "<div class=\"w-full h-full relative overflow-hidden bg-slate-5\">\n <!-- \u2550\u2550\u2550 SCREEN 1: THREADS \u2550\u2550\u2550 -->\n <div\n class=\"absolute inset-0 flex flex-col transition-transform duration-300 ease-out\"\n [ngClass]=\"\n screen() === 'threads' ? 'translate-x-0' : 'ltr:-translate-x-full rtl:translate-x-full'\n \"\n >\n <!-- Threads header -->\n <div class=\"shrink-0 px-5 pt-5 pb-4 flex items-center justify-between\">\n <div>\n <h2 class=\"text-[15px] font-semibold text-slate-800 tracking-tight\">\n {{ 'translate_threads' | translate }}\n </h2>\n <p class=\"text-[11px] text-slate-400 mt-0.5\">\n {{\n 'translate_n_conversations' | translate: { count: chatService.threads()?.length ?? 0 }\n }}\n </p>\n </div>\n <button\n (click)=\"newChat()\"\n class=\"inline-flex items-center gap-1.5 text-[12px] font-medium text-slate-100 bg-slate-800 px-3 py-1.5 rounded-md hover:bg-slate-700 active:bg-slate-900 transition-colors cursor-pointer\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path\n d=\"M8 3v10M3 8h10\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n {{ 'translate_new' | translate }}\n </button>\n </div>\n\n <!-- Threads list -->\n <div class=\"flex-1 overflow-y-auto px-3 pb-3\">\n <!-- Threads loading skeleton -->\n @if (chatService.threads() === undefined) {\n @for (i of [1, 2, 3, 4]; track i) {\n <div class=\"flex items-center gap-3 px-3 py-3 mb-0.5\">\n <div class=\"skeleton shrink-0 size-8\"></div>\n <div class=\"flex-1 flex flex-col gap-1.5\">\n <div class=\"skeleton h-3.5\" [style.width]=\"40 + i * 12 + '%'\"></div>\n </div>\n </div>\n }\n } @else {\n @for (thread of chatService.threads(); track thread.id) {\n <button\n (click)=\"selectThread(thread)\"\n class=\"group w-full text-start px-3 py-3 rounded-lg flex items-center gap-3 transition-colors cursor-pointer mb-0.5\"\n [ngClass]=\"{\n 'bg-slate-200 text-slate-100': thread.id === chatService.selectedThread()?.id,\n 'text-slate-600 hover:bg-slate-100 active:bg-slate-150':\n thread.id !== chatService.selectedThread()?.id,\n }\"\n >\n <!-- Thread icon -->\n <div\n class=\"shrink-0 size-8 rounded-md flex items-center justify-center text-[11px] font-semibold rtl:transform rtl:rotate-180\"\n [ngClass]=\"{\n 'bg-slate-700 text-slate-300': thread.id === chatService.selectedThread()?.id,\n 'bg-slate-200 text-slate-500': thread.id !== chatService.selectedThread()?.id,\n }\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path\n d=\"M2.5 4.5h11M2.5 8h7M2.5 11.5h9\"\n stroke=\"currentColor\"\n stroke-width=\"1.3\"\n stroke-linecap=\"round\"\n />\n </svg>\n </div>\n\n <!-- Thread info -->\n <div class=\"min-w-0 flex-1\">\n <p\n class=\"text-[13px] font-medium truncate leading-tight\"\n [ngClass]=\"{\n 'text-slate-100': thread.id === chatService.selectedThread()?.id,\n 'text-slate-700': thread.id !== chatService.selectedThread()?.id,\n }\"\n >\n {{ thread.title ?? ('translate_untitled' | translate) }}\n </p>\n </div>\n\n <!-- Delete button -->\n <button\n (click)=\"$event.stopPropagation(); chatService.deleteThread(thread.id).subscribe()\"\n class=\"shrink-0 opacity-0 group-hover:opacity-100 transition-opacity size-7 rounded-md flex items-center justify-center cursor-pointer\"\n [ngClass]=\"{\n 'hover:bg-slate-600 text-slate-400': thread.id === chatService.selectedThread()?.id,\n 'hover:bg-slate-200 text-slate-400': thread.id !== chatService.selectedThread()?.id,\n }\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n stroke=\"currentColor\"\n stroke-width=\"1.3\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n\n <!-- Arrow -->\n <svg\n class=\"shrink-0 transition-colors rtl:transform rtl:rotate-180\"\n [ngClass]=\"{\n 'text-slate-500': thread.id === chatService.selectedThread()?.id,\n 'text-slate-300 group-hover:text-slate-400':\n thread.id !== chatService.selectedThread()?.id,\n }\"\n width=\"10\"\n height=\"10\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n >\n <path\n d=\"M6 4l4 4-4 4\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n }\n\n @if (!chatService.threads()?.length) {\n <div class=\"flex flex-col items-center justify-center py-16 text-center\">\n <div class=\"size-10 rounded-lg bg-slate-200 flex items-center justify-center mb-3\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 16 16\" fill=\"none\" class=\"text-slate-400\">\n <path\n d=\"M2.5 4.5h11M2.5 8h7M2.5 11.5h9\"\n stroke=\"currentColor\"\n stroke-width=\"1.3\"\n stroke-linecap=\"round\"\n />\n </svg>\n </div>\n <p class=\"text-[13px] text-slate-400 font-medium\">No threads yet</p>\n <p class=\"text-[11px] text-slate-350 mt-1\">Start a new conversation</p>\n </div>\n }\n }\n </div>\n </div>\n\n <!-- \u2550\u2550\u2550 SCREEN 2: CHAT \u2550\u2550\u2550 -->\n <div\n class=\"absolute inset-0 flex flex-col transition-transform duration-300 ease-out\"\n [ngClass]=\"screen() === 'chat' ? 'translate-x-0' : 'ltr:translate-x-full rtl:-translate-x-full'\"\n >\n <!-- Chat header -->\n <div class=\"shrink-0 h-12 px-4 flex items-center gap-3 border-b border-slate-200 bg-white\">\n <!-- Back button -->\n <button\n (click)=\"goBack()\"\n class=\"shrink-0 size-8 rounded-md flex items-center justify-center text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors cursor-pointer\"\n >\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"rtl:transform rtl:rotate-180\"\n >\n <path\n d=\"M10 3L5 8l5 5\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n\n <!-- Title -->\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-[13px] font-medium text-slate-700 truncate\">\n {{ chatService.selectedThread()?.title ?? ('translate_new_chat' | translate) }}\n </p>\n </div>\n\n <!-- Debug toggle -->\n <button\n (click)=\"toggleDebugMode()\"\n class=\"shrink-0 text-[11px] font-medium px-2.5 py-1 rounded-md transition-colors cursor-pointer\"\n [ngClass]=\"{\n 'bg-amber-50 text-amber-600 hover:bg-amber-100': debugMode(),\n 'bg-slate-100 text-slate-400 hover:bg-slate-200': !debugMode(),\n }\"\n >\n {{ debugMode() ? ('translate_debug' | translate) : ('translate_normal' | translate) }}\n </button>\n </div>\n\n <!-- Messages loading skeleton -->\n @if (chatService.isLoadingMessages() && !chatService.isAgentWorking()) {\n <div class=\"flex-1 overflow-hidden p-4 flex flex-col gap-2\">\n <!-- User message skeleton -->\n <div class=\"w-full flex flex-col items-start\">\n <div\n class=\"max-w-4/5 p-4 rounded-2xl rounded-bl-none border border-slate-200 bg-slate-50 flex flex-col gap-2\"\n >\n <div class=\"skeleton h-3 w-52\"></div>\n <div class=\"skeleton h-3 w-36\"></div>\n </div>\n </div>\n <!-- Assistant message skeleton -->\n <div class=\"w-full flex flex-col items-end\">\n <div\n class=\"max-w-4/5 p-4 rounded-2xl rounded-br-none border border-slate-200 bg-slate-100 flex flex-col gap-2\"\n >\n <div class=\"skeleton h-3 w-64\"></div>\n <div class=\"skeleton h-3 w-72\"></div>\n <div class=\"skeleton h-3 w-48\"></div>\n </div>\n </div>\n <!-- User message skeleton -->\n <div class=\"w-full flex flex-col items-start\">\n <div\n class=\"max-w-4/5 p-4 rounded-2xl rounded-bl-none border border-slate-200 bg-slate-50 flex flex-col gap-2\"\n >\n <div class=\"skeleton h-3 w-44\"></div>\n </div>\n </div>\n <!-- Assistant message skeleton -->\n <div class=\"w-full flex flex-col items-end\">\n <div\n class=\"max-w-4/5 p-4 rounded-2xl rounded-br-none border border-slate-200 bg-slate-100 flex flex-col gap-2\"\n >\n <div class=\"skeleton h-3 w-56\"></div>\n <div class=\"skeleton h-3 w-60\"></div>\n </div>\n </div>\n </div>\n } @else {\n <!-- Messages area -->\n <cortex-message-list class=\"flex-1 overflow-hidden\" [debugMode]=\"debugMode()\" />\n }\n\n <!-- Agent working indicator -->\n @if (chatService.isAgentWorking() && !chatService.hasPendingToolCalls()) {\n <div class=\"shrink-0 px-4 py-1.5 flex items-center gap-1.5\">\n <div class=\"flex items-center gap-[3px]\">\n <span class=\"cortex-working-dot block size-[5px] rounded-full bg-slate-400\"></span>\n <span class=\"cortex-working-dot block size-[5px] rounded-full bg-slate-400\"></span>\n <span class=\"cortex-working-dot block size-[5px] rounded-full bg-slate-400\"></span>\n </div>\n <span class=\"text-[11px] text-slate-400 font-medium\">{{\n 'translate_thinking' | translate\n }}</span>\n </div>\n }\n\n <!-- Chat input (hidden during pending tool calls) -->\n @if (!chatService.hasPendingToolCalls()) {\n <div class=\"shrink-0 p-3 bg-white border-t border-slate-200\">\n <div\n class=\"flex items-end gap-2 rounded-xl border border-slate-200 p-1.5 transition-colors\"\n [ngClass]=\"{\n 'bg-slate-100': chatService.isAgentWorking(),\n 'bg-slate-50 focus-within:border-slate-300 focus-within:bg-white':\n !chatService.isAgentWorking(),\n }\"\n >\n <textarea\n (keydown)=\"onKeydown($event)\"\n [(ngModel)]=\"text\"\n [placeholder]=\"\n chatService.isAgentWorking() ? '' : ('translate_type_a_message' | translate)\n \"\n [disabled]=\"chatService.isAgentWorking()\"\n rows=\"1\"\n class=\"flex-1 bg-transparent resize-none border-0 outline-none text-[13px] placeholder:text-slate-350 py-1.5 px-2 min-h-[28px] max-h-[120px] leading-snug disabled:cursor-not-allowed disabled:border-0\"\n [ngClass]=\"{\n 'text-slate-400': chatService.isAgentWorking(),\n 'text-slate-700': !chatService.isAgentWorking(),\n }\"\n ></textarea>\n @if (chatService.isAgentWorking()) {\n <!-- Stop button -->\n <button\n (click)=\"chatService.abort()\"\n class=\"stop-btn shrink-0 size-8 rounded-lg flex items-center justify-center cursor-pointer relative\"\n >\n <!-- Pulsing ring -->\n <span class=\"stop-btn-ring absolute inset-0 rounded-lg\"></span>\n <!-- Stop icon (rounded square) -->\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" class=\"relative z-10\">\n <rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" rx=\"2.5\" fill=\"currentColor\" />\n </svg>\n </button>\n } @else {\n <!-- Send button -->\n <button\n (click)=\"send()\"\n class=\"shrink-0 size-8 rounded-lg flex items-center justify-center transition-colors\"\n [ngClass]=\"{\n 'bg-slate-200 text-slate-400': !text().trim(),\n 'bg-slate-800 text-white hover:bg-slate-700 cursor-pointer': text().trim(),\n }\"\n >\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"rtl:transform rtl:rotate-180\"\n >\n <path\n d=\"M3 8h10M9 4l4 4-4 4\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";@keyframes skeleton-shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}.skeleton{background:linear-gradient(90deg,#e2e8f0,#f1f5f9,#e2e8f0 80%);background-size:200% 100%;animation:skeleton-shimmer 1.8s ease-in-out infinite;border-radius:6px}@keyframes cortex-pulse{0%,to{opacity:.4}50%{opacity:1}}@keyframes cortex-dot-bounce{0%,80%,to{transform:translateY(0)}40%{transform:translateY(-3px)}}.cortex-working-dot{animation:cortex-pulse 1.6s ease-in-out infinite,cortex-dot-bounce 1.2s ease-in-out infinite}.cortex-working-dot:nth-child(2){animation-delay:.15s}.cortex-working-dot:nth-child(3){animation-delay:.3s}@keyframes stop-pulse-ring{0%{opacity:.45;transform:scale(1)}50%{opacity:.15;transform:scale(1.12)}to{opacity:.45;transform:scale(1)}}@keyframes stop-fade-in{0%{opacity:0;transform:scale(.7)}to{opacity:1;transform:scale(1)}}.stop-btn{background:#e11d48;color:#fff;animation:stop-fade-in .2s ease-out both;transition:background .15s ease}.stop-btn:hover{background:#be123c}.stop-btn:active{transform:scale(.92)}.stop-btn-ring{background:#e11d48;animation:stop-pulse-ring 2s ease-in-out infinite;pointer-events:none}\n"] }]
|
|
1232
|
+
}], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }, { type: i0.Output, args: ["textChange"] }] } });
|
|
1233
|
+
|
|
1234
|
+
class PrettyJsonPipe {
|
|
1235
|
+
transform(value) {
|
|
1236
|
+
if (value == null)
|
|
1237
|
+
return '';
|
|
1238
|
+
const formatted = this.deepFormat(value);
|
|
1239
|
+
return JSON.stringify(formatted, null, 2).replace(/\\n/g, '\n');
|
|
1240
|
+
}
|
|
1241
|
+
deepFormat(value) {
|
|
1242
|
+
if (typeof value === 'string') {
|
|
1243
|
+
const trimmed = value.trim();
|
|
1244
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
1245
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
1246
|
+
try {
|
|
1247
|
+
const parsed = JSON.parse(trimmed);
|
|
1248
|
+
return this.deepFormat(parsed);
|
|
1249
|
+
}
|
|
1250
|
+
catch {
|
|
1251
|
+
// Not valid JSON, fall through
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return value;
|
|
1255
|
+
}
|
|
1256
|
+
if (Array.isArray(value)) {
|
|
1257
|
+
return value.map((item) => this.deepFormat(item));
|
|
1258
|
+
}
|
|
1259
|
+
if (value !== null && typeof value === 'object') {
|
|
1260
|
+
const result = {};
|
|
1261
|
+
for (const [key, val] of Object.entries(value)) {
|
|
1262
|
+
result[key] = this.deepFormat(val);
|
|
1263
|
+
}
|
|
1264
|
+
return result;
|
|
1265
|
+
}
|
|
1266
|
+
return value;
|
|
1267
|
+
}
|
|
1268
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PrettyJsonPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
1269
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.0", ngImport: i0, type: PrettyJsonPipe, isStandalone: true, name: "prettyJson" });
|
|
1270
|
+
}
|
|
1271
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PrettyJsonPipe, decorators: [{
|
|
1272
|
+
type: Pipe,
|
|
1273
|
+
args: [{ name: 'prettyJson', standalone: true }]
|
|
1274
|
+
}] });
|
|
1275
|
+
|
|
1276
|
+
/*
|
|
1277
|
+
* Public API Surface of cortex-client
|
|
1278
|
+
*/
|
|
1279
|
+
// Services
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* Generated bundle index. Do not edit.
|
|
1283
|
+
*/
|
|
1284
|
+
|
|
1285
|
+
export { CortexChatService, CortexChatWidgetComponent, CortexClientWebSocketService, FileInputComponent, HighlightPipe, JsonTreeComponent, MarkedPipe, MessageCaptureFilesPartComponent, MessageComponent, MessageListComponent, MessagePartComponent, MessageReasoningAnimatedComponent, MessageReasoningPartComponent, MessageTextPartComponent, MessageToolCallAnimatedComponent, MessageToolCallPartComponent, PrettyJsonPipe };
|
|
1286
|
+
//# sourceMappingURL=m6d-cortex-client.mjs.map
|