@k2b/cloud 0.26.0 → 0.28.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/package.json +3 -3
- package/src/_internal/define-app.ts +8 -1
- package/src/_internal/process-identity.ts +7 -1
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/ai/admin.ts +1 -0
- package/src/ai/browser-code-contracts.ts +46 -64
- package/src/ai/browser.ts +9 -1
- package/src/ai/capabilities.ts +82 -22
- package/src/ai/chat/blocks.tsx +16 -7
- package/src/ai/chat/builtin-tools.tsx +31 -23
- package/src/ai/chat/file-tools.tsx +4 -1
- package/src/ai/chat/live-turn.browser-harness.tsx +15 -5
- package/src/ai/chat/message-actions.tsx +123 -101
- package/src/ai/chat/message-utils.ts +30 -1
- package/src/ai/chat/messages.ts +206 -2
- package/src/ai/chat/presentation.tsx +94 -28
- package/src/ai/chat/tool-groups.ts +1 -1
- package/src/ai/chat/turn-error.ts +21 -0
- package/src/ai/chat/turn-view.tsx +102 -16
- package/src/ai/chat/user-message.tsx +19 -14
- package/src/ai/chat/visual-tools.tsx +1 -1
- package/src/ai/client/controller.ts +83 -37
- package/src/ai/client/file-source.ts +20 -3
- package/src/ai/client/projection.ts +7 -2
- package/src/ai/code-mode-skill.ts +32 -36
- package/src/ai/code-runtime-tools.ts +14 -1
- package/src/ai/code-source-contracts.ts +56 -6
- package/src/ai/code-source-tools.ts +10 -3
- package/src/ai/credentials.ts +17 -3
- package/src/ai/data-analysis-skill.ts +3 -3
- package/src/ai/default-tools.ts +15 -16
- package/src/ai/executor.ts +281 -141
- package/src/ai/file-context.ts +14 -2
- package/src/ai/file-tools.ts +17 -3
- package/src/ai/files-store.ts +134 -11
- package/src/ai/grids-skill.ts +2 -2
- package/src/ai/index.ts +9 -0
- package/src/ai/memories.ts +14 -0
- package/src/ai/migrate.ts +125 -0
- package/src/ai/model-request-settings.ts +98 -0
- package/src/ai/open-tool-calls.ts +87 -0
- package/src/ai/protocol.ts +6 -0
- package/src/ai/provider.ts +7 -1
- package/src/ai/quota-provider.ts +2 -2
- package/src/ai/request-headers.ts +117 -0
- package/src/ai/routes.ts +40 -6
- package/src/ai/run-timeout.ts +4 -5
- package/src/ai/runtime.ts +9 -1
- package/src/ai/settings.ts +19 -2
- package/src/ai/skill-seeds.ts +35 -7
- package/src/ai/skills.ts +26 -0
- package/src/ai/solid.ts +1 -1
- package/src/ai/store.ts +155 -71
- package/src/ai/stream.ts +180 -37
- package/src/ai/structured.ts +20 -5
- package/src/ai/system-prompt.ts +8 -0
- package/src/ai/tool-call-names.ts +45 -0
- package/src/ai/turn-failure.ts +100 -0
- package/src/ai/turn-policy.ts +248 -0
- package/src/ai/types.ts +52 -4
- package/src/api/admin-ai-quotas.ts +36 -1
- package/src/api/admin-core-settings.ts +16 -23
- package/src/api/admin-outgoing-mail.ts +242 -0
- package/src/api/index.ts +2 -0
- package/src/browser/FileChooser.tsx +11 -0
- package/src/browser/file-chooser-messages.ts +2 -0
- package/src/cli/admin/ai-quotas.ts +70 -1
- package/src/cli/admin/index.ts +8 -0
- package/src/cli/admin/notifications.ts +6 -0
- package/src/cli/admin/outgoing-mail.ts +215 -0
- package/src/contracts/app.ts +2 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/outgoing-mail.ts +265 -0
- package/src/contracts/registry.ts +2 -0
- package/src/services/help/store.ts +2 -1
- package/src/services/index.ts +13 -0
- package/src/services/notifications/batches.ts +139 -79
- package/src/services/notifications/channels.ts +33 -13
- package/src/services/notifications/dispatcher.ts +57 -5
- package/src/services/notifications/email-frame.fixture.html +51 -0
- package/src/services/notifications/{email.ts → email-frame.ts} +12 -45
- package/src/services/notifications/email-mail.ts +101 -0
- package/src/services/notifications/index.ts +49 -36
- package/src/services/notifications/observability.ts +9 -1
- package/src/services/notifications/platform.ts +1 -1
- package/src/services/notifications/runtime.ts +9 -3
- package/src/services/outgoing-mail/admin.ts +84 -0
- package/src/services/outgoing-mail/attachments.ts +135 -0
- package/src/services/outgoing-mail/bulk.ts +11 -0
- package/src/services/outgoing-mail/dispatcher.ts +231 -0
- package/src/services/outgoing-mail/drain.ts +60 -0
- package/src/services/outgoing-mail/enqueue.ts +180 -0
- package/src/services/outgoing-mail/index.ts +112 -0
- package/src/services/outgoing-mail/message.ts +14 -0
- package/src/services/outgoing-mail/messages.ts +431 -0
- package/src/services/outgoing-mail/retention.ts +48 -0
- package/src/services/outgoing-mail/runtime.ts +72 -0
- package/src/services/outgoing-mail/send.ts +130 -0
- package/src/services/outgoing-mail/store.ts +397 -0
- package/src/services/outgoing-mail/sync.ts +39 -0
- package/src/services/outgoing-mail/test-send.ts +40 -0
- package/src/services/outgoing-mail/transport.ts +17 -0
- package/src/services/pdf/markdown.ts +22 -4
- package/src/services/postgres.ts +15 -0
- package/src/services/settings/core-settings.ts +19 -38
- package/src/services/settings/store.ts +5 -1
- package/src/shared/ai-model-request-settings.ts +21 -0
- package/src/shared/ai-platform-prompt.ts +49 -5
- package/src/shared/ai-request-options.ts +185 -0
- package/src/shared/markdown/extensions/links.ts +28 -20
- package/src/shared/markdown/index.ts +14 -5
- package/src/shared/markdown/shared.ts +0 -7
- package/src/ssr/GlobalAnnouncements.island.tsx +1 -1
- package/src/ssr/admin-navigation.ts +1 -1
- package/src/ssr/platform-messages.ts +3 -1
- package/src/ssr/workspace-navigation.ts +7 -1
- package/src/styles/effects.css +15 -17
- package/src/styles/tokens.css +2 -0
- package/src/styles/utilities-markdown-editor.css +4 -39
- package/src/styles/utilities-markdown-table.css +14 -17
package/src/ai/chat/messages.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
import type { AiTurnError } from "../types";
|
|
2
3
|
|
|
3
4
|
const messages = i18n.define({
|
|
4
5
|
baseLocale: "en",
|
|
@@ -21,6 +22,24 @@ const messages = i18n.define({
|
|
|
21
22
|
thinking: "Thinking",
|
|
22
23
|
showReasoning: "Show reasoning",
|
|
23
24
|
reconnecting: "Reconnecting",
|
|
25
|
+
turnErrorTitle: "The answer was interrupted.",
|
|
26
|
+
turnErrorReason: ({ code, limitMinutes }: AiTurnError) => {
|
|
27
|
+
if (code === "model_unavailable") return "The model service did not answer.";
|
|
28
|
+
if (code === "quota_exhausted") return "Your AI usage limit for this period is reached. You can continue once it resets.";
|
|
29
|
+
if (code === "context_full") return "This chat is too long for the model. Start a new chat to continue; this one stays as it is.";
|
|
30
|
+
if (code === "time_limit")
|
|
31
|
+
return limitMinutes ? `The run time limit of ${limitMinutes} minutes was reached.` : "The run time limit was reached.";
|
|
32
|
+
if (code === "step_limit") return "The model reached its step limit for one answer.";
|
|
33
|
+
if (code === "wait_expired") return "The approval or answer did not arrive in time.";
|
|
34
|
+
if (code === "interrupted") return "The work was interrupted several times and could not be resumed.";
|
|
35
|
+
if (code === "not_allowed")
|
|
36
|
+
return "This chat may no longer use its model, project, or apps. Choose another model or ask your administrator.";
|
|
37
|
+
return "Something went wrong.";
|
|
38
|
+
},
|
|
39
|
+
turnErrorKept: "The results so far are kept.",
|
|
40
|
+
turnErrorContinueHint: "Send a new message to continue.",
|
|
41
|
+
continueTurn: "Continue",
|
|
42
|
+
continueTurnMessage: "Continue where you left off.",
|
|
24
43
|
byteRange: ({ start, end }: { start: string; end: string }) => `Bytes ${start}–${end}`,
|
|
25
44
|
worked: "Worked",
|
|
26
45
|
workedFor: ({ duration }: { duration: string }) => `Worked ${duration}`,
|
|
@@ -160,7 +179,10 @@ const messages = i18n.define({
|
|
|
160
179
|
loadedCount: ({ count }: { count: number }) => `${count} loaded`,
|
|
161
180
|
toolsLoaded: "Loaded",
|
|
162
181
|
toolsAlreadyLoaded: "Already loaded",
|
|
163
|
-
|
|
182
|
+
toolUnknown: "Not found",
|
|
183
|
+
toolNotOffered: "Not available here",
|
|
184
|
+
toolNotAllowed: "Not allowed",
|
|
185
|
+
toolAppOffline: "App not reachable",
|
|
164
186
|
toolsEvicted: "Evicted",
|
|
165
187
|
loadedSkill: ({ name }: { name: string }) => `Loaded skill ${name}`,
|
|
166
188
|
help: "Help",
|
|
@@ -184,6 +206,86 @@ const messages = i18n.define({
|
|
|
184
206
|
calculate: "Calculate",
|
|
185
207
|
inspectImage: ({ name }: { name: string }) => `Inspect image: ${name}`,
|
|
186
208
|
readResource: ({ ref }: { ref: string }) => `Read resource: ${ref}`,
|
|
209
|
+
actionHelpful: "Helpful",
|
|
210
|
+
actionHelpfulRemove: "Remove positive feedback",
|
|
211
|
+
actionImprove: "Needs improvement",
|
|
212
|
+
actionImproveRemove: "Remove negative feedback",
|
|
213
|
+
actionMessageInfo: "Response details",
|
|
214
|
+
actionCopy: "Copy",
|
|
215
|
+
actionFork: "Continue in a new chat",
|
|
216
|
+
actionTryAgain: "Try again",
|
|
217
|
+
actionMoreDetailed: "More detailed",
|
|
218
|
+
actionMoreConcise: "More concise",
|
|
219
|
+
actionEditPrompt: "Edit and try again",
|
|
220
|
+
actionRetryGuidance: "Send guidance again",
|
|
221
|
+
cancel: "Cancel",
|
|
222
|
+
forkTitle: "Continue in a new chat",
|
|
223
|
+
forkConfirm: "Create chat",
|
|
224
|
+
forkName: "Chat name",
|
|
225
|
+
forkDefaultName: "Continued chat",
|
|
226
|
+
retryTitle: "Edit and try again",
|
|
227
|
+
retryPrompt: "Message",
|
|
228
|
+
retryPromptHint: "Attachments from the original message stay attached.",
|
|
229
|
+
retryFailed: "Could not try again. Your changes are still here.",
|
|
230
|
+
retrying: "Trying again",
|
|
231
|
+
feedbackTitle: "Help improve this response",
|
|
232
|
+
feedbackIntro: "What should be better? Choose all that apply or leave a short note.",
|
|
233
|
+
feedbackDetails: "Anything else",
|
|
234
|
+
feedbackDetailsHint: "Optional. Please leave out confidential information.",
|
|
235
|
+
feedbackSend: "Send feedback",
|
|
236
|
+
feedbackIncorrect: "Incorrect",
|
|
237
|
+
feedbackIncorrectHint: "Facts or conclusions were wrong.",
|
|
238
|
+
feedbackIgnoredRequest: "Did not do what I asked",
|
|
239
|
+
feedbackIgnoredRequestHint: "Important instructions were missed.",
|
|
240
|
+
feedbackIncomplete: "Incomplete",
|
|
241
|
+
feedbackIncompleteHint: "Important information or work was missing.",
|
|
242
|
+
feedbackWrongApp: "Used the wrong app or tool",
|
|
243
|
+
feedbackWrongAppHint: "Something was missing, unnecessary, or used badly.",
|
|
244
|
+
feedbackSlow: "Too slow",
|
|
245
|
+
feedbackSlowHint: "The answer took too long for the result.",
|
|
246
|
+
feedbackOther: "Something else",
|
|
247
|
+
feedbackOtherHint: "Something else should be better.",
|
|
248
|
+
infoSubtitle: "Technical details of this answer",
|
|
249
|
+
infoDetails: "Details",
|
|
250
|
+
infoProviderModel: "Provider model",
|
|
251
|
+
infoModelProfile: "Model",
|
|
252
|
+
infoLoopId: "Run ID",
|
|
253
|
+
infoFinished: "Ended",
|
|
254
|
+
infoEndCompleted: "Completed",
|
|
255
|
+
infoEndStopped: "Stopped",
|
|
256
|
+
infoEndFailed: "Failed",
|
|
257
|
+
infoEndInterrupted: "Interrupted",
|
|
258
|
+
infoEndStepLimit: "Step limit reached",
|
|
259
|
+
infoEndUsageLimit: "Usage limit reached",
|
|
260
|
+
infoEndContextFull: "Context window full",
|
|
261
|
+
infoEndLengthLimit: "Answer length limit reached",
|
|
262
|
+
infoCreated: "Created",
|
|
263
|
+
infoUnknown: "Unknown",
|
|
264
|
+
infoLegacy: "Older message",
|
|
265
|
+
infoTiming: "Time",
|
|
266
|
+
infoWorked: "Worked",
|
|
267
|
+
infoWorkedHint: "writing and tools",
|
|
268
|
+
infoGeneration: "Writing",
|
|
269
|
+
infoToolTime: "Tools",
|
|
270
|
+
infoWaiting: "Waiting for you",
|
|
271
|
+
infoWaitingHint: "approvals and answers",
|
|
272
|
+
infoWallClock: "Total time",
|
|
273
|
+
infoSpeed: "Writing speed",
|
|
274
|
+
infoTokensPerSecond: ({ value }: { value: string }) => `${value} tokens/s`,
|
|
275
|
+
infoUsage: "Usage",
|
|
276
|
+
infoInputTokens: "Input tokens",
|
|
277
|
+
infoOutputTokens: "Output tokens",
|
|
278
|
+
infoTotalTokens: "Total tokens",
|
|
279
|
+
infoTools: "Tools",
|
|
280
|
+
infoToolsHint: "Tools this answer used.",
|
|
281
|
+
infoIssueErrors: "errors",
|
|
282
|
+
infoIssueStream: "connection problems",
|
|
283
|
+
infoIssueMalformed: "malformed",
|
|
284
|
+
infoIssueCancelled: "cancelled",
|
|
285
|
+
compactedCount: ({ count, date }: { count: number; date: string }) =>
|
|
286
|
+
`${count === 1 ? "1 message" : `${count} messages`} summarized · ${date}`,
|
|
287
|
+
compactedHint: "From here on, the model sees this summary instead of the messages above.",
|
|
288
|
+
compactedEmpty: "No visible content",
|
|
187
289
|
},
|
|
188
290
|
de: {
|
|
189
291
|
backgroundRun: "Hintergrundlauf",
|
|
@@ -204,6 +306,26 @@ const messages = i18n.define({
|
|
|
204
306
|
thinking: "Denkt nach",
|
|
205
307
|
showReasoning: "Denkprozess anzeigen",
|
|
206
308
|
reconnecting: "Verbindung wird wiederhergestellt",
|
|
309
|
+
turnErrorTitle: "Die Antwort wurde abgebrochen.",
|
|
310
|
+
turnErrorReason: ({ code, limitMinutes }) => {
|
|
311
|
+
if (code === "model_unavailable") return "Der KI-Dienst hat nicht geantwortet.";
|
|
312
|
+
if (code === "quota_exhausted")
|
|
313
|
+
return "Dein KI-Kontingent für diesen Zeitraum ist aufgebraucht. Du kannst weiterarbeiten, sobald es sich erneuert.";
|
|
314
|
+
if (code === "context_full")
|
|
315
|
+
return "Dieser Chat ist zu lang für das Modell. Starte einen neuen Chat, um weiterzuarbeiten; dieser bleibt erhalten.";
|
|
316
|
+
if (code === "time_limit")
|
|
317
|
+
return limitMinutes ? `Das Laufzeitlimit von ${limitMinutes} Minuten ist erreicht.` : "Das Laufzeitlimit ist erreicht.";
|
|
318
|
+
if (code === "step_limit") return "Das Modell hat sein Schrittlimit für eine Antwort erreicht.";
|
|
319
|
+
if (code === "wait_expired") return "Die Freigabe oder Antwort kam nicht rechtzeitig.";
|
|
320
|
+
if (code === "interrupted") return "Die Arbeit wurde mehrmals unterbrochen und konnte nicht fortgesetzt werden.";
|
|
321
|
+
if (code === "not_allowed")
|
|
322
|
+
return "Dieser Chat darf sein Modell, sein Projekt oder seine Apps nicht mehr nutzen. Wähle ein anderes Modell oder wende dich an deine Administration.";
|
|
323
|
+
return "Etwas ist schiefgelaufen.";
|
|
324
|
+
},
|
|
325
|
+
turnErrorKept: "Die bisherigen Ergebnisse bleiben erhalten.",
|
|
326
|
+
turnErrorContinueHint: "Mit einer neuen Nachricht geht es weiter.",
|
|
327
|
+
continueTurn: "Weiterarbeiten",
|
|
328
|
+
continueTurnMessage: "Mach an der Stelle weiter, an der du aufgehört hast.",
|
|
207
329
|
byteRange: ({ start, end }) => `Bytes ${start}–${end}`,
|
|
208
330
|
worked: "Gearbeitet",
|
|
209
331
|
workedFor: ({ duration }) => `${duration} gearbeitet`,
|
|
@@ -347,7 +469,10 @@ const messages = i18n.define({
|
|
|
347
469
|
loadedCount: ({ count }) => `${count} geladen`,
|
|
348
470
|
toolsLoaded: "Geladen",
|
|
349
471
|
toolsAlreadyLoaded: "Bereits geladen",
|
|
350
|
-
|
|
472
|
+
toolUnknown: "Nicht gefunden",
|
|
473
|
+
toolNotOffered: "Hier nicht verfügbar",
|
|
474
|
+
toolNotAllowed: "Nicht erlaubt",
|
|
475
|
+
toolAppOffline: "App nicht erreichbar",
|
|
351
476
|
toolsEvicted: "Entladen",
|
|
352
477
|
loadedSkill: ({ name }) => `Skill ${name} geladen`,
|
|
353
478
|
help: "Hilfe",
|
|
@@ -371,6 +496,85 @@ const messages = i18n.define({
|
|
|
371
496
|
calculate: "Berechnen",
|
|
372
497
|
inspectImage: ({ name }) => `Bild prüfen: ${name}`,
|
|
373
498
|
readResource: ({ ref }) => `Ressource lesen: ${ref}`,
|
|
499
|
+
actionHelpful: "Hilfreich",
|
|
500
|
+
actionHelpfulRemove: "Positive Bewertung entfernen",
|
|
501
|
+
actionImprove: "Verbesserungswürdig",
|
|
502
|
+
actionImproveRemove: "Negative Bewertung entfernen",
|
|
503
|
+
actionMessageInfo: "Details zur Antwort",
|
|
504
|
+
actionCopy: "Kopieren",
|
|
505
|
+
actionFork: "In neuem Chat fortsetzen",
|
|
506
|
+
actionTryAgain: "Noch einmal versuchen",
|
|
507
|
+
actionMoreDetailed: "Ausführlicher",
|
|
508
|
+
actionMoreConcise: "Kürzer",
|
|
509
|
+
actionEditPrompt: "Bearbeiten und neu senden",
|
|
510
|
+
actionRetryGuidance: "Hinweis erneut senden",
|
|
511
|
+
cancel: "Abbrechen",
|
|
512
|
+
forkTitle: "In neuem Chat fortsetzen",
|
|
513
|
+
forkConfirm: "Chat erstellen",
|
|
514
|
+
forkName: "Name des Chats",
|
|
515
|
+
forkDefaultName: "Fortgesetzter Chat",
|
|
516
|
+
retryTitle: "Bearbeiten und neu senden",
|
|
517
|
+
retryPrompt: "Nachricht",
|
|
518
|
+
retryPromptHint: "Die Anhänge der ursprünglichen Nachricht bleiben erhalten.",
|
|
519
|
+
retryFailed: "Das hat nicht geklappt. Deine Änderungen sind noch da.",
|
|
520
|
+
retrying: "Wird gesendet",
|
|
521
|
+
feedbackTitle: "Hilf, diese Antwort zu verbessern",
|
|
522
|
+
feedbackIntro: "Was sollte besser sein? Wähle alles Passende aus oder schreib eine kurze Notiz.",
|
|
523
|
+
feedbackDetails: "Sonst noch etwas",
|
|
524
|
+
feedbackDetailsHint: "Optional. Bitte keine vertraulichen Daten angeben.",
|
|
525
|
+
feedbackSend: "Feedback senden",
|
|
526
|
+
feedbackIncorrect: "Falsch",
|
|
527
|
+
feedbackIncorrectHint: "Fakten oder Schlussfolgerungen stimmten nicht.",
|
|
528
|
+
feedbackIgnoredRequest: "Hat nicht getan, worum ich gebeten habe",
|
|
529
|
+
feedbackIgnoredRequestHint: "Wichtige Anweisungen wurden übersehen.",
|
|
530
|
+
feedbackIncomplete: "Unvollständig",
|
|
531
|
+
feedbackIncompleteHint: "Wichtige Informationen oder Arbeitsschritte fehlten.",
|
|
532
|
+
feedbackWrongApp: "Falsche App oder falsches Werkzeug",
|
|
533
|
+
feedbackWrongAppHint: "Etwas fehlte, war unnötig oder wurde falsch genutzt.",
|
|
534
|
+
feedbackSlow: "Zu langsam",
|
|
535
|
+
feedbackSlowHint: "Die Antwort hat für das Ergebnis zu lange gedauert.",
|
|
536
|
+
feedbackOther: "Etwas anderes",
|
|
537
|
+
feedbackOtherHint: "Etwas anderes sollte besser werden.",
|
|
538
|
+
infoSubtitle: "Technische Angaben zu dieser Antwort",
|
|
539
|
+
infoDetails: "Details",
|
|
540
|
+
infoProviderModel: "Modell beim Anbieter",
|
|
541
|
+
infoModelProfile: "Modell",
|
|
542
|
+
infoLoopId: "Durchlauf-ID",
|
|
543
|
+
infoFinished: "Beendet",
|
|
544
|
+
infoEndCompleted: "Abgeschlossen",
|
|
545
|
+
infoEndStopped: "Gestoppt",
|
|
546
|
+
infoEndFailed: "Fehlgeschlagen",
|
|
547
|
+
infoEndInterrupted: "Unterbrochen",
|
|
548
|
+
infoEndStepLimit: "Schrittlimit erreicht",
|
|
549
|
+
infoEndUsageLimit: "Nutzungslimit erreicht",
|
|
550
|
+
infoEndContextFull: "Kontextfenster voll",
|
|
551
|
+
infoEndLengthLimit: "Maximale Antwortlänge erreicht",
|
|
552
|
+
infoCreated: "Erstellt",
|
|
553
|
+
infoUnknown: "Unbekannt",
|
|
554
|
+
infoLegacy: "Ältere Nachricht",
|
|
555
|
+
infoTiming: "Zeit",
|
|
556
|
+
infoWorked: "Gearbeitet",
|
|
557
|
+
infoWorkedHint: "Schreiben und Werkzeuge",
|
|
558
|
+
infoGeneration: "Schreiben",
|
|
559
|
+
infoToolTime: "Werkzeuge",
|
|
560
|
+
infoWaiting: "Wartezeit auf dich",
|
|
561
|
+
infoWaitingHint: "Freigaben und Antworten",
|
|
562
|
+
infoWallClock: "Gesamtdauer",
|
|
563
|
+
infoSpeed: "Schreibtempo",
|
|
564
|
+
infoTokensPerSecond: ({ value }) => `${value} Token/s`,
|
|
565
|
+
infoUsage: "Verbrauch",
|
|
566
|
+
infoInputTokens: "Eingabe-Token",
|
|
567
|
+
infoOutputTokens: "Ausgabe-Token",
|
|
568
|
+
infoTotalTokens: "Token gesamt",
|
|
569
|
+
infoTools: "Werkzeuge",
|
|
570
|
+
infoToolsHint: "Werkzeuge, die diese Antwort genutzt hat.",
|
|
571
|
+
infoIssueErrors: "Fehler",
|
|
572
|
+
infoIssueStream: "Verbindungsprobleme",
|
|
573
|
+
infoIssueMalformed: "fehlerhaft",
|
|
574
|
+
infoIssueCancelled: "abgebrochen",
|
|
575
|
+
compactedCount: ({ count, date }) => `${count === 1 ? "1 Nachricht" : `${count} Nachrichten`} zusammengefasst · ${date}`,
|
|
576
|
+
compactedHint: "Ab hier sieht das Modell diese Zusammenfassung statt der Nachrichten darüber.",
|
|
577
|
+
compactedEmpty: "Kein sichtbarer Inhalt",
|
|
374
578
|
},
|
|
375
579
|
},
|
|
376
580
|
});
|
|
@@ -13,11 +13,12 @@ import {
|
|
|
13
13
|
Show,
|
|
14
14
|
} from "solid-js";
|
|
15
15
|
import type { AiActiveTurn } from "../client/projection";
|
|
16
|
-
import { type AiActiveTurnSegment, splitActiveTurnBlocks } from "../protocol";
|
|
16
|
+
import { type AiActiveTurnSegment, type AiTurnBlock, splitActiveTurnBlocks } from "../protocol";
|
|
17
17
|
import { type AiAssistantTimelineItem, buildAiMessageTimeline } from "../timeline";
|
|
18
|
-
import type { AiConversationTimelineEntry, AiStoredMessage } from "../types";
|
|
18
|
+
import type { AiConversationTimelineEntry, AiStoredMessage, AiTurnError } from "../types";
|
|
19
19
|
import { type AiChatActions, AiChatActionsProvider, createAssistantMessageActions, useAiChatActions } from "./message-actions";
|
|
20
20
|
import { isRecord, isSurveyToolName, textFromMessage } from "./message-utils";
|
|
21
|
+
import { aiChatMessages } from "./messages";
|
|
21
22
|
import { type AiToolDisclosureState, createAiToolDisclosureState } from "./tool-disclosure";
|
|
22
23
|
import { type AiTurnLayout, type AiTurnPhase, layoutAiTurn, waitsForUser } from "./turn-layout";
|
|
23
24
|
import { TurnNavigator } from "./turn-navigator";
|
|
@@ -108,8 +109,9 @@ const storedPhase = (entries: readonly AiStoredMessage[]): AiTurnPhase => {
|
|
|
108
109
|
return reason === "aborted" ? "stopped" : "failed";
|
|
109
110
|
};
|
|
110
111
|
|
|
111
|
-
/** Responses with a work line
|
|
112
|
-
const isWideLayout = (layout: AiTurnLayout) =>
|
|
112
|
+
/** Responses with a work line, rich places, or a failure notice span the message column so their right edges align. */
|
|
113
|
+
const isWideLayout = (layout: AiTurnLayout, error?: AiTurnError) =>
|
|
114
|
+
layout.showWork || layout.results.length > 0 || layout.actions.length > 0 || Boolean(error);
|
|
113
115
|
|
|
114
116
|
const isEmptyLayout = (layout: AiTurnLayout) => !isWideLayout(layout) && layout.text.length === 0;
|
|
115
117
|
|
|
@@ -162,13 +164,49 @@ const storedItems = (
|
|
|
162
164
|
actions: AiChatActions,
|
|
163
165
|
views: TurnViews,
|
|
164
166
|
locale: string,
|
|
167
|
+
idle: boolean,
|
|
165
168
|
): ChatTimelineItem[] => {
|
|
166
169
|
const timeline = buildAiMessageTimeline([...messages]);
|
|
167
170
|
const lastItemOfLoop = new Map<string, number>();
|
|
171
|
+
// A failed turn's notice ends its last item, which is its user message when the model never answered.
|
|
172
|
+
const lastEntryOfLoop = new Map<string, number>();
|
|
168
173
|
timeline.forEach((item, index) => {
|
|
169
|
-
|
|
174
|
+
const loopId = item.type === "summary" ? null : item.type === "user" ? item.entry.loopId : item.loopId;
|
|
175
|
+
if (!loopId) return;
|
|
176
|
+
lastEntryOfLoop.set(loopId, index);
|
|
177
|
+
if (item.type === "assistant") lastItemOfLoop.set(loopId, index);
|
|
170
178
|
});
|
|
179
|
+
const turnErrors = new Map<string, AiTurnError>();
|
|
180
|
+
for (const message of messages) if (message.loopId && message.meta?.turnError) turnErrors.set(message.loopId, message.meta.turnError);
|
|
181
|
+
// Only the chat's newest turn can be continued, and only while no other turn runs.
|
|
182
|
+
const newestLoop = idle ? (messages.at(-1)?.loopId ?? null) : null;
|
|
171
183
|
const segmentId = createSegmentIds();
|
|
184
|
+
// The model never answered: the notice stands where the live turn showed its progress, under the same id.
|
|
185
|
+
const unansweredNotice = (entry: AiStoredMessage, itemIndex: number): ChatTimelineItem[] => {
|
|
186
|
+
const loopId = entry.loopId;
|
|
187
|
+
const error = loopId && lastEntryOfLoop.get(loopId) === itemIndex ? turnErrors.get(loopId) : undefined;
|
|
188
|
+
if (!loopId || !error) return [];
|
|
189
|
+
const id = segmentId(loopId, "start");
|
|
190
|
+
return [
|
|
191
|
+
{
|
|
192
|
+
kind: "message",
|
|
193
|
+
id,
|
|
194
|
+
role: "assistant",
|
|
195
|
+
createdAt: entry.createdAt,
|
|
196
|
+
class: "ai-chat-message-wide",
|
|
197
|
+
content: views.content({
|
|
198
|
+
id,
|
|
199
|
+
turnId: loopId,
|
|
200
|
+
phase: "failed",
|
|
201
|
+
layout: layoutAiTurn([], { phase: "failed" }),
|
|
202
|
+
earlier: false,
|
|
203
|
+
duration: () => null,
|
|
204
|
+
error,
|
|
205
|
+
continuable: newestLoop === loopId,
|
|
206
|
+
}),
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
};
|
|
172
210
|
return timeline.flatMap((item, itemIndex): ChatTimelineItem | ChatTimelineItem[] => {
|
|
173
211
|
if (item.type === "user") {
|
|
174
212
|
const text = aiUserMessageText(item.entry);
|
|
@@ -176,7 +214,7 @@ const storedItems = (
|
|
|
176
214
|
if (agentMessage) {
|
|
177
215
|
const separator = text.indexOf("\n\n");
|
|
178
216
|
const forwardedText = separator >= 0 ? text.slice(separator + 2) : text;
|
|
179
|
-
|
|
217
|
+
const systemItem: ChatTimelineItem = {
|
|
180
218
|
kind: "message",
|
|
181
219
|
id: item.id,
|
|
182
220
|
role: "system",
|
|
@@ -208,18 +246,20 @@ const storedItems = (
|
|
|
208
246
|
),
|
|
209
247
|
anchorId: item.entry.seq,
|
|
210
248
|
};
|
|
249
|
+
return [systemItem, ...unansweredNotice(item.entry, itemIndex)];
|
|
211
250
|
}
|
|
212
|
-
|
|
251
|
+
const userItem: ChatTimelineItem = {
|
|
213
252
|
kind: "message",
|
|
214
253
|
id: item.id,
|
|
215
254
|
role: "user",
|
|
216
255
|
createdAt: item.entry.createdAt,
|
|
217
256
|
content: text ? <p class="whitespace-pre-wrap">{text}</p> : undefined,
|
|
218
257
|
attachments: aiUserMessageAttachments(item.entry, actions),
|
|
219
|
-
actions: createAiUserMessageActions(item.entry, actions),
|
|
258
|
+
actions: createAiUserMessageActions(item.entry, actions, locale),
|
|
220
259
|
actionDisplay: "menu",
|
|
221
260
|
anchorId: item.entry.seq,
|
|
222
261
|
};
|
|
262
|
+
return [userItem, ...unansweredNotice(item.entry, itemIndex)];
|
|
223
263
|
}
|
|
224
264
|
|
|
225
265
|
if (item.type === "summary") {
|
|
@@ -228,16 +268,14 @@ const storedItems = (
|
|
|
228
268
|
return {
|
|
229
269
|
kind: "activity",
|
|
230
270
|
id: item.id,
|
|
231
|
-
label:
|
|
232
|
-
description: count ?
|
|
271
|
+
label: aiChatMessages(locale).compacted,
|
|
272
|
+
description: count ? aiChatMessages(locale).compactedCount({ count, date }) : date,
|
|
233
273
|
icon: "ti ti-brain",
|
|
234
274
|
tone: "ai",
|
|
235
275
|
content: (
|
|
236
276
|
<div>
|
|
237
|
-
<p class="mb-1 text-
|
|
238
|
-
|
|
239
|
-
</p>
|
|
240
|
-
<p class="whitespace-pre-wrap">{textFromMessage(item.entry.message) || "No visible content"}</p>
|
|
277
|
+
<p class="mb-1 text-dimmed">{aiChatMessages(locale).compactedHint}</p>
|
|
278
|
+
<p class="whitespace-pre-wrap">{textFromMessage(item.entry.message) || aiChatMessages(locale).compactedEmpty}</p>
|
|
241
279
|
</div>
|
|
242
280
|
),
|
|
243
281
|
};
|
|
@@ -245,9 +283,12 @@ const storedItems = (
|
|
|
245
283
|
|
|
246
284
|
const actionEntry = item.actionEntry?.compactedAt ? null : item.actionEntry;
|
|
247
285
|
const lastOfLoop = !item.loopId || lastItemOfLoop.get(item.loopId) === itemIndex;
|
|
248
|
-
const
|
|
286
|
+
const turnError = item.loopId && lastOfLoop ? turnErrors.get(item.loopId) : undefined;
|
|
287
|
+
const phase = turnError ? "failed" : lastOfLoop ? storedPhase(item.entries) : "completed";
|
|
249
288
|
const segments = splitSurveyResults(item.blocks);
|
|
250
|
-
|
|
289
|
+
// A turn that failed after an accepted answer ends with its notice where the live turn showed its progress, below
|
|
290
|
+
// the answer and under the same id, as activeItems() places it.
|
|
291
|
+
if (segments.length === 0 || (turnError && segments.at(-1)?.type === "survey")) segments.push({ type: "assistant", blocks: [] });
|
|
251
292
|
const lastAssistant = segments.findLastIndex((segment) => segment.type === "assistant");
|
|
252
293
|
const scheduledTask = item.entries.find((entry) => entry.meta?.scheduledTask)?.meta?.scheduledTask ?? null;
|
|
253
294
|
let firstSegment = true;
|
|
@@ -268,7 +309,8 @@ const storedItems = (
|
|
|
268
309
|
: undefined;
|
|
269
310
|
const task = firstSegment ? scheduledTask : null;
|
|
270
311
|
firstSegment = false;
|
|
271
|
-
|
|
312
|
+
const error = last ? turnError : undefined;
|
|
313
|
+
if (isEmptyLayout(layout) && !messageActions && !task && !error) return [];
|
|
272
314
|
const id = item.loopId ? segmentId(item.loopId, segmentOpener) : `${item.id}:${index}`;
|
|
273
315
|
return [
|
|
274
316
|
{
|
|
@@ -276,7 +318,7 @@ const storedItems = (
|
|
|
276
318
|
id,
|
|
277
319
|
role: "assistant",
|
|
278
320
|
createdAt: actionEntry?.createdAt ?? item.entries.at(-1)?.createdAt,
|
|
279
|
-
class: isWideLayout(layout) ? "ai-chat-message-wide" : undefined,
|
|
321
|
+
class: isWideLayout(layout, error) ? "ai-chat-message-wide" : undefined,
|
|
280
322
|
content: views.content({
|
|
281
323
|
id,
|
|
282
324
|
turnId,
|
|
@@ -285,6 +327,8 @@ const storedItems = (
|
|
|
285
327
|
earlier: !last,
|
|
286
328
|
duration: last ? () => ({ workedMs: item.workedMs, waitingMs: null }) : () => null,
|
|
287
329
|
scheduledTask: task,
|
|
330
|
+
error,
|
|
331
|
+
continuable: Boolean(error) && newestLoop === item.loopId,
|
|
288
332
|
}),
|
|
289
333
|
actions: messageActions,
|
|
290
334
|
actionDisplay: "inline",
|
|
@@ -299,6 +343,8 @@ const activeItems = (
|
|
|
299
343
|
actions: AiChatActions,
|
|
300
344
|
views: TurnViews,
|
|
301
345
|
duration: Accessor<AiTurnDuration | null>,
|
|
346
|
+
reconnecting: boolean,
|
|
347
|
+
locale: string,
|
|
302
348
|
): ChatTimelineItem[] => {
|
|
303
349
|
if (!turn) return [];
|
|
304
350
|
const segments = splitActiveTurnBlocks(turn.blocks).flatMap((segment): (AiActiveTurnSegment | SurveySegment)[] =>
|
|
@@ -314,6 +360,14 @@ const activeItems = (
|
|
|
314
360
|
// Until the model takes up the steering message, the segment above it is still the one at work.
|
|
315
361
|
const above = steerTail ? segments.findLastIndex((segment, index) => segment.type === "assistant" && index < lastAssistant) : -1;
|
|
316
362
|
const working = above >= 0 ? above : lastAssistant;
|
|
363
|
+
const phaseOf = (index: number): AiTurnPhase =>
|
|
364
|
+
index !== working ? "completed" : turn.status === "waiting_for_action" ? "waiting" : "running";
|
|
365
|
+
const layoutOf = (blocks: AiTurnBlock[], index: number) =>
|
|
366
|
+
layoutAiTurn(blocks, { phase: phaseOf(index), codePresentations: Boolean(actions.renderCodePresentation) });
|
|
367
|
+
// The work line says in place that the turn reconnects. A turn without one yet says it in a row at its end, where a
|
|
368
|
+
// row that comes and goes moves nothing above it.
|
|
369
|
+
const workingSegment = segments[working];
|
|
370
|
+
const reconnectInLine = reconnecting && workingSegment?.type === "assistant" && layoutOf(workingSegment.blocks, working).showWork;
|
|
317
371
|
const segmentId = createSegmentIds();
|
|
318
372
|
let opener = "start";
|
|
319
373
|
|
|
@@ -331,7 +385,7 @@ const activeItems = (
|
|
|
331
385
|
role: "user",
|
|
332
386
|
status: block.status === "pending" ? "pending" : block.status === "failed" ? "error" : "complete",
|
|
333
387
|
content: <p class="whitespace-pre-wrap">{aiSteerMessageText(block)}</p>,
|
|
334
|
-
actions: createAiSteerMessageActions(block, actions),
|
|
388
|
+
actions: createAiSteerMessageActions(block, actions, locale),
|
|
335
389
|
actionDisplay: "menu",
|
|
336
390
|
anchorId: turn.seq,
|
|
337
391
|
};
|
|
@@ -339,8 +393,8 @@ const activeItems = (
|
|
|
339
393
|
|
|
340
394
|
const last = index === lastAssistant;
|
|
341
395
|
const live = index === working;
|
|
342
|
-
const phase
|
|
343
|
-
const layout =
|
|
396
|
+
const phase = phaseOf(index);
|
|
397
|
+
const layout = layoutOf(segment.blocks, index);
|
|
344
398
|
// Consecutive steering messages leave empty segments between them; history has none. The last segment stays, since
|
|
345
399
|
// it carries the progress indicator.
|
|
346
400
|
if (!last && isEmptyLayout(layout)) return [];
|
|
@@ -358,7 +412,7 @@ const activeItems = (
|
|
|
358
412
|
layout,
|
|
359
413
|
earlier: !live,
|
|
360
414
|
duration: live ? duration : () => null,
|
|
361
|
-
|
|
415
|
+
reconnect: live && reconnectInLine ? "line" : last && reconnecting && !reconnectInLine ? "row" : undefined,
|
|
362
416
|
}),
|
|
363
417
|
};
|
|
364
418
|
});
|
|
@@ -367,9 +421,10 @@ const activeItems = (
|
|
|
367
421
|
/**
|
|
368
422
|
* Work time of the active turn: wall time minus time spent waiting for the user. A state snapshot seeds it after a
|
|
369
423
|
* reconnect; the clock stands while the turn waits for an approval or an answer and continues from the same value
|
|
370
|
-
* afterwards.
|
|
424
|
+
* afterwards. While `paused`, the shown time stands too and then jumps to the true work time; the clock itself keeps
|
|
425
|
+
* running, so a step that starts right after the pause is timed from the true work time.
|
|
371
426
|
*/
|
|
372
|
-
const createActiveTurnClock = (source: AiChatTimelineSource): Accessor<AiTurnDuration | null> => {
|
|
427
|
+
const createActiveTurnClock = (source: AiChatTimelineSource, paused: Accessor<boolean>): Accessor<AiTurnDuration | null> => {
|
|
373
428
|
const [now, setNow] = createSignal(Date.now());
|
|
374
429
|
const running = createMemo(() => source.activeTurn() !== null);
|
|
375
430
|
createEffect(() => {
|
|
@@ -379,7 +434,7 @@ const createActiveTurnClock = (source: AiChatTimelineSource): Accessor<AiTurnDur
|
|
|
379
434
|
onCleanup(() => window.clearInterval(timer));
|
|
380
435
|
});
|
|
381
436
|
let state: { turnId: string; seed: string; startedAt: number; waitMs: number; waitingSince: number | null } | null = null;
|
|
382
|
-
|
|
437
|
+
const duration = createMemo(() => {
|
|
383
438
|
const turn = source.activeTurn();
|
|
384
439
|
const time = now();
|
|
385
440
|
if (!turn) {
|
|
@@ -411,11 +466,19 @@ const createActiveTurnClock = (source: AiChatTimelineSource): Accessor<AiTurnDur
|
|
|
411
466
|
waitingMs: state.waitingSince === null ? null : Math.max(0, time - state.waitingSince),
|
|
412
467
|
};
|
|
413
468
|
});
|
|
469
|
+
let shown: AiTurnDuration | null = null;
|
|
470
|
+
return createMemo(() => {
|
|
471
|
+
const current = duration();
|
|
472
|
+
if (!paused() || !shown || !current) shown = current;
|
|
473
|
+
return shown;
|
|
474
|
+
});
|
|
414
475
|
};
|
|
415
476
|
|
|
416
477
|
export type AiChatTimelineSource = {
|
|
417
478
|
messages: Accessor<readonly AiStoredMessage[]>;
|
|
418
479
|
activeTurn: Accessor<AiActiveTurn | null>;
|
|
480
|
+
/** The conversation stream lost its connection and reconnects, such as `streamStatus() === "reconnecting"`. */
|
|
481
|
+
reconnecting?: Accessor<boolean>;
|
|
419
482
|
};
|
|
420
483
|
|
|
421
484
|
/**
|
|
@@ -428,9 +491,12 @@ export function createAiChatTimeline(source: AiChatTimelineSource): Accessor<rea
|
|
|
428
491
|
const locale = useLocale();
|
|
429
492
|
const disclosureState = createAiToolDisclosureState();
|
|
430
493
|
const views = createTurnViews(disclosureState);
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
const
|
|
494
|
+
// While the stream reconnects or a model call waits for its retry, the live turn says so and its clock stands.
|
|
495
|
+
const reconnecting = createMemo(() => source.activeTurn()?.providerRetry === true || source.reconnecting?.() === true);
|
|
496
|
+
const clock = createActiveTurnClock(source, reconnecting);
|
|
497
|
+
const idle = createMemo(() => source.activeTurn() === null);
|
|
498
|
+
const stored = createMemo(() => storedItems(source.messages(), actions, views, locale(), idle()));
|
|
499
|
+
const active = createMemo(() => activeItems(source.activeTurn(), actions, views, clock, reconnecting(), locale()));
|
|
434
500
|
return createMemo(() => {
|
|
435
501
|
const items = [...stored(), ...active()];
|
|
436
502
|
views.retain(new Set(items.map((item) => item.id)));
|
|
@@ -12,7 +12,7 @@ export function isFailedTool(tool: Tool): boolean {
|
|
|
12
12
|
if (tool.isError || tool.status === "failed") return true;
|
|
13
13
|
const result = tool.result;
|
|
14
14
|
return (
|
|
15
|
-
["code_run", "code_inspect"
|
|
15
|
+
["code_run", "code_inspect"].includes(tool.name) &&
|
|
16
16
|
result !== null &&
|
|
17
17
|
typeof result === "object" &&
|
|
18
18
|
"status" in result &&
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AiTurnError } from "../types";
|
|
2
|
+
import { aiChatMessages } from "./messages";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether a new message can pick up the work. A full context, a used-up quota, or lost access would fail the next
|
|
6
|
+
* turn the same way; the reason then names the next step itself.
|
|
7
|
+
*/
|
|
8
|
+
export const aiTurnErrorCanContinue = (error: AiTurnError): boolean =>
|
|
9
|
+
error.code !== "context_full" && error.code !== "quota_exhausted" && error.code !== "not_allowed";
|
|
10
|
+
|
|
11
|
+
/** What the chat shows under the title: the reason, and that the results so far are kept when the work can go on. */
|
|
12
|
+
export const aiTurnErrorDescription = (error: AiTurnError, locale: string): string => {
|
|
13
|
+
const t = aiChatMessages(locale);
|
|
14
|
+
return aiTurnErrorCanContinue(error) ? `${t.turnErrorReason(error)} ${t.turnErrorKept}` : t.turnErrorReason(error);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** The turn's stored error for readers without the chat view, such as `cld`: the description and how to go on. */
|
|
18
|
+
export const aiTurnErrorText = (error: AiTurnError, locale: string): string =>
|
|
19
|
+
aiTurnErrorCanContinue(error)
|
|
20
|
+
? `${aiTurnErrorDescription(error, locale)} ${aiChatMessages(locale).turnErrorContinueHint}`
|
|
21
|
+
: aiTurnErrorDescription(error, locale);
|