@adecore/agents-react 0.41.1 → 0.43.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/dist/chat/actions.js +4 -0
- package/dist/chat/logic/lightbox.d.ts +31 -0
- package/dist/chat/logic/lightbox.js +61 -0
- package/dist/chat/logic/timeline.d.ts +1 -0
- package/dist/chat/logic/timeline.js +3 -0
- package/dist/chat/ui/AnsiOutput.d.ts +3 -2
- package/dist/chat/ui/AnsiOutput.js +26 -22
- package/dist/chat/ui/ImageView.d.ts +3 -0
- package/dist/chat/ui/ImageView.js +761 -437
- package/dist/chat/ui/intelligent-ui/renderers/content.js +4 -2
- package/dist/chat/ui/rows/MessageRows.js +10 -8
- package/dist/chat/ui/rows/Rows.js +42 -39
- package/dist/chat/ui/rows/WorkRows.d.ts +4 -2
- package/dist/chat/ui/rows/WorkRows.js +580 -211
- package/dist/chat/ui/rows/WorkflowRow.js +43 -41
- package/dist/host.d.ts +1 -0
- package/dist/locales/en/agent-chat.d.ts +9 -0
- package/dist/locales/en/agent-chat.js +11 -2
- package/dist/locales/nl/agent-chat.d.ts +9 -0
- package/dist/locales/nl/agent-chat.js +11 -2
- package/package.json +8 -8
- package/src/chat/actions.ts +1 -0
- package/src/chat/logic/lightbox.ts +88 -0
- package/src/chat/logic/timeline.ts +13 -2
- package/src/chat/ui/AnsiOutput.tsx +13 -2
- package/src/chat/ui/ImageView.tsx +255 -63
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +4 -2
- package/src/chat/ui/rows/MessageRows.tsx +10 -8
- package/src/chat/ui/rows/Rows.tsx +9 -1
- package/src/chat/ui/rows/WorkRows.tsx +127 -27
- package/src/chat/ui/rows/WorkflowRow.tsx +1 -1
- package/src/host.ts +2 -0
- package/src/locales/en/agent-chat.ts +11 -2
- package/src/locales/nl/agent-chat.ts +11 -2
|
@@ -26,11 +26,12 @@ import { useChatHost, type CommandLook } from '../../../host';
|
|
|
26
26
|
import { useCommandNames } from '../../../host-hooks';
|
|
27
27
|
import { lazyNamed } from '../../../lazy';
|
|
28
28
|
import { useChatActions } from '../../actions';
|
|
29
|
+
import { formatBytes } from '../../attachments';
|
|
29
30
|
import { ChatScopeContext, useChatScope } from '../../../scope';
|
|
30
31
|
import { useAwaitsApproval, useCurrentItem } from '../../../state/chats';
|
|
31
32
|
import { useTimelineFlash } from '../../timeline-flash';
|
|
32
33
|
import { formatClockDuration, formatElapsedShort } from '@adecore/ui/format';
|
|
33
|
-
import { Icon, LazyBoundary, MiddleTruncate, Spinner, useTickingText } from '@adecore/ui';
|
|
34
|
+
import { Button, Icon, LazyBoundary, MiddleTruncate, Spinner, useTickingText } from '@adecore/ui';
|
|
34
35
|
import { ROW_GUTTER, toolIcon } from '../icons';
|
|
35
36
|
import { AnsiOutput } from '../AnsiOutput';
|
|
36
37
|
import { RowTime } from '../RowTime';
|
|
@@ -251,36 +252,119 @@ function ChangedFileLine({
|
|
|
251
252
|
);
|
|
252
253
|
}
|
|
253
254
|
|
|
255
|
+
type WholeState<T> = { status: 'idle' } | { status: 'loading' } | { status: 'loaded'; value: T } | { status: 'failed' };
|
|
256
|
+
|
|
257
|
+
interface Whole<T> {
|
|
258
|
+
state: WholeState<T>;
|
|
259
|
+
/* Null where nothing can load it. */
|
|
260
|
+
load: (() => void) | null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* The whole of what the thread keeps only the beginning of, loaded once a person asks; a failure can be tried again. */
|
|
264
|
+
function useWhole<T>(fetch: (() => Promise<T>) | null): Whole<T> {
|
|
265
|
+
const [state, setState] = useState<WholeState<T>>({ status: 'idle' });
|
|
266
|
+
|
|
267
|
+
if (fetch === null) {
|
|
268
|
+
return { state, load: null };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const load = (): void => {
|
|
272
|
+
setState({ status: 'loading' });
|
|
273
|
+
|
|
274
|
+
fetch().then(
|
|
275
|
+
(value) => setState({ status: 'loaded', value }),
|
|
276
|
+
() => setState({ status: 'failed' })
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return { state, load };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* The line under a beginning that offers the rest: its size when known, and a retry after a failure. */
|
|
284
|
+
function WholeBar<T>({ whole, label, failedLabel, bytes }: { whole: Whole<T>; label: string; failedLabel: string; bytes?: number | undefined }) {
|
|
285
|
+
const { t } = useTranslation('agent-chat');
|
|
286
|
+
const { state, load } = whole;
|
|
287
|
+
|
|
288
|
+
if (load === null || state.status === 'loaded') {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const failed = state.status === 'failed';
|
|
293
|
+
const loading = state.status === 'loading';
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<div className="flex items-center gap-2 py-1.5 font-sans text-xs text-text-faint">
|
|
297
|
+
{failed && <span className="text-status-error">{failedLabel}</span>}
|
|
298
|
+
{!failed && bytes !== undefined && <span>{t('work.wholeSize', { size: formatBytes(bytes) })}</span>}
|
|
299
|
+
<Button size="xs" variant="secondary" disabled={loading} onClick={load}>
|
|
300
|
+
{loading && <Spinner size={12} />}
|
|
301
|
+
{failed ? t('work.retry') : label}
|
|
302
|
+
</Button>
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* What a call answered as far as the thread shows it, and the whole once a person asks: from the host when the item carries only the beginning. */
|
|
308
|
+
function useToolText(tool: ChatToolItem, chatId: string | undefined): { text: string | null; whole: Whole<string | null> } {
|
|
309
|
+
const { toolOutput } = useChatActions();
|
|
310
|
+
const stored = tool.outputRef !== undefined;
|
|
311
|
+
let fetch: (() => Promise<string | null>) | null = null;
|
|
312
|
+
|
|
313
|
+
if (stored && chatId !== undefined && toolOutput !== undefined) {
|
|
314
|
+
fetch = () => toolOutput(chatId, tool.id);
|
|
315
|
+
} else if (!stored && (tool.output?.length ?? 0) > OUTPUT_LIMIT) {
|
|
316
|
+
fetch = () => Promise.resolve(tool.output);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const whole = useWhole(fetch);
|
|
320
|
+
|
|
321
|
+
return { text: whole.state.status === 'loaded' ? whole.state.value : tool.output, whole };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* An output clipped while it is the beginning and whole once loaded. A beginning the host keeps the rest of counts no characters it cannot see. */
|
|
325
|
+
function OutputText({ text, whole, stored }: { text: string; whole: Whole<string | null>; stored: boolean }) {
|
|
326
|
+
const loaded = whole.state.status === 'loaded';
|
|
327
|
+
|
|
328
|
+
return <AnsiOutput text={text} limit={loaded ? undefined : OUTPUT_LIMIT} marksOmitted={loaded || !stored} />;
|
|
329
|
+
}
|
|
330
|
+
|
|
254
331
|
/* A diff keeps its card, with what the call answered under it. */
|
|
255
|
-
function DiffBody({ tool, entries, patched }: { tool: ChatToolItem; entries: readonly ReviewEntry[]; patched: boolean }) {
|
|
332
|
+
function DiffBody({ tool, entries, patched, chatId }: { tool: ChatToolItem; entries: readonly ReviewEntry[]; patched: boolean; chatId: string | undefined }) {
|
|
333
|
+
const { t } = useTranslation('agent-chat');
|
|
334
|
+
const { text, whole } = useToolText(tool, chatId);
|
|
335
|
+
|
|
256
336
|
return (
|
|
257
337
|
<div className="mt-1.5 mb-2 ml-6 overflow-hidden rounded-md border border-border bg-surface-raised">
|
|
258
338
|
<DiffStack entries={entries} />
|
|
259
|
-
{hasOutput(
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
339
|
+
{hasOutput(text) && !patched && (
|
|
340
|
+
<div className="border-t border-border px-3 py-2">
|
|
341
|
+
<pre
|
|
342
|
+
data-find-field="output"
|
|
343
|
+
className={clsx(
|
|
344
|
+
'max-h-64 overflow-auto whitespace-pre-wrap font-mono text-code select-text',
|
|
345
|
+
tool.state === 'error' ? 'text-chat-removed' : 'text-chat-output'
|
|
346
|
+
)}
|
|
347
|
+
>
|
|
348
|
+
<OutputText text={text} whole={whole} stored={tool.outputRef !== undefined} />
|
|
349
|
+
</pre>
|
|
350
|
+
<WholeBar whole={whole} label={t('work.showFullOutput')} failedLabel={t('work.fullOutputFailed')} bytes={tool.outputRef?.bytes} />
|
|
351
|
+
</div>
|
|
269
352
|
)}
|
|
270
353
|
</div>
|
|
271
354
|
);
|
|
272
355
|
}
|
|
273
356
|
|
|
274
357
|
/* What a call answered, muted under the call; a failure in the error color, or its exit code when the tool names one. */
|
|
275
|
-
function ToolOutput({ tool }: { tool: ChatToolItem }) {
|
|
358
|
+
function ToolOutput({ tool, chatId }: { tool: ChatToolItem; chatId: string | undefined }) {
|
|
276
359
|
const { t } = useTranslation('agent-chat');
|
|
360
|
+
const { text, whole } = useToolText(tool, chatId);
|
|
277
361
|
|
|
278
|
-
if (!hasOutput(
|
|
362
|
+
if (!hasOutput(text)) {
|
|
279
363
|
return null;
|
|
280
364
|
}
|
|
281
365
|
|
|
282
366
|
const failed = tool.state === 'error';
|
|
283
|
-
const { code, rest } = failed ? exitCodeOf(
|
|
367
|
+
const { code, rest } = failed ? exitCodeOf(text) : { code: null, rest: text };
|
|
284
368
|
|
|
285
369
|
return (
|
|
286
370
|
<>
|
|
@@ -289,21 +373,22 @@ function ToolOutput({ tool }: { tool: ChatToolItem }) {
|
|
|
289
373
|
data-find-field="output"
|
|
290
374
|
className={clsx('max-h-64 overflow-auto whitespace-pre-wrap', failed && code === null ? 'text-status-error' : 'text-text-muted')}
|
|
291
375
|
>
|
|
292
|
-
<
|
|
376
|
+
<OutputText text={rest} whole={whole} stored={tool.outputRef !== undefined} />
|
|
293
377
|
</pre>
|
|
294
378
|
)}
|
|
379
|
+
<WholeBar whole={whole} label={t('work.showFullOutput')} failedLabel={t('work.fullOutputFailed')} bytes={tool.outputRef?.bytes} />
|
|
295
380
|
{code !== null && <div className="text-status-error">{t('work.exitCode', { code: String(code) })}</div>}
|
|
296
381
|
</>
|
|
297
382
|
);
|
|
298
383
|
}
|
|
299
384
|
|
|
300
|
-
/* An opened call without a card: what it was asked in the foreground, what it answered muted below. */
|
|
301
|
-
export function ToolBody({ tool }: { tool: ChatToolItem }) {
|
|
385
|
+
/* An opened call without a card: what it was asked in the foreground, what it answered muted below. With `chatId` it can ask for an output the thread keeps only the beginning of. */
|
|
386
|
+
export function ToolBody({ tool, chatId }: { tool: ChatToolItem; chatId?: string | undefined }) {
|
|
302
387
|
const patches = unifiedChanges(tool);
|
|
303
388
|
const changes = patches.length > 0 ? [] : fileChanges(tool.name, tool.input);
|
|
304
389
|
|
|
305
390
|
if (patches.length + changes.length > 0) {
|
|
306
|
-
return <DiffBody tool={tool} entries={reviewEntries(patches, changes)} patched={patches.length > 0} />;
|
|
391
|
+
return <DiffBody tool={tool} entries={reviewEntries(patches, changes)} patched={patches.length > 0} chatId={chatId} />;
|
|
307
392
|
}
|
|
308
393
|
|
|
309
394
|
const lines = toolCallLines(tool.name, tool.input);
|
|
@@ -327,7 +412,7 @@ export function ToolBody({ tool }: { tool: ChatToolItem }) {
|
|
|
327
412
|
</dl>
|
|
328
413
|
)}
|
|
329
414
|
{lines.text !== null && <pre className="max-h-64 overflow-auto whitespace-pre-wrap wrap-anywhere text-text">{clip(lines.text)}</pre>}
|
|
330
|
-
<ToolOutput tool={tool} />
|
|
415
|
+
<ToolOutput tool={tool} chatId={chatId} />
|
|
331
416
|
</div>
|
|
332
417
|
);
|
|
333
418
|
}
|
|
@@ -380,7 +465,7 @@ export function WorkRow({
|
|
|
380
465
|
time={tool.createdAt}
|
|
381
466
|
/>
|
|
382
467
|
{image !== null && ReadImage !== null && <ReadImage path={image} />}
|
|
383
|
-
{open && opens && <ToolBody tool={tool} />}
|
|
468
|
+
{open && opens && <ToolBody tool={tool} chatId={chatId} />}
|
|
384
469
|
</div>
|
|
385
470
|
);
|
|
386
471
|
}
|
|
@@ -427,7 +512,7 @@ export function WorkLiveRow({ chatId, tool: derived }: { chatId: string; tool: C
|
|
|
427
512
|
}
|
|
428
513
|
live
|
|
429
514
|
/>
|
|
430
|
-
{open && opens && <ToolBody tool={tool} />}
|
|
515
|
+
{open && opens && <ToolBody tool={tool} chatId={chatId} />}
|
|
431
516
|
{tail !== null && !open && (
|
|
432
517
|
<pre className="mt-1.5 mb-2 ml-6 max-h-48 overflow-auto rounded-md border border-border bg-surface-raised px-3 py-2 font-mono text-code whitespace-pre-wrap text-chat-output select-text">
|
|
433
518
|
<AnsiOutput text={tail} tailLines={12} />
|
|
@@ -520,12 +605,22 @@ function omittedLabel(reason: 'binary' | 'too-large'): string {
|
|
|
520
605
|
return reason === 'binary' ? i18next.t('agent-chat:work.omitted.binary') : i18next.t('agent-chat:work.omitted.tooLarge');
|
|
521
606
|
}
|
|
522
607
|
|
|
523
|
-
/* One file of a turn's checkpoint diff: its patch,
|
|
524
|
-
function CheckpointFileBody({ file }: { file: ChatCheckpointFile }) {
|
|
608
|
+
/* One file of a turn's checkpoint diff: its patch, the reason there is none, or the offer of the whole diff where the thread held this one back. */
|
|
609
|
+
function CheckpointFileBody({ file, whole }: { file: ChatCheckpointFile; whole: Whole<ChatCheckpointDiff | null> | null }) {
|
|
610
|
+
const { t } = useTranslation('agent-chat');
|
|
611
|
+
|
|
525
612
|
if (file.omitted) {
|
|
526
613
|
return <div className="px-3 py-2 text-xs text-text-faint">{omittedLabel(file.omitted)}</div>;
|
|
527
614
|
}
|
|
528
615
|
|
|
616
|
+
if (whole !== null && file.diff === '') {
|
|
617
|
+
return (
|
|
618
|
+
<div className="px-3">
|
|
619
|
+
<WholeBar whole={whole} label={t('work.showFullDiff')} failedLabel={t('work.fullDiffFailed')} />
|
|
620
|
+
</div>
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
529
624
|
return (
|
|
530
625
|
<DiffSuspense>
|
|
531
626
|
<UnifiedDiff change={file} />
|
|
@@ -595,6 +690,7 @@ function ChangedFilesCard({
|
|
|
595
690
|
* checkpoint diff comes first (the working tree against the tree the turn started from), then the
|
|
596
691
|
* unified diffs the CLI reported, then the before and after of its own edits. A turn that has a
|
|
597
692
|
* checkpoint but no stored diff asks the host for one, which is also how a running turn gets it.
|
|
693
|
+
* A diff the thread keeps only the first files of (`heldBack`) asks for the whole when a person does.
|
|
598
694
|
*/
|
|
599
695
|
export function ChangedFilesRow({
|
|
600
696
|
tools,
|
|
@@ -602,7 +698,8 @@ export function ChangedFilesRow({
|
|
|
602
698
|
checkpoint,
|
|
603
699
|
baseMoved,
|
|
604
700
|
chatId,
|
|
605
|
-
turnId
|
|
701
|
+
turnId,
|
|
702
|
+
heldBack = false
|
|
606
703
|
}: {
|
|
607
704
|
tools: ChatToolItem[];
|
|
608
705
|
diff: ChatCheckpointDiff | null;
|
|
@@ -610,10 +707,13 @@ export function ChangedFilesRow({
|
|
|
610
707
|
baseMoved?: BaseMoved | undefined;
|
|
611
708
|
chatId: string;
|
|
612
709
|
turnId: string;
|
|
710
|
+
heldBack?: boolean;
|
|
613
711
|
}) {
|
|
614
712
|
const [open, setOpen] = useState<Record<string, boolean>>({});
|
|
615
713
|
const [fetched, setFetched] = useState<ChatCheckpointDiff | null>(null);
|
|
616
714
|
const actions = useChatActions();
|
|
715
|
+
const whole = useWhole(heldBack ? () => actions.turnDiff(chatId, turnId) : null);
|
|
716
|
+
const loaded = whole.state.status === 'loaded' ? whole.state.value : null;
|
|
617
717
|
|
|
618
718
|
useEffect(() => {
|
|
619
719
|
if (diff !== null || !checkpoint) {
|
|
@@ -636,7 +736,7 @@ export function ChangedFilesRow({
|
|
|
636
736
|
};
|
|
637
737
|
}, [actions, chatId, turnId, diff, checkpoint]);
|
|
638
738
|
|
|
639
|
-
const checkpointDiff = diff ?? fetched;
|
|
739
|
+
const checkpointDiff = loaded ?? diff ?? fetched;
|
|
640
740
|
const { keyOf } = useChatScope();
|
|
641
741
|
const flash = useTimelineFlash((s) => s.target);
|
|
642
742
|
const revealNonce = flash !== null && flash.opens && flash.key === keyOf(chatId) && flash.rowId === `files-${turnId}` ? flash.nonce : 0;
|
|
@@ -666,7 +766,7 @@ export function ChangedFilesRow({
|
|
|
666
766
|
</>
|
|
667
767
|
}
|
|
668
768
|
>
|
|
669
|
-
<CheckpointFileBody file={file} />
|
|
769
|
+
<CheckpointFileBody file={file} whole={loaded === null && heldBack ? whole : null} />
|
|
670
770
|
</ChangedFileLine>
|
|
671
771
|
))}
|
|
672
772
|
</ChangedFilesCard>
|
|
@@ -202,7 +202,7 @@ export function WorkflowRow({
|
|
|
202
202
|
</div>
|
|
203
203
|
{running && chatId && workflow.taskId && <IconButton icon={Square} size="xs" label={t('activity.stop')} onClick={stop} />}
|
|
204
204
|
</div>
|
|
205
|
-
{open && opens && <ToolBody tool={tool} />}
|
|
205
|
+
{open && opens && <ToolBody tool={tool} chatId={chatId} />}
|
|
206
206
|
<div className="mt-1.5 ml-6">
|
|
207
207
|
{workflow.name !== null && <div className="truncate text-sm font-medium text-text">{workflow.name}</div>}
|
|
208
208
|
{description !== null && <p className="mt-0.5 line-clamp-2 text-xs text-text-muted">{description}</p>}
|
package/src/host.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface ChatActions {
|
|
|
63
63
|
continueOn(chatId: string, account: string): Promise<void>;
|
|
64
64
|
/* Null when the turn has no checkpoint to compare against. */
|
|
65
65
|
turnDiff(chatId: string, turnId: string): Promise<ChatCheckpointDiff | null>;
|
|
66
|
+
/* The whole of what a tool call answered, where its item carries only the beginning; without it that beginning is all a person sees. */
|
|
67
|
+
toolOutput?(chatId: string, itemId: string): Promise<string | null>;
|
|
66
68
|
stopSubagent(chatId: string, toolUseId: string): Promise<void>;
|
|
67
69
|
stopTask(chatId: string, taskId: string): Promise<void>;
|
|
68
70
|
/* Pauses, resumes or clears the chat's goal; absent where the host offers none of it, which leaves the goal row without its buttons. */
|
|
@@ -217,7 +217,13 @@ export default {
|
|
|
217
217
|
alsoRan: '+ {{programs}}',
|
|
218
218
|
heredoc: 'heredoc',
|
|
219
219
|
moreRuns_one: '+{{count}}',
|
|
220
|
-
moreRuns_other: '+{{count}}'
|
|
220
|
+
moreRuns_other: '+{{count}}',
|
|
221
|
+
showFullOutput: 'Show full output',
|
|
222
|
+
showFullDiff: 'Show full diff',
|
|
223
|
+
wholeSize: '{{size}} in total',
|
|
224
|
+
fullOutputFailed: 'Could not load the full output',
|
|
225
|
+
fullDiffFailed: 'Could not load the full diff',
|
|
226
|
+
retry: 'Try again'
|
|
221
227
|
},
|
|
222
228
|
timeline: {
|
|
223
229
|
welcome: {
|
|
@@ -536,7 +542,10 @@ export default {
|
|
|
536
542
|
image: {
|
|
537
543
|
zoomIn: 'Zoom in',
|
|
538
544
|
zoomOut: 'Zoom out',
|
|
539
|
-
reset: 'Reset'
|
|
545
|
+
reset: 'Reset',
|
|
546
|
+
previous: 'Previous image',
|
|
547
|
+
next: 'Next image',
|
|
548
|
+
position: '{{index}} of {{count}}'
|
|
540
549
|
},
|
|
541
550
|
generatedImage: {
|
|
542
551
|
label: 'Generated image',
|
|
@@ -217,7 +217,13 @@ export default {
|
|
|
217
217
|
alsoRan: '+ {{programs}}',
|
|
218
218
|
heredoc: 'heredoc',
|
|
219
219
|
moreRuns_one: '+{{count}}',
|
|
220
|
-
moreRuns_other: '+{{count}}'
|
|
220
|
+
moreRuns_other: '+{{count}}',
|
|
221
|
+
showFullOutput: 'Volledige uitvoer tonen',
|
|
222
|
+
showFullDiff: 'Volledige diff tonen',
|
|
223
|
+
wholeSize: '{{size}} in totaal',
|
|
224
|
+
fullOutputFailed: 'De volledige uitvoer kon niet worden geladen',
|
|
225
|
+
fullDiffFailed: 'De volledige diff kon niet worden geladen',
|
|
226
|
+
retry: 'Opnieuw proberen'
|
|
221
227
|
},
|
|
222
228
|
timeline: {
|
|
223
229
|
welcome: {
|
|
@@ -536,7 +542,10 @@ export default {
|
|
|
536
542
|
image: {
|
|
537
543
|
zoomIn: 'Inzoomen',
|
|
538
544
|
zoomOut: 'Uitzoomen',
|
|
539
|
-
reset: 'Herstellen'
|
|
545
|
+
reset: 'Herstellen',
|
|
546
|
+
previous: 'Vorige afbeelding',
|
|
547
|
+
next: 'Volgende afbeelding',
|
|
548
|
+
position: '{{index}} van {{count}}'
|
|
540
549
|
},
|
|
541
550
|
generatedImage: {
|
|
542
551
|
label: 'Gegenereerde afbeelding',
|